@ux-aspects/ux-aspects
Version:
Open source user interface framework for building modern, responsive, mobile big data applications
31,432 lines • 1.82 MB
JavaScript
import { Observable, Subject, BehaviorSubject, ReplaySubject, merge, of, combineLatest, timer, fromEvent, Subscription, from, isObservable, concat } from 'rxjs';
import * as i0 from '@angular/core';
import { Directive, Injectable, InjectionToken, inject, RendererFactory2, ElementRef, NgZone, EventEmitter, Output, Input, HostBinding, Component, NgModule, Renderer2, PLATFORM_ID, HostListener, ContentChildren, ChangeDetectorRef, QueryList, ChangeDetectionStrategy, ContentChild, TemplateRef, ViewChild, ViewContainerRef, forwardRef, Pipe, ViewEncapsulation, LOCALE_ID, DestroyRef, ViewChildren, SecurityContext, Attribute, ComponentFactoryResolver, Injector, ApplicationRef, IterableDiffers } from '@angular/core';
import { takeUntil, filter, debounceTime, map, switchMap, take, pairwise, distinctUntilChanged, first, tap, delay, combineLatest as combineLatest$1, auditTime, mergeMap, skip, withLatestFrom, startWith } from 'rxjs/operators';
import { coerceBooleanProperty, coerceNumberProperty, coerceArray, coerceCssPixelValue } from '@angular/cdk/coercion';
import * as i1$1 from '@angular/cdk/a11y';
import { FocusMonitor, FocusKeyManager, A11yModule, LiveAnnouncer } from '@angular/cdk/a11y';
import * as i1 from '@angular/common';
import { isPlatformBrowser, CommonModule, WeekDay, formatDate, LocationStrategy, DOCUMENT, isPlatformServer } from '@angular/common';
import { Platform, PlatformModule } from '@angular/cdk/platform';
import * as i9 from 'angular-split';
import { SplitComponent, SplitAreaDirective, AngularSplitModule } from 'angular-split';
import { END, HOME, DOWN_ARROW, RIGHT_ARROW, UP_ARROW, LEFT_ARROW, TAB, ENTER, SPACE, ESCAPE, DELETE, BACKSPACE, PAGE_DOWN, PAGE_UP } from '@angular/cdk/keycodes';
import * as i2 from '@angular/router';
import { RouterModule, Router, NavigationEnd, ActivatedRoute, NavigationCancel, NavigationError } from '@angular/router';
import * as i2$2 from '@angular/forms';
import { NG_VALUE_ACCESSOR, FormsModule, FormGroupDirective, NG_VALIDATORS } from '@angular/forms';
import { trigger, transition, style, animate, query, stagger, state } from '@angular/animations';
import { Overlay, OverlayModule, ScrollDispatcher } from '@angular/cdk/overlay';
import { TemplatePortal, ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import * as i2$1 from '@angular/cdk/observers';
import { ObserversModule as ObserversModule$1 } from '@angular/cdk/observers';
import { ScrollDispatcher as ScrollDispatcher$1, ViewportRuler } from '@angular/cdk/scrolling';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import * as i6 from '@angular/cdk/drag-drop';
import { moveItemInArray, CdkDropList, CdkDragHandle, CDK_DRAG_HANDLE, CdkDrag, CDK_DRAG_PARENT, transferArrayItem, CDK_DROP_LIST, DragDropModule } from '@angular/cdk/drag-drop';
import { DomSanitizer } from '@angular/platform-browser';
import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { select, transition as transition$1, easeCubic, pointer, interpolate, arc, zoom, zoomTransform, hierarchy, tree, linkVertical, scaleLinear, partition, sum } from 'd3';
import { ArrayDataSource } from '@angular/cdk/collections';
import * as i5 from '@angular/cdk/tree';
import { FlatTreeControl, CdkTreeModule } from '@angular/cdk/tree';
import { AutofillMonitor } from '@angular/cdk/text-field';
var Color;
(function (Color) {
Color["Primary"] = "primary";
Color["Accent"] = "accent";
Color["Secondary"] = "secondary";
Color["Alternate1"] = "alternate1";
Color["Alternate2"] = "alternate2";
Color["Alternate3"] = "alternate3";
Color["Vibrant1"] = "vibrant1";
Color["Vibrant2"] = "vibrant2";
Color["Grey1"] = "grey1";
Color["Grey2"] = "grey2";
Color["Grey3"] = "grey3";
Color["Grey4"] = "grey4";
Color["Grey5"] = "grey5";
Color["Grey6"] = "grey6";
Color["Grey7"] = "grey7";
Color["Grey8"] = "grey8";
Color["Chart1"] = "chart1";
Color["Chart2"] = "chart2";
Color["Chart3"] = "chart3";
Color["Chart4"] = "chart4";
Color["Chart5"] = "chart5";
Color["Chart6"] = "chart6";
Color["Ok"] = "ok";
Color["Warning"] = "warning";
Color["Critical"] = "critical";
Color["Partition1"] = "partition1";
Color["Partition9"] = "partition9";
Color["Partition10"] = "partition10";
Color["Partition11"] = "partition11";
Color["Partition12"] = "partition12";
Color["Partition13"] = "partition13";
Color["Partition14"] = "partition14";
Color["SocialChartNode"] = "social-chart-node";
Color["SocialChartEdge"] = "social-chart-edge";
})(Color || (Color = {}));
/**
* Determine the type of icon based upon the identifier.
*
* We support the following iconset:
*
* - `ux-icon` - UX Icon Set
* - `component` - Component icon not tied to a specific set
*
* @param identifier - The name of the icon
*/
function getIconType(identifier) {
if (identifier && identifier.trim().indexOf('ux-') === 0) {
return IconType.UxIcon;
}
return IconType.Component;
}
var IconType;
(function (IconType) {
IconType["UxIcon"] = "ux-icon";
IconType["Component"] = "component";
})(IconType || (IconType = {}));
/**
* This is a simple RxJS operator to allow us to avoid the
* "expression has changed after it was checked issue"
* by making the subscription asynchronous. We could just use a
* delay operator but this uses a timeout which is significantly
* slower than using requestAnimationFrame.
*/
const tick = () => (source) => new Observable(subscriber => {
source.subscribe({
next(value) {
requestAnimationFrame(() => subscriber.next(value));
},
error(err) {
subscriber.error(err);
},
complete() {
subscriber.complete();
},
});
});
/**
* A button will trigger a click event whenever the a mouse click occurs or the enter key is pressed.
* These functions can be used to identify if a `click` event was caused by the keyboard or
* by a mouse.
*
* The `event.detail` property will change based on the source of the event.
* A mouse click will have varying values based on the browser, however
* the enter key will always have a value of `0` so we can check against that
*/
function isKeyboardTrigger(event) {
return event.detail === 0;
}
function isMouseTrigger(event) {
return !isKeyboardTrigger(event);
}
class AccordionPanelHeadingDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionPanelHeadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: AccordionPanelHeadingDirective, isStandalone: false, selector: "ux-accordion-panel-header", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionPanelHeadingDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-accordion-panel-header',
standalone: false,
}]
}] });
class AccordionService {
constructor() {
this.collapseOthers = false;
this.collapse = new Subject();
}
collapseAll() {
this.collapse.next();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionService, decorators: [{
type: Injectable
}] });
const ACCESSIBILITY_OPTIONS_TOKEN = new InjectionToken('ACCESSIBILITY_OPTIONS');
class AccessibilityOptionsService {
constructor() {
/** Get the user specified options - but handle cases where they may not be specified */
this._options = inject(ACCESSIBILITY_OPTIONS_TOKEN, { optional: true });
/** Determine the default options */
this._defaultOptions = {
mouseFocusIndicator: false,
touchFocusIndicator: false,
keyboardFocusIndicator: true,
programmaticFocusIndicator: false,
};
}
/** Get the complete options populating unspecified options with the default values */
get options() {
return { ...this._defaultOptions, ...this._options };
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityOptionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityOptionsService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityOptionsService, decorators: [{
type: Injectable
}] });
class LocalFocusIndicatorOptions {
}
class FocusIndicator {
/** Apply a class when the item is focused */
set isFocused(isFocused) {
// update the class on the element
isFocused
? this._renderer.addClass(this._element, 'ux-focus-indicator-active')
: this._renderer.removeClass(this._element, 'ux-focus-indicator-active');
// emit the focus state
this.isFocused$.next(isFocused);
}
/** Provide a convenience getter to allow access to focus state without a subscription */
get isFocused() {
return this.isFocused$.value;
}
constructor(_element, _focusMonitor, _renderer, _options, _focusIndicatorOrigin) {
this._element = _element;
this._focusMonitor = _focusMonitor;
this._renderer = _renderer;
this._options = _options;
this._focusIndicatorOrigin = _focusIndicatorOrigin;
/** An observable to monitor the focus state */
this.isFocused$ = new BehaviorSubject(false);
/** An observable to monitor the focus origin */
this.origin$ = new Subject();
/** Remove all subscriptions on destroy */
this._onDestroy = new Subject();
// check if the element is already being monitored
if (!_element.classList.contains('ux-focus-indicator')) {
this.initialise();
}
}
/** Setup the focus monitoring */
initialise() {
// add a class to the element to specify we are controlling the focus
this._renderer.addClass(this._element, 'ux-focus-indicator');
// watch for any changes to the focus state
this._focusMonitor
.monitor(this._element, this._options.checkChildren)
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onFocusChange.bind(this));
}
/** Focus the element with a specific origin */
focus(origin, options) {
this._focusIndicatorOrigin.setOrigin(origin);
this._element.focus(options);
}
/** Tear down the subscriptions */
destroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.isFocused$.complete();
this._focusMonitor.stopMonitoring(this._element);
}
/** Allow the options to be updates */
setOptions(options) {
this._options = { ...this._options, ...options };
}
/** Monitor changes to an elements focus state */
onFocusChange(origin) {
// if the origin is null then we blurred
if (origin === null) {
this.isFocused = false;
this.origin$.next(null);
return;
}
// get the origin if there is one
const syntheticOrigin = this._focusIndicatorOrigin.getOrigin();
// emit the origin
this.origin$.next(syntheticOrigin || origin);
switch (syntheticOrigin || origin) {
case 'mouse':
this.isFocused = this._options.mouseFocusIndicator;
break;
case 'touch':
this.isFocused = this._options.touchFocusIndicator;
break;
case 'keyboard':
this.isFocused = this._options.keyboardFocusIndicator;
break;
case 'program':
this.isFocused = this._options.programmaticFocusIndicator;
break;
default:
this.isFocused = false;
}
}
}
class FocusIndicatorOriginService {
/** Store the event source origin */
setOrigin(origin) {
this._origin = origin;
}
/** Get the most recent event origin */
getOrigin() {
// get the most recent origin if there is one
const origin = this._origin;
// we should clear the origin so this value doesn't cause issues with future focus events
this._origin = null;
return origin;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOriginService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOriginService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOriginService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class FocusIndicatorService {
constructor() {
this._localOptions = inject(ACCESSIBILITY_OPTIONS_TOKEN, { optional: true });
this.rendererFactory = inject(RendererFactory2);
this._focusMonitor = inject(FocusMonitor);
this._globalOptions = inject(AccessibilityOptionsService);
this._focusIndicatorOrigin = inject(FocusIndicatorOriginService);
// programmatically create a renderer as it can't be injected into a service
this._renderer = this.rendererFactory.createRenderer(null, null);
}
/** This is essentially just a factory method to prevent the user having to pass in focus monitor, renderer and global options each time */
monitor(element, options = {
...this._globalOptions.options,
...this._localOptions,
checkChildren: false,
}) {
return new FocusIndicator(element, this._focusMonitor, this._renderer, { ...this._globalOptions.options, ...this._localOptions, ...options }, this._focusIndicatorOrigin);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class FocusIndicatorDirective {
/** Specify whether or not we should mark this element as having focus if a child is focused */
set checkChildren(checkChildren) {
// allow a string to be used so we can skip checking a binding for performance benefits
checkChildren = coerceBooleanProperty(checkChildren);
if (checkChildren !== null && checkChildren !== undefined) {
this._checkChildren = checkChildren;
this.setOptions();
}
}
/** Indicate whether or not mouse events should cause the focus indicator to appear - will override any global setting */
set mouseFocusIndicator(mouseFocusIndicator) {
// allow a string to be used so we can skip checking a binding for performance benefits
mouseFocusIndicator = coerceBooleanProperty(mouseFocusIndicator);
if (mouseFocusIndicator !== null && mouseFocusIndicator !== undefined) {
this._options.set('mouseFocusIndicator', mouseFocusIndicator);
this.setOptions();
}
}
/** Indicate whether or not touch events should cause the focus indicator to appear - will override any global setting */
set touchFocusIndicator(touchFocusIndicator) {
// allow a string to be used so we can skip checking a binding for performance benefits
touchFocusIndicator = coerceBooleanProperty(touchFocusIndicator);
if (touchFocusIndicator !== null && touchFocusIndicator !== undefined) {
this._options.set('touchFocusIndicator', touchFocusIndicator);
this.setOptions();
}
}
/** Indicate whether or not keyboard events should cause the focus indicator to appear - will override any global setting */
set keyboardFocusIndicator(keyboardFocusIndicator) {
// allow a string to be used so we can skip checking a binding for performance benefits
keyboardFocusIndicator = coerceBooleanProperty(keyboardFocusIndicator);
if (keyboardFocusIndicator !== null && keyboardFocusIndicator !== undefined) {
this._options.set('keyboardFocusIndicator', keyboardFocusIndicator);
this.setOptions();
}
}
/** Indicate whether or not programmatic events should cause the focus indicator to appear - will override any global setting */
set programmaticFocusIndicator(programmaticFocusIndicator) {
// allow a string to be used so we can skip checking a binding for performance benefits
programmaticFocusIndicator = coerceBooleanProperty(programmaticFocusIndicator);
if (programmaticFocusIndicator !== null && programmaticFocusIndicator !== undefined) {
this._options.set('programmaticFocusIndicator', programmaticFocusIndicator);
this.setOptions();
}
}
constructor() {
this.optionsService = inject(AccessibilityOptionsService);
this._elementRef = inject(ElementRef);
this._focusIndicatorService = inject(FocusIndicatorService);
this._ngZone = inject(NgZone);
this.localOptions = inject(LocalFocusIndicatorOptions, { optional: true });
/** Emit the latest focus state */
this.indicator = new EventEmitter();
/** Store a private reference for the checkChildren option */
this._checkChildren = false;
/** Store all configuation options*/
this._options = new Map();
/** Unsubscribe on component destroy */
this._onDestroy = new Subject();
// set the inital option values based on global options
for (const option in this.optionsService.options || {}) {
this._options.set(option, this.optionsService.options[option]);
}
// set the inital option values based on local options (if there are any)
for (const option in this.localOptions || {}) {
this._options.set(option, this.localOptions[option]);
}
}
/** Setup the focus monitoring */
ngOnInit() {
// start the focus monitoring
this._focusIndicator = this._focusIndicatorService.monitor(this._elementRef.nativeElement, {
checkChildren: this._checkChildren,
mouseFocusIndicator: this._options.get('mouseFocusIndicator'),
touchFocusIndicator: this._options.get('touchFocusIndicator'),
keyboardFocusIndicator: this._options.get('keyboardFocusIndicator'),
programmaticFocusIndicator: this._options.get('programmaticFocusIndicator'),
});
// subscribe to the focus state to emit an event on change
this._focusIndicator.isFocused$.pipe(takeUntil(this._onDestroy)).subscribe(isFocused => {
// emit the latest value
this._ngZone.run(() => this.indicator.emit(isFocused));
});
}
/** Tear down the directive */
ngOnDestroy() {
if (this._focusIndicator) {
this._focusIndicator.destroy();
}
// unsubscribe from all observables
this._onDestroy.next();
this._onDestroy.complete();
}
/** Focus this element with a specific origin */
focus(origin, options) {
this._focusIndicator.focus(origin, options);
}
/** Update the focus indicator with the latest options */
setOptions() {
if (this._focusIndicator) {
this._focusIndicator.setOptions({
checkChildren: this._checkChildren,
mouseFocusIndicator: this._options.get('mouseFocusIndicator'),
touchFocusIndicator: this._options.get('touchFocusIndicator'),
keyboardFocusIndicator: this._options.get('keyboardFocusIndicator'),
programmaticFocusIndicator: this._options.get('programmaticFocusIndicator'),
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FocusIndicatorDirective, isStandalone: false, selector: "[uxFocusIndicator]", inputs: { checkChildren: "checkChildren", mouseFocusIndicator: "mouseFocusIndicator", touchFocusIndicator: "touchFocusIndicator", keyboardFocusIndicator: "keyboardFocusIndicator", programmaticFocusIndicator: "programmaticFocusIndicator" }, outputs: { indicator: "indicator" }, exportAs: ["ux-focus-indicator"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFocusIndicator]',
exportAs: 'ux-focus-indicator',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { checkChildren: [{
type: Input
}], mouseFocusIndicator: [{
type: Input
}], touchFocusIndicator: [{
type: Input
}], keyboardFocusIndicator: [{
type: Input
}], programmaticFocusIndicator: [{
type: Input
}], indicator: [{
type: Output
}] } });
let uniqueId$e = 1;
class AccordionPanelComponent {
constructor() {
this.accordion = inject(AccordionService);
this.panelId = `ux-accordion-panel-${uniqueId$e++}`;
this.headingId = `${this.panelId}-heading`;
this.disabled = false;
this.expanded = false;
this.expandedChange = new EventEmitter();
this._onDestroy = new Subject();
}
ngOnInit() {
this.accordion.collapse.pipe(takeUntil(this._onDestroy)).subscribe(() => this.collapse());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
toggle() {
if (this.expanded) {
this.collapse();
return;
}
// check if we should collapse others
if (this.accordion.collapseOthers) {
this.accordion.collapseAll();
}
// store the new expanded state
this.expand();
}
expand() {
if (this.disabled === false && this.expanded === false) {
this.expanded = true;
this.expandedChange.next(true);
}
}
collapse() {
if (this.disabled === false && this.expanded === true) {
this.expanded = false;
this.expandedChange.next(false);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AccordionPanelComponent, isStandalone: false, selector: "ux-accordion-panel", inputs: { panelId: "panelId", headingId: "headingId", disabled: "disabled", heading: "heading", expanded: "expanded" }, outputs: { expandedChange: "expandedChange" }, host: { properties: { "class.panel-open": "this.expanded" }, classAttribute: "panel panel-default" }, ngImport: i0, template: "<div\n class=\"panel-heading\"\n role=\"button\"\n uxFocusIndicator\n [tabindex]=\"disabled ? -1 : 0\"\n [id]=\"headingId\"\n [attr.aria-expanded]=\"expanded\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (keydown.spacebar)=\"toggle(); $event.preventDefault()\"\n>\n <div class=\"panel-title\">\n {{ heading }}\n <ng-content select=\"ux-accordion-panel-header\"></ng-content>\n </div>\n</div>\n\n<div\n [id]=\"panelId\"\n class=\"panel-collapse collapse\"\n [class.in]=\"expanded\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"headingId\"\n>\n <div class=\"panel-body\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionPanelComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-accordion-panel', host: {
class: 'panel panel-default',
}, standalone: false, template: "<div\n class=\"panel-heading\"\n role=\"button\"\n uxFocusIndicator\n [tabindex]=\"disabled ? -1 : 0\"\n [id]=\"headingId\"\n [attr.aria-expanded]=\"expanded\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (keydown.spacebar)=\"toggle(); $event.preventDefault()\"\n>\n <div class=\"panel-title\">\n {{ heading }}\n <ng-content select=\"ux-accordion-panel-header\"></ng-content>\n </div>\n</div>\n\n<div\n [id]=\"panelId\"\n class=\"panel-collapse collapse\"\n [class.in]=\"expanded\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"headingId\"\n>\n <div class=\"panel-body\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
}], propDecorators: { panelId: [{
type: Input
}], headingId: [{
type: Input
}], disabled: [{
type: Input
}], heading: [{
type: Input
}], expanded: [{
type: Input
}, {
type: HostBinding,
args: ['class.panel-open']
}], expandedChange: [{
type: Output
}] } });
class AccordionComponent {
constructor() {
this._accordion = inject(AccordionService);
}
set collapseOthers(collapseOthers) {
this._accordion.collapseOthers = collapseOthers;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AccordionComponent, isStandalone: false, selector: "ux-accordion", inputs: { collapseOthers: "collapseOthers" }, host: { attributes: { "aria-multiselectable": "true" }, classAttribute: "panel-group" }, providers: [AccordionService], ngImport: i0, template: "<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-accordion', providers: [AccordionService], host: {
class: 'panel-group',
'aria-multiselectable': 'true',
}, standalone: false, template: "<ng-content></ng-content>\n" }]
}], propDecorators: { collapseOthers: [{
type: Input
}] } });
const KEPPEL_COLOR_SET = {
primary: '#00a7a2',
accent: '#7b63a3',
secondary: '#fff',
alternate1: '#3baa43',
alternate2: '#025662',
alternate3: '#b08f5c',
vibrant1: '#00cceb',
vibrant2: '#ff9048',
grey1: '#2a2a2a',
grey2: '#333',
grey3: '#666',
grey4: '#999',
grey5: '#ccc',
grey6: '#eee',
grey7: '#f5f5f5',
grey8: '#fafafa',
chart1: '#00a7a2',
chart2: '#7b63a3',
chart3: '#3baa43',
chart4: '#025662',
chart5: '#b08f5c',
chart6: '#ccc',
ok: '#3baa43',
warning: '#ff9048',
critical: '#ff454f',
partition1: '#635387',
partition9: '#4a4066',
partition10: '#308935',
partition11: '#023e42',
partition12: '#91744d',
partition13: '#999',
partition14: '#294266',
'social-chart-node': '#00cceb',
'social-chart-edge': '#00cceb',
};
const MICRO_FOCUS_COLOR_SET = {
'brand-blue': '#0073e7',
cerulean: '#1668c1',
aqua: '#29ceff',
aquamarine: '#2fd6c3',
fuchsia: '#c6179d',
indigo: '#7425ad',
'dark-blue': '#231ca5',
white: '#ffffff',
'slightly-gray': '#f5f7f8',
'bright-gray': '#f1f2f3',
gray: '#dcdedf',
silver: '#bdbec0',
'dim-gray': '#656668',
'dark-gray': '#323435',
black: '#000000',
'crimson-negative': '#e5004c',
apricot: '#f48b34',
yellow: '#fcdb1f',
'green-positive': '#1aac60',
ultramarine: '#3939c6',
skyblue: '#00abf3',
'pale-aqua': '#43e4ff',
'pale-green': '#1ffbba',
lime: '#75da4d',
orange: '#ffce00',
magenta: '#eb23c2',
'pale-purple': '#ba47e2',
'dark-ultramarine': '#271782',
steelblue: '#014272',
'arctic-blue': '#0b8eac',
emerald: '#00a989',
olive: '#5bba36',
goldenrod: '#ffb000',
purple: '#9b1e83',
'pale-eggplant': '#5216ac',
red: '#ff454f',
'pale-amber': '#ffb24d',
'pale-lemon': '#fde159',
'pale-emerald': '#33c180',
plum: '#b21646',
copper: '#e57828',
amber: '#ffc002',
'leaf-green': '#118c4f',
'forest-green': '#00645a',
primary: '#0073e7',
accent: '#7425ad',
secondary: '#ffffff',
alternate1: '#29ceff',
alternate2: '#2fd6c3',
alternate3: '#c6179d',
vibrant1: '#43e4ff',
vibrant2: '#ffce00',
grey1: '#000000',
grey2: '#323435',
grey3: '#656668',
grey4: '#bdbec0',
grey5: '#dcdedf',
grey6: '#f1f2f3',
grey7: '#f5f7f8',
grey8: '#ffffff',
chart1: '#3939c6',
chart2: '#00abf3',
chart3: '#75da4d',
chart4: '#ffce00',
chart5: '#eb23c2',
chart6: '#ba47e2',
info: '#00abf3',
ok: '#1aac60',
warning: '#fcdb1f',
danger: '#f48b34',
critical: '#e5004c',
partition1: '#7425ad',
partition9: '#5216ac',
partition10: '#5bba36',
partition11: '#014272',
partition12: '#ffb000',
partition13: '#bdbec0',
partition14: '#271782',
'social-chart-node': '#ff00ff',
'social-chart-edge': '#ff00ff',
};
const colorSets = {
keppel: {
colorValueSet: KEPPEL_COLOR_SET,
},
microFocus: {
colorValueSet: MICRO_FOCUS_COLOR_SET,
},
};
/** Provide a default color set for an application */
const COLOR_SET_TOKEN = new InjectionToken('COLOR_SET_TOKEN');
class ThemeColor {
constructor(_r, _g, _b, _a = '1') {
this._r = _r;
this._g = _g;
this._b = _b;
this._a = _a;
}
/**
* Create a ThemeColor object from a CSS color string
* @param value The CSS color string to derive a ThemeColor object from
*/
static parse(value) {
let r, g, b, a = '1';
const rgbaPattern = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/;
const shortHexPattern = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const longHexPattern = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
const rgbaMatch = value.match(rgbaPattern);
const shortHexMatch = value.match(shortHexPattern);
const longHexMatch = value.match(longHexPattern);
if (rgbaMatch) {
r = rgbaMatch[1];
g = rgbaMatch[2];
b = rgbaMatch[3];
a = rgbaMatch[4] ? rgbaMatch[4] : '1';
}
else if (longHexMatch) {
r = parseInt(longHexMatch[1], 16).toString();
g = parseInt(longHexMatch[2], 16).toString();
b = parseInt(longHexMatch[3], 16).toString();
}
else if (shortHexMatch) {
r = parseInt(shortHexMatch[1] + shortHexMatch[1], 16).toString();
g = parseInt(shortHexMatch[2] + shortHexMatch[2], 16).toString();
b = parseInt(shortHexMatch[3] + shortHexMatch[3], 16).toString();
}
else {
throw new Error(`Cannot parse color - ${value} is not a valid color.`);
}
return new ThemeColor(r, g, b, a);
}
/**
* Clone a theme color so it can be modified without affecting other places using the color
* @param themeColor The original theme color to clone
*/
static from(themeColor) {
return new ThemeColor(themeColor.getRed(), themeColor.getGreen(), themeColor.getBlue(), themeColor.getAlpha());
}
/**
* Determine if an object is an instance of a theme color.
* Using a simple instanceof check will not always work in plunker
* where the ThemeColor is from @ux-aspects/ux-aspects and the color
* comes from @micro-focus/ux-aspects
*/
static isInstanceOf(themeColor) {
return (typeof themeColor === 'object' &&
// eslint-disable-next-line no-prototype-builtins
themeColor.hasOwnProperty('_r') &&
// eslint-disable-next-line no-prototype-builtins
themeColor.hasOwnProperty('_g') &&
// eslint-disable-next-line no-prototype-builtins
themeColor.hasOwnProperty('_b') &&
// eslint-disable-next-line no-prototype-builtins
themeColor.hasOwnProperty('_a'));
}
/**
* Convert the theme color to a CSS hex color code
*/
toHex() {
let red = parseInt(this._r).toString(16);
let green = parseInt(this._g).toString(16);
let blue = parseInt(this._b).toString(16);
if (red.length < 2) {
red = '0' + red;
}
if (green.length < 2) {
green = '0' + green;
}
if (blue.length < 2) {
blue = '0' + blue;
}
return '#' + red + green + blue;
}
/**
* Convert the theme color to a CSS rgb color code
*/
toRgb() {
return 'rgb(' + this._r + ', ' + this._g + ', ' + this._b + ')';
}
/**
* Convert the theme color to a CSS rgbs color code
*/
toRgba() {
return 'rgba(' + this._r + ', ' + this._g + ', ' + this._b + ', ' + this._a + ')';
}
/**
* Get the red value from the RGBA color value
*/
getRed() {
return this._r;
}
/**
* Get the green value from the RGBA color value
*/
getGreen() {
return this._g;
}
/**
* Get the blue value from the RGBA color value
*/
getBlue() {
return this._b;
}
/**
* Get the alpha value from the RGBA color value
*/
getAlpha() {
return this._a;
}
/**
* Set the red value from the RGBA color value
*/
setRed(red) {
this._r = red;
return this;
}
/**
* Set the green value from the RGBA color value
*/
setGreen(green) {
this._g = green;
return this;
}
/**
* Set the blue value from the RGBA color value
*/
setBlue(blue) {
this._b = blue;
return this;
}
/**
* Set the alpha value from the RGBA color value
*/
setAlpha(alpha) {
this._a = alpha.toString();
return this;
}
}
class ColorService {
/** Allow the color set to be provided in a forRoot function otherwise set it to the Keppel theme by default */
constructor() {
this._colorSet = inject(COLOR_SET_TOKEN, { optional: true });
// resolve the theme based on the colorset
this._theme = this.getTheme(this._colorSet);
}
/**
* Get a ThemeColor object from a color name
* @param colorName The name of the color from the color palette
*/
getColor(colorName) {
// get the matching ThemeColor from the active theme
const themeColor = this._theme[this.resolveColorName(colorName)];
// if there is not a match then throw an error
if (!themeColor) {
throw new Error('Color not found: ' + colorName);
}
return new ThemeColor(themeColor.getRed(), themeColor.getGreen(), themeColor.getBlue(), themeColor.getAlpha());
}
/**
* Get the active color set
*/
getColorSet() {
return this._colorSet;
}
/**
* Define the current color set and produce a Theme from it
*/
setColorSet(colorSet) {
this._colorSet = colorSet;
this._theme = this.getTheme(colorSet);
}
/**
* Resolve a color value. This may be the name of a color from the color set
* or it may simply be a hex or rgb(a) color value. This function will return
* a CSS color value regardless of which one of these formats it is
* @param value The color name, hex code or rgb(a) value to resolve
* @returns If the color is the name of a color in the set, the `rgba` color will be returned, otherwise the original CSS value will be returned.
*/
resolve(value) {
if (!value) {
return;
}
const colorName = this.resolveColorName(value);
for (const color in this._theme) {
if (colorName === color.toLowerCase()) {
return this.getColor(colorName).toRgba();
}
}
return value;
}
/**
* Converts a color name to an appropriate ColorSet name. For example
* a color may be written in lower-camel-case, however color sets are in
* kebab-case. This will convert to the appropriate naming format
* @param colorName The color name to resolve
*/
resolveColorName(colorName = '') {
return colorName.replace(/\s+/g, '-').toLowerCase();
}
/** Determine if the current colorset has a specific color */
colorExists(name) {
return !!Object.keys(this._theme).find(colorName => colorName === this.resolveColorName(name));
}
/** Create a theme from a colorset */
getTheme(colorSet) {
// create a new theme object
const theme = {};
// ensure we have a colorset
if (!colorSet) {
colorSet = colorSets.keppel;
}
// iterate over each hex code and convert it to a theme color
for (const color in colorSet.colorValueSet) {
theme[color] = ThemeColor.parse(colorSet.colorValueSet[color]);
}
return theme;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class ColorServiceModule {
/**
* The function allows the consuming applications to specify the applications
* color set once in the app module, eg:
* ```
* ColorServiceModule.forRoot(colorSets.microFocus);
* ```
* @param colorSet The color set the application should use
*/
static forRoot(colorSet) {
return {
ngModule: ColorServiceModule,
providers: [
{ provide: COLOR_SET_TOKEN, useValue: colorSet ? colorSet : colorSets.keppel },
ColorService,
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorServiceModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ColorServiceModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorServiceModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorServiceModule, decorators: [{
type: NgModule,
args: [{}]
}] });
class ContrastService {
/**
* Calculate the contract ratio between two colors.
* This uses the official WCAG Color Contrast Ratio
* Algorithm: https://www.w3.org/TR/WCAG20-TECHS/G17.html
*/
getContrastColor(backgroundColor, lightColor, darkColor) {
// get a ThemeColor from the ColorPickerColor
const themeColor = ThemeColor.parse(backgroundColor.toHex());
const background = this.getLuminance(themeColor);
const light = this.getLuminance(lightColor);
const dark = this.getLuminance(darkColor);
// determine the contrast for both black and white
const whiteContrast = (light + 0.05) / (background + 0.05);
const blackContrast = (background + 0.05) / (dark + 0.05);
// return the color with the most contrast ratio
return blackContrast > whiteContrast ? darkColor : lightColor;
}
getLuminance(color) {
// normalize the colors
let r = +color.getRed() / 255;
let g = +color.getGreen() / 255;
let b = +color.getBlue() / 255;
// calculate the value required for each color component
r = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
g = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
b = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
// return the luminance
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContrastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContrastService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ContrastService, decorators: [{
type: Injectable
}] });
class ColorContrastDirective {
constructor() {
this._colorService = inject(ColorService);
this._contrastService = inject(ContrastService);
/** Store the light color as a ThemeColor object */
this._lightColor = ThemeColor.parse('#fff');
/** Store the light color as a ThemeColor object */
this._darkColor = ThemeColor.parse('#000');
}
/**
* Define the background color for contrast comparison.
* This can be a CSS color value or the name of a
* color from the color palette.
*/
set uxColorContrast(backgroundColor) {
this._backgroundColor = ThemeColor.parse(this._colorService.resolve(backgroundColor));
}
/**
* Define the light color for contrast comparison.
* This can be a CSS color value or the name of a
* color from the color palette.
*/
set lightColor(lightColor) {
this._lightColor = ThemeColor.parse(this._colorService.resolve(lightColor));
}
/**
* Define the dark color for contrast comparison.
* This can be a CSS color value or the name of a
* color from the color palette.
*/
set darkColor(darkColor) {
this._darkColor = ThemeColor.parse(this._colorService.resolve(darkColor));
}
/** Determine the color to set based on the supplied parameters */
get _color() {
return this._backgroundColor
? this._contrastService
.getContrastColor(this._backgroundColor, this._lightColor, this._darkColor)
.toRgba()
: null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorContrastDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ColorContrastDirective, isStandalone: false, selector: "[uxColorContrast]", inputs: { uxColorContrast: "uxColorContrast", lightColor: "lightColor", darkColor: "darkColor" }, host: { properties: { "style.color": "this._color" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorContrastDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxColorContrast]',
standalone: false,
}]
}], propDecorators: { uxColorContrast: [{
type: Input
}], lightColor: [{
type: Input
}], darkColor: [{
type: Input
}], _color: [{
type: HostBinding,
args: ['style.color']
}] } });
/**
* This directive can be used to target specific elements based on their CSS
* class so we can control when the focus shows. This will help prevent us
* polluting the FocusIndicatorDirective with an lot of selectors.
*
* If the button has a uxFocusIndicator, uxMenuTriggerFor or uxMenuNavigationToggle directive applied we should skip this
*/
class DefaultFocusIndicatorDirective extends FocusIndicatorDirective {
constructor() {
super();
// Enable programmatic focus by default
this.programmaticFocusIndicator = true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DefaultFocusIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DefaultFocusIndicatorDirective, isStandalone: false, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])", usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DefaultFocusIndicatorDirective, decorators: [{
type: Directive,
args: [{
selector: '.btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])',
standalone: false,
}]
}], ctorParameters: () => [] });
class FocusIndicatorOptionsDirective {
constructor() {
this._options = inject(LocalFocusIndicatorOptions, { self: true });
}
/** If `true`, this element will receive a focus indicator when the element is clicked on. */
set mouseFocusIndicator(mouseFocusIndicator) {
this._options.mouseFocusIndicator = mouseFocusIndicator;
}
/** If `true`, this element will receive a focus indicator when the element is touched. */
set touchFocusIndicator(touchFocusIndicator) {
this._options.touchFocusIndicator = touchFocusIndicator;
}
/** If `true`, this element will receive a focus indicator when the element is focused using the keyboard. */
set keyboardFocusIndicator(keyboardFocusIndicator) {
this._options.keyboardFocusIndicator = keyboardFocusIndicator;
}
/** If `true`, this element will receive a focus indicator when the element is programmatically focused. */
set programmaticFocusIndicator(programmaticFocusIndicator) {
this._options.programmaticFocusIndicator = programmaticFocusIndicator;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOptionsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FocusIndicatorOptionsDirective, isStandalone: false, selector: "[uxFocusIndicatorOptions]", inputs: { mouseFocusIndicator: "mouseFocusIndicator", touchFocusIndicator: "touchFocusIndicator", keyboardFocusIndicator: "keyboardFocusIndicator", programmaticFocusIndicator: "programmaticFocusIndicator" }, providers: [LocalFocusIndicatorOptions], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOptionsDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFocusIndicatorOptions]',
providers: [LocalFocusIndicatorOptions],
standalone: false,
}]
}], propDecorators: { mouseFocusIndicator: [{
type: Input
}], touchFocusIndicator: [{
type: Input
}], keyboardFocusIndicator: [{
type: Input
}], programmaticFocusIndicator: [{
type: Input
}] } });
/**
* When working with component host elements
* we cannot apply directives, eg. FocusIndicatorOriginDirective
* however we may still want the functionality to be applied to
* the host element. This class allows the host element to become
* a focus indicator origin
*/
class FocusIndicatorOrigin {
constructor(_focusIndicatorOrigin, elementRef, renderer) {
this._focusIndicatorOrigin = _focusIndicatorOrigin;
/** Store all event handlers */
this._handlers = [];
// add event handlers
this._handlers = [
renderer.listen(elementRef.nativeElement, 'click', () => this.onClick()),
renderer.listen(elementRef.nativeElement, 'mousedown', () => this.onMouseDown()),
renderer.listen(elementRef.nativeElement, 'keydown', () => this.onKeydown()),
];
}
/** Remove all event handlers */
destroy() {
this._handlers.forEach(handler => handler());
}
onMouseDown() {
this._isMouseEvent = true;
}
onClick() {
// if the click was triggered after a mousedown event then it is a keyboard event
this._focusIndicatorOrigin.setOrigin(this._isMouseEvent ? 'mouse' : 'keyboard');
// reset the mouse event flag
this._isMouseEvent = false;
}
onKeydown() {
this._isMouseEvent = false;
this._focusIndicatorOrigin.setOrigin('keyboard');
}
}
class FocusIndicatorOriginDirective {
constructor() {
this.focusOriginService = inject(FocusIndicatorOriginService);
this.elementRef = inject(ElementRef);
this.renderer = inject(Renderer2);
this._focusOrigin = new FocusIndicatorOrigin(this.focusOriginService, this.elementRef, this.renderer);
}
ngOnDestroy() {
this._focusOrigin.destroy();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOriginDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FocusIndicatorOriginDirective, isStandalone: false, selector: "[uxFocusIndicatorOrigin]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIndicatorOriginDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFocusIndicatorOrigin]',
standalone: false,
}]
}], ctorParameters: () => [] });
class FocusWithinDirective {
/**
* Note: We used to use @angular/cdk FocusMonitor here instead of manually listening
* to focus blur events, however this was problematic as any child elements using the FocusMonitor,
* eg: `uxFocusIndicator` which not get the correct `origin`, they will instead get a programmatic
* origin even if it was clicked or focused via the keyboard.
*/
constructor() {
this._elementRef = inject(ElementRef);
/** Emits when a child element gains focus */
this.uxFocusWithin = new EventEmitter();
/** Emits when a child element loses focus */
this.uxBlurWithin = new EventEmitter();
// We need to listen in capture phase since focus events don't bubble.
this._elementRef.nativeElement.addEventListener('focus', this.onFocus.bind(this), true);
this._elementRef.nativeElement.addEventListener('blur', this.onBlur.bind(this), true);
}
ngOnDestroy() {
this._elementRef.nativeElement.removeEventListener('focus', this.onFocus.bind(this), true);
this._elementRef.nativeElement.removeEventListener('blur', this.onBlur.bind(this), true);
}
onFocus() {
this.uxFocusWithin.emit();
}
onBlur() {
this.uxBlurWithin.emit();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusWithinDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FocusWithinDirective, isStandalone: false, selector: "[uxFocusWithin],[uxBlurWithin]", outputs: { uxFocusWithin: "uxFocusWithin", uxBlurWithin: "uxBlurWithin" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusWithinDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFocusWithin],[uxBlurWithin]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxFocusWithin: [{
type: Output
}], uxBlurWithin: [{
type: Output
}] } });
class ManagedFocusContainerService {
constructor() {
this.rendererFactory = inject(RendererFactory2);
this._containers = [];
// programmatically create a renderer as it can't be injected into a service
this._renderer = this.rendererFactory.createRenderer(null, null);
}
/**
* Create or get an existing object which manages the tabindex of descendants.
* @param element The element containing focusable descendants.
* @param component The component requesting the managed focus container.
*/
register(element, component) {
// Only create a new instance if no other component has created a container on the same element
let containerRef = this._containers.find(ref => ref.container.element.isEqualNode(element));
if (!containerRef) {
containerRef = new ManagedFocusContainerWithReferences(new ManagedFocusContainer(element, this._renderer));
this._containers.push(containerRef);
// Start listening for focus
containerRef.container.register();
}
// Track references to dispose correctly
if (component) {
containerRef.addReference(component);
}
}
/**
* Remove the container object. This will call `unregister` on the container if `component` is the last reference
* to it.
* @param element The element containing focusable descendants.
* @param component The component requesting the managed focus container.
*/
unregister(element, component) {
// Remove the container's reference to the source component
const containerRef = this._containers.find(ref => ref.container.element.isEqualNode(element));
// technically this function can be called before the register function if ngOnDestroy runs before it
// is fully initialized so we should stop here if there is no containRef.
if (!containerRef) {
return;
}
containerRef.removeReference(component);
if (!containerRef.isAlive()) {
// Last reference was removed, so unregister the listeners
containerRef.container.unregister();
// Clean up the reference tracking array
this._containers = this._containers.filter(c => c !== containerRef);
}
}
/**
* Get an observable which can be used to determine when the element or one of its descendants has focus.
* @param element The element containing focusable descendants.
*/
hasFocus(element) {
const container = this.getContainer(element);
return container ? container.hasFocus$.asObservable() : null;
}
getContainer(element) {
const containerRef = this._containers.find(ref => ref.container.element.isEqualNode(element));
return containerRef ? containerRef.container : null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ManagedFocusContainerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ManagedFocusContainerService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ManagedFocusContainerService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class ManagedFocusContainer {
constructor(element, _renderer) {
this.element = element;
this._renderer = _renderer;
/** Whether the container or one of its descendants has focus. */
this.hasFocus$ = new BehaviorSubject(false);
this._modifiedElements = [];
this._unlisten = [];
}
/** Start managing the focus of child elements. */
register() {
this._unlisten.push(this._renderer.listen(this.element, 'focusin', this.onFocusIn.bind(this)));
this._unlisten.push(this._renderer.listen(this.element, 'focusout', this.onFocusOut.bind(this)));
// Check if the container already has focus
setTimeout(() => {
if (!this.element.contains(document.activeElement)) {
this.removeTabFocus();
}
});
}
/** Stop managing the focus of child elements. */
unregister() {
// Dispose the event handlers
this._unlisten.forEach(unlisten => unlisten());
this._unlisten = [];
// Undo any tabindex modifications
this.restoreTabFocus();
}
onFocusIn() {
this.restoreTabFocus();
}
onFocusOut() {
this.removeTabFocus();
}
removeTabFocus() {
this.hasFocus$.next(false);
// Clear the list of affected elements
this._modifiedElements = [];
// Get all focusable children
const focusable = this.element.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
// Remove focusable children from the tab ring
Array.from(focusable).forEach(element => {
const originalTabIndex = element.getAttribute('tabindex');
this._renderer.setAttribute(element, 'tabindex', '-1');
this._modifiedElements.push({
element,
tabindex: originalTabIndex,
});
});
}
restoreTabFocus() {
this.hasFocus$.next(true);
// Restore tab focus ability by removing the custom `tabindex` attribute
this._modifiedElements.forEach(elementInfo => {
if (elementInfo.tabindex === null) {
this._renderer.removeAttribute(elementInfo.element, 'tabindex');
}
else {
this._renderer.setAttribute(elementInfo.element, 'tabindex', elementInfo.tabindex);
}
});
// Clear the list of affected elements
this._modifiedElements = [];
}
}
class ManagedFocusContainerWithReferences {
constructor(container) {
this.container = container;
this._components = [];
}
addReference(component) {
this._components.push(component);
}
removeReference(component) {
this._components = this._components.filter(c => c !== component);
}
isAlive() {
return this._components.length > 0;
}
}
class ManagedFocusContainerDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._managedFocusContainerService = inject(ManagedFocusContainerService);
}
ngOnInit() {
this._managedFocusContainerService.register(this._elementRef.nativeElement, this);
}
ngOnDestroy() {
this._managedFocusContainerService.unregister(this._elementRef.nativeElement, this);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ManagedFocusContainerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ManagedFocusContainerDirective, isStandalone: false, selector: "[uxManagedFocusContainer]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ManagedFocusContainerDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxManagedFocusContainer]',
standalone: false,
}]
}] });
class SplitterAccessibilityDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._splitter = inject(SplitComponent);
this._focusIndicatorService = inject(FocusIndicatorService);
this._platform = inject(PLATFORM_ID);
/** Emit an event whenever the gutter is moved using the keyboard */
this.gutterKeydown = new EventEmitter();
/** Store all the gutter elements */
this._gutters = [];
/** Teardown our observables on destroy */
this._onDestroy = new Subject();
/** Store references to all focus indicators */
this._focusIndicators = [];
}
ngOnInit() {
// update aria values when the a gutter is dragged
this._splitter.dragProgress$
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.updateGutterAttributes());
}
/** Once initialised make the gutters accessible */
ngAfterViewInit() {
// find the gutters
this.onGutterChange();
// if the number of split areas change then update the gutters and apply aria properties
this.areas.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => this.onGutterChange());
// we can't know when additional split-gutters appear using ContentChildren as the directive class is not exported and selector doesn't work - use mutation observer instead
if (isPlatformBrowser(this._platform)) {
// create the mutation observer
this._observer = new MutationObserver(() => this.onGutterChange());
// begin observing the child nodes
this._observer.observe(this._elementRef.nativeElement, { childList: true });
}
}
/** Destroy all observables and observers */
ngOnDestroy() {
if (this._observer) {
this._observer.disconnect();
}
this._onDestroy.next();
this._onDestroy.complete();
// destroy all existing focus indicators
this._focusIndicators.forEach(indicator => indicator.destroy());
}
/** We should focus the gutter when it is clicked */
onClick(event) {
if (this.isSplitterGutter(event.target)) {
event.target.parentElement.focus();
}
}
/** Find all the gutters and set their attributes */
onGutterChange() {
// destroy all existing focus indicators
this._focusIndicators.forEach(indicator => indicator.destroy());
// reset the array
this._focusIndicators = [];
// get the new gutter elements
this._gutters = this.getGutters();
// monitor the focus of each gutter
this._gutters.forEach(gutter => this._focusIndicators.push(this._focusIndicatorService.monitor(gutter)));
// apply all required accessibility attributes to the gutter elements
this.setGutterAttributes();
}
/** Get all the gutter elements */
getGutters() {
// This function uses DOM accessing properties - which won't work if server side rendered
if (isPlatformBrowser(this._platform)) {
const gutters = [];
for (let idx = 0; idx < this._elementRef.nativeElement.children.length; idx++) {
const node = this._elementRef.nativeElement.children.item(idx);
if (this.isSplitterGutter(node)) {
gutters.push(node);
}
}
return gutters;
}
return [];
}
/** Set the appropriate attributes on the gutter elements */
setGutterAttributes() {
// apply attribute to every gutter
this._gutters.forEach(gutter => {
// apply the separator role
this._renderer.setAttribute(gutter, 'role', 'separator');
// make the gutters tabbable
this._renderer.setAttribute(gutter, 'tabindex', '0');
// set the value now aria property
this.updateGutterAttributes();
});
}
/** Apply the aria attribute values */
updateGutterAttributes() {
// update the value now properties of each gutter
this._gutters.forEach((gutter, idx) => {
this.setGutterValueNow(gutter, idx);
this.setGutterValueMin(gutter, idx);
this.setGutterValueMax(gutter, idx);
});
}
/** Apply the value now aria attribute */
setGutterValueNow(gutter, index) {
// get the matching split area
const area = this._splitter.displayedAreas[index];
if (area.size === '*') {
return;
}
// indicate the size
this._renderer.setAttribute(gutter, 'aria-valuenow', `${Math.round(area.size)}`);
}
/** Apply the value min aria attribute */
setGutterValueMin(gutter, index) {
// get the matching split area
const area = this.areas.toArray()[index];
// indicate the minimum size
this._renderer.setAttribute(gutter, 'aria-valuemin', `${Math.round(area.minSize)}`);
}
/** Apply the value max aria attribute */
setGutterValueMax(gutter, index) {
// get every other splitter area
const availableSize = this.areas
.filter((_area, idx) => index !== idx)
.reduce((total, area) => total + area.minSize, 0);
// indicate the minimum size
this._renderer.setAttribute(gutter, 'aria-valuemax', `${100 - Math.round(availableSize)}`);
}
onKeydown(event) {
if (this.isSplitterGutter(event.target)) {
this.gutterKeydown.emit(event);
}
}
onIncreaseKey(event) {
// only perform a move if a gutter is focused
if (this.isSplitterGutter(event.target)) {
this.setGutterPosition(event.target, -1);
// stop the browser from scrolling
event.preventDefault();
}
}
onDecreaseKey(event) {
// only perform a move if a gutter is focused
if (this.isSplitterGutter(event.target)) {
this.setGutterPosition(event.target, 1);
// stop the browser from scrolling
event.preventDefault();
}
}
onHomeKey(event) {
if (this.isSplitterGutter(event.target)) {
// get the affected panels
const areas = this.getAreasFromGutter(event.target);
if (areas.previous.size === '*') {
return;
}
// set the previous area to it's minimum size
const delta = areas.previous.size - areas.previous.minSize;
// update the sizes accordingly
this.setGutterPosition(event.target, delta);
// stop the browser from scrolling
event.preventDefault();
}
}
onEndKey(event) {
if (this.isSplitterGutter(event.target)) {
// get the affected panels
const areas = this.getAreasFromGutter(event.target);
if (areas.next.size === '*') {
return;
}
// set the next area to it's minimum size
const delta = areas.next.size - areas.next.minSize;
// update the sizes accordingly
this.setGutterPosition(event.target, -delta);
// stop the browser from scrolling
event.preventDefault();
}
}
/** Determine if an element is a gutter */
isSplitterGutter(element) {
return (element.classList.contains('as-split-gutter') ||
element.classList.contains('as-split-gutter-icon'));
}
/** Update the gutter position */
setGutterPosition(gutter, delta) {
// get the affected panels
const areas = this.getAreasFromGutter(gutter);
if (areas.previous.size === '*' || areas.next.size === '*') {
return;
}
// ensure we can perform the resize
if (areas.previous.size - delta < areas.previous.minSize ||
areas.next.size + delta < areas.next.minSize) {
return;
}
// perform the resize
areas.previous.size -= delta;
areas.next.size += delta;
// update the splitter - this is a private method but we need to call it
this._splitter.refreshStyleSizes();
// update the gutter aria values
this.updateGutterAttributes();
}
/** Get the split areas associated with a given gutter */
getAreasFromGutter(gutter) {
const index = this._gutters.indexOf(gutter);
return {
previous: this._splitter.displayedAreas[index],
next: this._splitter.displayedAreas[index + 1],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SplitterAccessibilityDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SplitterAccessibilityDirective, isStandalone: false, selector: "as-split", outputs: { gutterKeydown: "gutterKeydown" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "keydown.ArrowDown": "onIncreaseKey($event)", "keydown.ArrowRight": "onIncreaseKey($event)", "keydown.ArrowUp": "onDecreaseKey($event)", "keydown.ArrowLeft": "onDecreaseKey($event)", "keydown.Home": "onHomeKey($event)", "keydown.End": "onEndKey($event)" } }, queries: [{ propertyName: "areas", predicate: SplitAreaDirective }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SplitterAccessibilityDirective, decorators: [{
type: Directive,
args: [{
selector: 'as-split',
standalone: false,
}]
}], propDecorators: { gutterKeydown: [{
type: Output
}], areas: [{
type: ContentChildren,
args: [SplitAreaDirective]
}], onClick: [{
type: HostListener,
args: ['click', ['$event']]
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}], onIncreaseKey: [{
type: HostListener,
args: ['keydown.ArrowDown', ['$event']]
}, {
type: HostListener,
args: ['keydown.ArrowRight', ['$event']]
}], onDecreaseKey: [{
type: HostListener,
args: ['keydown.ArrowUp', ['$event']]
}, {
type: HostListener,
args: ['keydown.ArrowLeft', ['$event']]
}], onHomeKey: [{
type: HostListener,
args: ['keydown.Home', ['$event']]
}], onEndKey: [{
type: HostListener,
args: ['keydown.End', ['$event']]
}] } });
class TabbableListService {
constructor() {
/** Indicate is this is being using on a hierarchichal set of items */
this.hierarchy = false;
/** Determine if we all the alt key */
this.allowAltModifier = true;
/** Determine if we all the ctrl key */
this.allowCtrlModifier = true;
/** Determine if we allow the Home/End keys */
this.allowBoundaryKeys = false;
/** Determine if we should scroll the item into view on focus */
this.shouldScrollInView = true;
/** Indicate if we should refocus an item on QueryList change - for use within virtual lists */
this.shouldFocusOnChange = true;
/** Emit whenever focus does not change but tabindexes have */
this.onTabIndexChange = new Subject();
/** Determine if focus is currently within the tabbable list */
this.isFocused = false;
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.onTabIndexChange.complete();
}
initialize(items, direction, wrap) {
// store the items
this._items = items;
// create the new focus key manager
this.focusKeyManager = new FocusKeyManager(items);
// set the direction of the list
direction === 'vertical'
? this.focusKeyManager.withVerticalOrientation()
: this.focusKeyManager.withHorizontalOrientation('ltr');
this._direction = direction;
// enable wrapping if required
if (wrap) {
this.focusKeyManager.withWrap();
}
// make sure the first item in the list is tabbable
this.setFirstItemTabbable();
// call the init function on each item
this._items.forEach(item => item.onInit());
// if the list changes we need to ensure there is always at least one tabbable item
this._items.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => {
// call the on init function on any new items
this._items.filter(item => !item.initialized).forEach(item => item.onInit());
// ensure we update the tab indexes
this.onTabIndexChange.next();
// ensure there is at least one item tabbable at all times
this.ensureTabbableItem();
});
}
/** Give and item focus or just make it the current tabbable item */
activate(item, updateIndexOnly = false) {
if (!item) {
return;
}
// get the item index
const index = this._items.toArray().indexOf(item);
this.activateItemAtIndex(index, updateIndexOnly);
}
/** Give and item focus or just make it the current tabbable item */
activateItemAtIndex(index, updateIndexOnly = false) {
// if we only want to update the index
if (updateIndexOnly) {
return this.updateActiveItemIndex(index);
}
// update active the item only if it is not already active
if (this.focusKeyManager.activeItemIndex !== index) {
this.focusKeyManager.setActiveItem(index);
}
}
isItemActive(item) {
// if this is called before the items have been set then do nothing
if (!this._items) {
return false;
}
// find the index of the item
const index = this._items.toArray().findIndex(_item => _item.id === item.id);
// check if the item is active (we check against index as it can be updated without setting the activeItem)
return this.focusKeyManager && this.focusKeyManager.activeItemIndex === index;
}
setFirstItemTabbable() {
// find the first item that is not disabled
const first = this._items.toArray().findIndex(item => !item.disabled);
if (first !== -1) {
this.updateActiveItemIndex(first);
}
}
isAnyItemTabbable() {
const tabbable = this._items.toArray().find(item => item.tabindex === 0);
return tabbable ? true : false;
}
ensureTabbableItem() {
// check to see if any item is tabbable
const active = this._items.find(item => this.isItemActive(item));
if (!active) {
this.setFirstItemTabbable();
}
}
focusTabbableItem() {
if (!this._items) {
return;
}
// find the item in the list with a tab index
const index = this._items.toArray().findIndex(item => this.isItemActive(item));
// if an item was found then focus it
if (index !== -1) {
this.focusKeyManager.setActiveItem(index);
}
}
onKeydown(source, event) {
// prevent anything happening when modifier keys are pressed if they have been disabled
if ((!this.allowAltModifier && event.altKey) || (!this.allowCtrlModifier && event.ctrlKey)) {
return;
}
this.focusKeyManager.onKeydown(event);
// if the key is a boundary key and boundary keys are enabled
if (this.allowBoundaryKeys) {
switch (event.which) {
case HOME:
this.focusKeyManager.setFirstItemActive();
event.preventDefault();
break;
case END:
this.focusKeyManager.setLastItemActive();
event.preventDefault();
break;
}
}
if (this.hierarchy) {
if ((this._direction === 'horizontal' && event.keyCode === DOWN_ARROW) ||
(this._direction === 'vertical' && event.keyCode === RIGHT_ARROW)) {
source.keyboardExpanded$.next(true);
}
else if ((this._direction === 'horizontal' && event.keyCode === UP_ARROW) ||
(this._direction === 'vertical' && event.keyCode === LEFT_ARROW)) {
if (source.children.length > 0 && source.expanded) {
source.keyboardExpanded$.next(false);
}
else if (source.parent) {
source.parent.keyboardExpanded$.next(false);
}
}
}
}
sortItemsByHierarchy(list) {
const topLevel = [];
// Populating children - clear previously generated collection
list.forEach(item => (item.children = []));
// Populating children - map from child -> parent relationship
list.forEach(item => {
if (item.parent) {
item.parent.children.push(item);
}
else {
topLevel.push(item);
}
});
// Flatten the tree to produce the cursor key order
return this.flattenHierarchy(topLevel);
}
/**
* In a uxVirtualFor list cells can be resused. This means that when we scroll
* the data associated with a given element may change and not the actual elements. If only the data changes
* then the QueryList will not emit a change so we may show focus indicatator on the element that previously displayed
* the correct data but no longer does.
*
* We need to handle this correctly here. We already have keys implements to handle virtual elements so we can check
* if a key changes and use it to update the focused item even if the QueryList doesn't inform us that we have changed.
*/
itemReferenceChange(previousKey, origin) {
// find the item that now has the previously focused key
const item = this.getItemByKey(previousKey);
// if no key was found then we should ensure there is a tabbable item
if (!item) {
return this.ensureTabbableItem();
}
// get the item index
const index = this._items.toArray().indexOf(item);
// activate the item without side effects
this.updateActiveItemIndex(index);
// focus the item with the same origin that it previously had
item.focusWithOrigin(origin);
}
/** Update the active item without causing focus */
updateActiveItemIndex(index) {
this.focusKeyManager.updateActiveItem(index);
this.onTabIndexChange.next();
}
/** Determine if there is an item with a tabindex of 0 */
hasTabbableItem() {
return this.focusKeyManager && this.focusKeyManager.activeItemIndex >= 0;
}
getItemByKey(key) {
return this._items.find(item => item.key === key);
}
flattenHierarchy(items) {
const flatList = [];
items.forEach(item => {
item.children.sort((a, b) => a.rank - b.rank);
flatList.push(item, ...this.flattenHierarchy(item.children));
});
return flatList;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListService, decorators: [{
type: Injectable
}] });
let nextId = 0;
let uniqueKey = 0;
class TabbableListItemDirective {
/** Provide a unique key to help identify items when used in a virtual list */
set key(key) {
// store the previous key
const previousKey = this._key;
// check if the key has changed eg. via cell reuse
const didChangeRef = previousKey && key !== previousKey;
// update the current key
this._key = key;
// if this element was the previously tabbable item then update the reference
if (didChangeRef && this.isTabbable()) {
// allow the virtual scroll to update
this._changeDetector.detectChanges();
// this item should no longer be tabbable
this._tabbableList.focusKeyManager.updateActiveItem(-1);
// store the focus origin before we blur
const origin = this._focusOrigin;
// blur this item
this._elementRef.nativeElement.blur();
// update the reference
this._tabbableList.itemReferenceChange(previousKey, origin);
}
}
get key() {
return this._key || this._defaultKey;
}
get tabindex() {
return this._tabbableList.isItemActive(this) ? 0 : -1;
}
constructor() {
/** Access the service to programmatically control focus indicators */
this.focusIndicatorService = inject(FocusIndicatorService);
/** Access the tabbable list service */
this._tabbableList = inject(TabbableListService);
/** Access the tabbable item element */
this._elementRef = inject(ElementRef);
/** Access the service responsible for handling focus in child elements */
this._managedFocusContainerService = inject(ManagedFocusContainerService);
/** Access the service which can provide us with browser identification */
this._platform = inject(Platform);
/** Access the change detector to ensure tabindex gets updated as expects */
this._changeDetector = inject(ChangeDetectorRef);
/** Access the focus origin if one is provided */
this._focusOriginService = inject(FocusIndicatorOriginService);
/** Access the renderer to make manual dom manipulations */
this._renderer = inject(Renderer2);
this.rank = 0;
/** Indicate if this item is disabled */
this.disabled = false;
/** Indicate if the item is expanded if used as a hierarchical item. */
this.expanded = false;
/** Emit when the expanded state changes. */
this.expandedChange = new EventEmitter();
/** Emit when the element receives focus via the tabbable list. */
this.activated = new EventEmitter();
/** Give each tabbable item a unique id */
this.id = nextId++;
/** Each item in the list needs to be initialised by the service. When the item QueryList changes this is used to identify which items previously existed and which are new */
this.initialized = false;
/** Store a list of all child tabbable items */
this.children = [];
/** Emit whenever the expanded state changes */
this.keyboardExpanded$ = new Subject();
/** Automatically unsubscribe from all observables */
this._onDestroy = new Subject();
/** Store a default key to use if one is not provided */
this._defaultKey = `tabbable-list-key-${uniqueKey++}`;
/** Determine if this element has a focus indicator visible */
this._focusOrigin = null;
// create the focus indicator
this._focusIndicator = this.focusIndicatorService.monitor(this._elementRef.nativeElement);
// store the most current focus origin
this._focusIndicator.origin$
.pipe(takeUntil(this._onDestroy))
.subscribe(origin => (this._focusOrigin = origin));
// watch for changes to tabindexes
this._tabbableList.onTabIndexChange
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.setTabIndex());
this.keyboardExpanded$.pipe(tick(), takeUntil(this._onDestroy)).subscribe(expanded => {
// Emit event which may alter the DOM
this.expandedChange.emit(expanded);
// Activate the appropriate item
if (expanded) {
if (this.children.length > 0) {
this._tabbableList.activate(this.children[0]);
}
}
else {
this._tabbableList.activate(this);
}
});
}
onInit() {
this.initialized = true;
// Watch for focus within the container element and manage tabindex of descendants
this._managedFocusContainerService.register(this._elementRef.nativeElement, this);
// ensure the tab index is initially set
this.setTabIndex();
}
ngOnDestroy() {
// check if this is the currently focused item - if so we need to make another item tabbable
if (this.tabindex === 0) {
this._tabbableList.setFirstItemTabbable();
}
this._onDestroy.next();
this._onDestroy.complete();
this._focusIndicator.destroy();
this._managedFocusContainerService.unregister(this._elementRef.nativeElement, this);
}
focus() {
// check if there are currently any items that are tabbable
const hasTabbableItem = this._tabbableList.hasTabbableItem();
// determine the focus origin
const origin = hasTabbableItem
? this._focusOriginService.getOrigin() || 'keyboard'
: 'keyboard';
// apply focus to the element
this.focusWithOrigin(origin, !this._tabbableList.shouldScrollInView);
// ensure the focus key manager updates the active item correctly
this._tabbableList.activate(this, hasTabbableItem);
// emit the focus event
this.activated.emit(origin);
}
onFocus() {
// if this item is not currently focused in the focusKeyManager set it as the active item
if (!this._tabbableList.isItemActive(this)) {
this._tabbableList.activate(this, true);
}
// also inform the service that an item within the list is now focused
this._tabbableList.isFocused = true;
}
onBlur() {
// if this is the current active item and it is blurred then update the isFocused state
if (this._tabbableList.isItemActive(this)) {
this._tabbableList.isFocused = false;
}
}
onKeydown(event) {
this._tabbableList.onKeydown(this, event);
}
getFocused() {
return this._elementRef.nativeElement === document.activeElement;
}
/** We can programmatically focus an element but may want a different origin than 'programmatic' */
focusWithOrigin(origin, preventScroll = true) {
if (origin) {
const scrollTop = this._tabbableList.containerRef.scrollTop;
// focus the item with a given origin
this._focusIndicator.focus(origin, { preventScroll });
// IE and Firefox don't support prevent scroll
if (preventScroll && !this._platform.WEBKIT) {
this._tabbableList.containerRef.scrollTop = scrollTop;
}
}
}
isTabbable() {
return this.tabindex === 0;
}
setTabIndex() {
// update the tabindex attribute
this._renderer.setAttribute(this._elementRef.nativeElement, 'tabindex', this.tabindex.toString());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TabbableListItemDirective, isStandalone: false, selector: "[uxTabbableListItem]", inputs: { parent: "parent", rank: "rank", disabled: "disabled", expanded: "expanded", key: "key" }, outputs: { expandedChange: "expandedChange", activated: "activated" }, host: { listeners: { "focus": "onFocus()", "click": "onFocus()", "blur": "onBlur()", "keydown": "onKeydown($event)" } }, exportAs: ["ux-tabbable-list-item"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTabbableListItem]',
exportAs: 'ux-tabbable-list-item',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { parent: [{
type: Input
}], rank: [{
type: Input
}], disabled: [{
type: Input
}], expanded: [{
type: Input
}], key: [{
type: Input
}], expandedChange: [{
type: Output
}], activated: [{
type: Output
}], onFocus: [{
type: HostListener,
args: ['focus']
}, {
type: HostListener,
args: ['click']
}], onBlur: [{
type: HostListener,
args: ['blur']
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class TabbableListDirective {
/** Enabling handling of hierarchical lists via use of the `TabbableListItemDirective.parent` property. */
set hierarchy(value) {
this._tabbableList.hierarchy = value;
}
/** Prevent keyboard interaction when alt modifier key is pressed */
set allowAltModifier(value) {
this._tabbableList.allowAltModifier = value;
}
/** Prevent keyboard interaction when ctrl modifier key is pressed */
set allowCtrlModifier(value) {
this._tabbableList.allowCtrlModifier = value;
}
/** Focus the first or last item when Home or End keys are pressed */
set allowBoundaryKeys(value) {
this._tabbableList.allowBoundaryKeys = value;
}
get focusKeyManager() {
return this._tabbableList.focusKeyManager;
}
constructor() {
/** Access the tabbable list service */
this._tabbableList = inject(TabbableListService);
/** Access the native dom element */
this.elementRef = inject(ElementRef);
/** Determine whether the up/down arrows should be used or the left/right arrows */
this.direction = 'vertical';
/** Indicate whether or not focus should loop back to the first element after the last */
this.wrap = true;
/** Indicate whether or not the first item should receive focus on show - useful for modals and popovers */
this.focusOnShow = false;
/** Indicate whether or not focus should be returned to the previous element (only applicable when using focusOnShow) */
this.returnFocus = false;
/** Unsubscribe from all observables automatically on destroy */
this._onDestroy = new Subject();
// store a reference to the container element
this._tabbableList.containerRef = this.elementRef.nativeElement;
}
ngAfterContentInit() {
// store the currently focused element
this._focusedElement = document.activeElement;
this._orderedItems = new QueryList();
if (this._tabbableList.hierarchy) {
// Sort items in a hierarchy
this._orderedItems.reset(this._tabbableList.sortItemsByHierarchy(this.items));
// Ensure that the child items remain sorted
this.items.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this._orderedItems.reset(this._tabbableList.sortItemsByHierarchy(this.items));
this._orderedItems.notifyOnChanges();
});
}
else {
// Items are already in order
this._orderedItems = this.items;
// Ensure we reselect a selected item after the querylist has changed
this.items.changes
.pipe(filter(() => this._tabbableList.shouldFocusOnChange && this._tabbableList.isFocused), takeUntil(this._onDestroy))
.subscribe((items) => {
// check if an item is currently focused
const activeItem = this._tabbableList.focusKeyManager.activeItem;
// restore the selected item if there was one and it is still visible
if (activeItem) {
// find the matching index
const index = items.toArray().findIndex(item => item.key === activeItem.key);
// if the item is still in the list we want to focus it
if (index > -1) {
// however we are refocusing an item that was focused so we dont want to scroll into view again as this can prevent wheel scrolling
this._tabbableList.shouldScrollInView = false;
// refocus the item again
this._tabbableList.activateItemAtIndex(index, !this._tabbableList.isFocused);
// re-enable scrolling into view
this._tabbableList.shouldScrollInView = true;
}
}
});
}
// Set up the focus monitoring
this._tabbableList.initialize(this._orderedItems, this.direction, this.wrap);
// focus the first element if specified
if (this.focusOnShow) {
this._tabbableList.focusKeyManager.setFirstItemActive();
}
}
ngOnDestroy() {
if (this.returnFocus && this._focusedElement instanceof HTMLElement) {
setTimeout(() => this._focusedElement.focus());
}
this._onDestroy.next();
this._onDestroy.complete();
}
setFirstItemTabbable() {
if (!this._tabbableList.isAnyItemTabbable()) {
this._tabbableList.setFirstItemTabbable();
}
}
focus() {
if (this._tabbableList.focusKeyManager && this._tabbableList.focusKeyManager.activeItem) {
this._tabbableList.focusKeyManager.activeItem.focus();
}
}
focusTabbableItem() {
this._tabbableList.focusTabbableItem();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TabbableListDirective, isStandalone: false, selector: "[uxTabbableList]", inputs: { direction: "direction", wrap: "wrap", focusOnShow: "focusOnShow", returnFocus: "returnFocus", hierarchy: "hierarchy", allowAltModifier: "allowAltModifier", allowCtrlModifier: "allowCtrlModifier", allowBoundaryKeys: "allowBoundaryKeys" }, providers: [TabbableListService], queries: [{ propertyName: "items", predicate: TabbableListItemDirective, descendants: true }], exportAs: ["ux-tabbable-list"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabbableListDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTabbableList]',
exportAs: 'ux-tabbable-list',
providers: [TabbableListService],
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { direction: [{
type: Input
}], wrap: [{
type: Input
}], focusOnShow: [{
type: Input
}], returnFocus: [{
type: Input
}], hierarchy: [{
type: Input
}], allowAltModifier: [{
type: Input
}], allowCtrlModifier: [{
type: Input
}], allowBoundaryKeys: [{
type: Input
}], items: [{
type: ContentChildren,
args: [TabbableListItemDirective, { descendants: true }]
}] } });
class AccessibilityModule {
static forRoot(options) {
return {
ngModule: AccessibilityModule,
providers: [{ provide: ACCESSIBILITY_OPTIONS_TOKEN, useValue: options }],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityModule, declarations: [DefaultFocusIndicatorDirective,
FocusIndicatorDirective,
FocusIndicatorOptionsDirective,
FocusIndicatorOriginDirective,
FocusWithinDirective,
ManagedFocusContainerDirective,
SplitterAccessibilityDirective,
TabbableListDirective,
TabbableListItemDirective,
FocusIndicatorOriginDirective,
ColorContrastDirective], imports: [A11yModule, ColorServiceModule, PlatformModule], exports: [DefaultFocusIndicatorDirective,
FocusIndicatorDirective,
FocusIndicatorOptionsDirective,
FocusIndicatorOriginDirective,
FocusWithinDirective,
ManagedFocusContainerDirective,
SplitterAccessibilityDirective,
TabbableListDirective,
TabbableListItemDirective,
FocusIndicatorOriginDirective,
ColorContrastDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityModule, providers: [
AccessibilityOptionsService,
ContrastService,
FocusIndicatorService,
ManagedFocusContainerService,
], imports: [A11yModule, ColorServiceModule, PlatformModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccessibilityModule, decorators: [{
type: NgModule,
args: [{
declarations: [
DefaultFocusIndicatorDirective,
FocusIndicatorDirective,
FocusIndicatorOptionsDirective,
FocusIndicatorOriginDirective,
FocusWithinDirective,
ManagedFocusContainerDirective,
SplitterAccessibilityDirective,
TabbableListDirective,
TabbableListItemDirective,
FocusIndicatorOriginDirective,
ColorContrastDirective,
],
imports: [A11yModule, ColorServiceModule, PlatformModule],
exports: [
DefaultFocusIndicatorDirective,
FocusIndicatorDirective,
FocusIndicatorOptionsDirective,
FocusIndicatorOriginDirective,
FocusWithinDirective,
ManagedFocusContainerDirective,
SplitterAccessibilityDirective,
TabbableListDirective,
TabbableListItemDirective,
FocusIndicatorOriginDirective,
ColorContrastDirective,
],
providers: [
AccessibilityOptionsService,
ContrastService,
FocusIndicatorService,
ManagedFocusContainerService,
],
}]
}] });
class AccordionModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: AccordionModule, declarations: [AccordionComponent, AccordionPanelComponent, AccordionPanelHeadingDirective], imports: [AccessibilityModule, CommonModule], exports: [AccordionComponent, AccordionPanelComponent, AccordionPanelHeadingDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionModule, imports: [AccessibilityModule, CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AccordionModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule],
declarations: [AccordionComponent, AccordionPanelComponent, AccordionPanelHeadingDirective],
exports: [AccordionComponent, AccordionPanelComponent, AccordionPanelHeadingDirective],
}]
}] });
class AlertIconDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: AlertIconDirective, isStandalone: false, selector: "[uxAlertIcon]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertIconDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxAlertIcon]',
standalone: false,
}]
}] });
const ICON_OPTIONS_TOKEN = new InjectionToken('ICON_OPTIONS_TOKEN');
/** AUTOGENERATED: DO NOT MODIFY **/
const commonIcons = [
'3d',
'achievement',
'action',
'actions',
'active',
'add',
'address-book',
'advanced-search',
'aggregate',
'alarm',
'alert-filled',
'alert',
'analytics',
'announcement',
'app',
'archive',
'article',
'ascend',
'assistant',
'attachment',
'bar-chart',
'blog',
'bloomberg',
'book',
'bookmark-filled',
'bookmark',
'bundle',
'calculator',
'calendar',
'camera-filled',
'camera',
'capacity',
'caret-down-filled',
'caret-down',
'caret-next-filled',
'caret-next',
'caret-previous-filled',
'caret-previous',
'caret-up-filled',
'caret-up',
'catalog',
'change-password',
'chapter-add',
'chapter-next-filled',
'chapter-next',
'chapter-previous-filled',
'chapter-previous',
'chart-organization',
'chart-partition',
'chart-sankey',
'chat-attachment',
'chat',
'checkbox-selected',
'checkbox',
'checkmark',
'chevron-down',
'chevron-left-double',
'chevron-left',
'chevron-right-double',
'chevron-right',
'chevron-up',
'chorus',
'circular-view',
'clipboard',
'clone',
'close',
'cloud-computer',
'cloud-download',
'cloud-software',
'cloud-upload',
'cloud',
'cluster',
'code',
'command-line',
'compare',
'compass',
'compliance',
'computer-personal',
'configuration-filled',
'configuration',
'confluence',
'connect',
'contact-card',
'contact-us-filled',
'contact-us',
'contract',
'copy',
'cube-filled',
'cube',
'cubes',
'cursor-filled',
'cursor',
'cut',
'cycle',
'dashboard',
'database',
'defect',
'deliver',
'deployment',
'descend',
'desktop',
'detach',
'directions',
'dislike-filled',
'dislike',
'divide-four',
'divide-right',
'divide-three',
'divide',
'document-cloud',
'document-compress',
'document-config',
'document-csv',
'document-data',
'document-download',
'document-excel',
'document-executable',
'document-html',
'document-image',
'document-import',
'document-locked',
'document-missing',
'document-notes',
'document-outlook',
'document-pdf',
'document-performance',
'document-powerpoint',
'document-rtf',
'document-sound',
'document-test',
'document-text',
'document-threat',
'document-time',
'document-transfer',
'document-txt',
'document-update',
'document-upload',
'document-user',
'document-verified',
'document-video',
'document-word',
'document',
'domain',
'down',
'download',
'drag',
'drive-cage',
'duplicate',
'edit-filled',
'edit',
'efax',
'eject-filled',
'eject',
'expand',
'export',
'fan',
'fast-forward-filled',
'fast-forward',
'favorite-filled',
'favorite',
'filter-filled',
'filter',
'first-aid',
'flag-filled',
'flag',
'folder-cycle',
'folder-open',
'folder',
'gallery-filled',
'gallery',
'globe',
'grid',
'group-add',
'group',
'grow',
'halt',
'help-circle',
'help',
'highlighting-remove',
'highlighting',
'history',
'home-filled',
'home',
'host-maintenance',
'host',
'image-filled',
'image',
'impact',
'import',
'in-progress',
'inactive',
'inbox',
'indicator-filled',
'indicator',
'information-filled',
'information',
'inherit-filled',
'inherit',
'input-to-process',
'install',
'integration',
'iteration-filled',
'iteration',
'java-filled',
'java',
'language',
'launch',
'license-filled',
'license',
'like-filled',
'like',
'line-chart',
'link-bottom',
'link-down',
'link-next',
'link-previous',
'link-top',
'link-up',
'link',
'list',
'location-filled',
'location-pin-filled',
'location-pin',
'location',
'lock',
'login',
'logout',
'mail-attachment',
'mail-filled',
'mail',
'manual',
'map-location',
'map',
'menu',
'microphone-filled',
'microphone',
'monitor',
'more',
'multiple',
'navigate',
'new-window',
'new',
'news-aggregation',
'news-collection',
'news-content',
'news',
'next',
'notes',
'notification-filled',
'notification',
'optimization',
'organization',
'overview',
'pan',
'pause-filled',
'pause',
'payment-google-wallet',
'payment-mastercard',
'payment-paypal',
'payment-square',
'payment-visa',
'pin-filled',
'pin',
'plan',
'platform-apple',
'platform-chrome',
'platform-dropbox',
'platform-edge',
'platform-firefox',
'platform-internet-explorer',
'platform-kubernetes',
'platform-skype',
'platform-windows',
'play-filled',
'play',
'power',
'previous',
'print',
'quick-view',
'radial-selected',
'radial',
'redo',
'refresh',
'resources',
'reuters',
'rewind-filled',
'rewind',
'risk',
'rss',
'satellite',
'save-filled',
'save',
'scale-out-repository',
'schedule-clone',
'schedule-new',
'schedule-play',
'schedule',
'scorecard',
'search',
'secure',
'select-left',
'select',
'server-cluster',
'server-started',
'server',
'servers',
'service-business',
'service-start',
'share',
'shield-configure',
'shield-filled',
'shield',
'shift',
'shop-basket',
'shop-cart',
'show-less',
'show-more',
'sms',
'soa',
'social-cisco-jabber',
'social-email',
'social-facebook-workplace',
'social-facebook',
'social-github',
'social-instagram',
'social-instant-message',
'social-jira',
'social-linkedin',
'social-medium',
'social-ms-teams',
'social-pinterest',
'social-reddit',
'social-salesforce-filled',
'social-salesforce',
'social-sharepoint',
'social-skype-for-business',
'social-slack',
'social-tumblr',
'social-twitter',
'social-vimeo',
'social-we-chat',
'social-whats-app',
'social-yammer',
'social-youtube',
'social-zoom',
'sort',
'stakeholder',
'star-filled',
'star-half',
'star',
'status-approved-filled',
'status-error-filled',
'status-information-filled',
'status-information',
'status-warning-filled',
'steps-filled',
'steps',
'storage',
'street-view-filled',
'street-view',
'subtitles',
'subtract',
'support',
'symphony',
'sync',
'system',
'tab-next',
'tab-previous',
'tab-up',
'table-add',
'table',
'tag-filled',
'tag',
'target',
'task',
'template',
'test-desktop',
'test',
'text-wrap',
'threats',
'ticket',
'tools',
'tooltip',
'transaction-filled',
'transaction',
'translate',
'trash-filled',
'trash',
'tree',
'trigger',
'trophy-filled',
'trophy',
'troubleshooting',
'undo',
'unlock',
'up',
'update',
'upgrade-filled',
'upgrade',
'upload',
'user-add-filled',
'user-add',
'user-admin',
'user-expert',
'user-female-filled',
'user-female',
'user-filled',
'user-manager',
'user-new',
'user-police',
'user-settings',
'user-worker',
'user',
'validation-filled',
'validation',
'video-filled',
'video',
'view-filled',
'view',
'virtual-machine',
'vm-maintenance',
'voltage',
'volume-filled',
'volume-low-filled',
'volume-low',
'volume-mute-filled',
'volume-mute',
'volume',
'vulnerability',
'waypoint-filled',
'waypoint',
'workshop',
'zoom-in',
'zoom-out',
];
/** We generate the iconset definition as hardcoding it increases bundle size by ~40kb per iconset */
const uxIconset = [
...commonIcons.map(uxIconMapper),
];
function uxIconMapper(icon) {
return { name: icon, iconset: 'ux-icon', icon: `ux-icon-${icon}` };
}
class IconService {
/** Inject a parent service if one exists */
constructor() {
this.options = inject(ICON_OPTIONS_TOKEN, { optional: true });
this._iconService = inject(IconService, { optional: true, skipSelf: true });
/** Emit whenever the iconset changes */
this.iconsChanged$ = new Subject();
/** Store a list of all icon */
this._icons = [...uxIconset];
// if the iconset was defined at the root or child module level apply this configuration
if (this.options && this.options.icons) {
this.setIcons(this.options.icons);
}
}
/** Define multiple icon definitions. This will override icon definitions if a name and size collision occurs */
setIcons(icons) {
icons.forEach(icon => this.setIcon(icon));
}
/** Provide an icon definition which will override if necessary */
setIcon({ name, icon, iconset, size }) {
// if there are multiple sizes specified add them all as individual records
if (Array.isArray(size)) {
return size.forEach(variant => this.setIcon({ name, icon, iconset, size: variant }));
}
// remove any existing definition with the same parameters
this._icons = this._icons.filter(definition => !(definition.name === name && definition.size === size));
// insert the new definition
this._icons = [...this._icons, { name, icon, iconset, size }];
// emit the icon change
this.iconsChanged$.next({ name, size });
}
/** Find an icon based on the given name and size if provided */
getIcon(name, size) {
// if no name was specified then do nothing (this can occur if the name input on the component is not initially defined)
if (!name) {
return;
}
// if there is a size specified then check for an exact match
if (size) {
// get an icon definition that matches both name and size
const sizedIcon = this._icons.find(definition => definition.name === name && definition.size === size);
// if there is a match then return otherwise fallthrough to the default
if (sizedIcon) {
return sizedIcon;
}
}
// find a general match with no size constraint
const icon = this._icons.find(definition => definition.name === name && definition.size === undefined);
// if no match is found and there is a parent service then we should check it
if (!icon && this._iconService) {
return this._iconService.getIcon(name, size);
}
else if (!icon) {
console.warn(`Icon '${name}' was not found.`);
}
return icon;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class IconComponent {
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._iconService = inject(IconService);
/** Store the boolean value of flip vertical */
this._flipVertical = false;
/** Store the boolean value of flip horizontal */
this._flipHorizontal = false;
/** Automatically unsubscribe from observables */
this._onDestroy = new Subject();
}
/** The number of degrees to rotate the icon */
set rotate(rotation) {
this._rotate = coerceNumberProperty(rotation);
}
get rotate() {
return this._rotate;
}
/** Define if the icon should be horizontally flipped */
set flipHorizontal(flipHorizontal) {
this._flipHorizontal = coerceBooleanProperty(flipHorizontal);
}
get flipHorizontal() {
return this._flipHorizontal;
}
/** Define if the icon should be horizontally flipped */
set flipVertical(flipVertical) {
this._flipVertical = coerceBooleanProperty(flipVertical);
}
get flipVertical() {
return this._flipVertical;
}
/** When inputs change ensure we have the best icon definition */
ngOnChanges(changes) {
// if the name or size changes then update the icon
if ((changes.name && changes.name.currentValue !== changes.name.previousValue) ||
(changes.size && changes.size.currentValue !== changes.size.previousValue)) {
this.updateIcon();
}
}
/** Watch for changes to the iconset */
ngAfterViewInit() {
// watch for changes to the iconset to check if we need to update.
this._iconService.iconsChanged$
.pipe(filter(event => this._icon && event.name === this._icon.name), takeUntil(this._onDestroy))
.subscribe(() => this.updateIcon());
}
/** Cleanup on component destroy */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** get the icon definition based on the name and size specified */
updateIcon() {
// remove the current icon set and icon classes of the old icon.
// note we are using the renderer and not HostBindings as a HostBinding
// on the `class` property will override any user added classes which is
// not desirable.
if (this._icon) {
this._renderer.removeClass(this._elementRef.nativeElement, this._icon.iconset);
this._renderer.removeClass(this._elementRef.nativeElement, this._icon.icon);
}
// update the stored icon definition with the best match based on name and size
this._icon = this._iconService.getIcon(this.name, this.size);
// add the new icon classes, again using the renderer to avoid overriding user classes
if (this._icon) {
this._renderer.addClass(this._elementRef.nativeElement, this._icon.iconset);
this._renderer.addClass(this._elementRef.nativeElement, this._icon.icon);
}
else if (this.name) {
console.warn(`The icon ${this.name} could not be found. Ensure you are using the correct iconset.`);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: IconComponent, isStandalone: false, selector: "ux-icon", inputs: { name: "name", size: "size", rotate: "rotate", flipHorizontal: "flipHorizontal", flipVertical: "flipVertical" }, host: { properties: { "style.font-size": "size", "class.ux-flip-horizontal": "flipHorizontal", "class.ux-flip-vertical": "flipVertical", "class.ux-rotate-90": "rotate == 90", "class.ux-rotate-180": "rotate == 180", "class.ux-rotate-270": "rotate == 270" } }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconComponent, decorators: [{
type: Component,
args: [{
selector: 'ux-icon',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[style.font-size]': 'size',
'[class.ux-flip-horizontal]': 'flipHorizontal',
'[class.ux-flip-vertical]': 'flipVertical',
'[class.ux-rotate-90]': 'rotate == 90',
'[class.ux-rotate-180]': 'rotate == 180',
'[class.ux-rotate-270]': 'rotate == 270',
},
standalone: false,
}]
}], propDecorators: { name: [{
type: Input
}], size: [{
type: Input
}], rotate: [{
type: Input
}], flipHorizontal: [{
type: Input
}], flipVertical: [{
type: Input
}] } });
class AlertComponent {
constructor() {
this.colorService = inject(ColorService);
/** Determine the style of the alert */
this.type = 'info';
/** Determine the the alert can be dismissed */
this.dismissible = false;
/** Define a custom aria label for the dismiss button */
this.dismissAriaLabel = 'Dismiss Alert';
/** Emit when the dismiss button is pressed */
this.dismiss = new EventEmitter();
}
/** Resolve the background color from the color set */
get _backgroundColor() {
return this.backgroundColor ? this.getColor(this.backgroundColor) : null;
}
/** Resolve the foreground color from the color set */
get _foregroundColor() {
return this.foregroundColor ? this.getColor(this.foregroundColor) : null;
}
/** Determine if we are using a prefined type or custom colors */
get _isCustomColor() {
return !!this.backgroundColor && !!this.foregroundColor;
}
getColor(color) {
// check if it is a color name from the color palette or just return the CSS color value
return this.colorService.resolve(color);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: AlertComponent, isStandalone: false, selector: "ux-alert", inputs: { type: "type", dismissible: "dismissible", backgroundColor: "backgroundColor", foregroundColor: "foregroundColor", dismissAriaLabel: "dismissAriaLabel" }, outputs: { dismiss: "dismiss" }, host: { attributes: { "role": "alert" }, properties: { "class.ux-alert-info": "type === \"info\" && !_isCustomColor", "class.ux-alert-error": "type === \"error\" && !_isCustomColor", "class.ux-alert-warning": "type === \"warning\" && !_isCustomColor", "class.ux-alert-success": "type === \"success\" && !_isCustomColor", "class.ux-alert-dark": "type === \"dark\" && !_isCustomColor", "style.background-color": "_backgroundColor", "style.color": "_foregroundColor" } }, queries: [{ propertyName: "icon", first: true, predicate: AlertIconDirective, descendants: true }], ngImport: i0, template: "@if (icon) {\n <div class=\"alert-icon\">\n <ng-content select=\"[uxAlertIcon]\"></ng-content>\n </div>\n}\n\n<div class=\"alert-content\">\n <ng-content></ng-content>\n</div>\n\n@if (dismissible) {\n <button\n uxFocusIndicator\n class=\"alert-close\"\n type=\"button\"\n (click)=\"dismiss.emit()\"\n [attr.aria-label]=\"dismissAriaLabel\"\n >\n <ux-icon name=\"close\" class=\"alert-close-icon\"></ux-icon>\n </button>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-alert', changeDetection: ChangeDetectionStrategy.OnPush, host: {
role: 'alert',
'[class.ux-alert-info]': 'type === "info" && !_isCustomColor',
'[class.ux-alert-error]': 'type === "error" && !_isCustomColor',
'[class.ux-alert-warning]': 'type === "warning" && !_isCustomColor',
'[class.ux-alert-success]': 'type === "success" && !_isCustomColor',
'[class.ux-alert-dark]': 'type === "dark" && !_isCustomColor',
'[style.background-color]': '_backgroundColor',
'[style.color]': '_foregroundColor',
}, standalone: false, template: "@if (icon) {\n <div class=\"alert-icon\">\n <ng-content select=\"[uxAlertIcon]\"></ng-content>\n </div>\n}\n\n<div class=\"alert-content\">\n <ng-content></ng-content>\n</div>\n\n@if (dismissible) {\n <button\n uxFocusIndicator\n class=\"alert-close\"\n type=\"button\"\n (click)=\"dismiss.emit()\"\n [attr.aria-label]=\"dismissAriaLabel\"\n >\n <ux-icon name=\"close\" class=\"alert-close-icon\"></ux-icon>\n </button>\n}\n" }]
}], propDecorators: { type: [{
type: Input
}], dismissible: [{
type: Input
}], backgroundColor: [{
type: Input
}], foregroundColor: [{
type: Input
}], dismissAriaLabel: [{
type: Input
}], dismiss: [{
type: Output
}], icon: [{
type: ContentChild,
args: [AlertIconDirective, { static: false }]
}] } });
class IconModule {
/** Allow configuration at AppModule level */
static forRoot(options) {
return {
ngModule: IconModule,
providers: [{ provide: ICON_OPTIONS_TOKEN, useValue: options }],
};
}
/** Allow configuration at a child module level */
static forChild(options) {
// the `forChild` does the same as `forRoot` however this having
// `forChild` follows the correct conventions as we should never
// import `forRoot` in a child module
return IconModule.forRoot(options);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: IconModule, declarations: [IconComponent], exports: [IconComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconModule, providers: [IconService] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: IconModule, decorators: [{
type: NgModule,
args: [{
declarations: [IconComponent],
exports: [IconComponent],
providers: [IconService],
}]
}] });
class AlertModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: AlertModule, declarations: [AlertComponent, AlertIconDirective], imports: [AccessibilityModule, CommonModule, IconModule], exports: [AlertComponent, AlertIconDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertModule, imports: [AccessibilityModule, CommonModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AlertModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, IconModule],
declarations: [AlertComponent, AlertIconDirective],
exports: [AlertComponent, AlertIconDirective],
}]
}] });
class BreadcrumbsComponent {
clickCrumb(event, crumb) {
if (crumb.onClick) {
crumb.onClick.call(null, event);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: BreadcrumbsComponent, isStandalone: false, selector: "ux-breadcrumbs", inputs: { crumbs: "crumbs" }, ngImport: i0, template: "<nav aria-label=\"Breadcrumb\">\n <ol class=\"breadcrumb\">\n @for (crumb of crumbs; track crumb) {\n <li>\n <!-- If there is a router link or an onClick function then use a tag -->\n @if (crumb.routerLink) {\n <a\n uxFocusIndicator\n tabindex=\"0\"\n [routerLink]=\"crumb.routerLink\"\n [fragment]=\"crumb.fragment\"\n [queryParams]=\"crumb.queryParams\"\n (click)=\"clickCrumb($event, crumb)\"\n >\n {{ crumb.title }}\n </a>\n }\n @if (crumb.onClick && !crumb.routerLink) {\n <a uxFocusIndicator tabindex=\"0\" (click)=\"clickCrumb($event, crumb)\">\n {{ crumb.title }}\n </a>\n }\n <!-- If there is no router link or onClick function then display text in a span -->\n @if (!crumb.routerLink && !crumb.onClick) {\n <span>{{ crumb.title }}</span>\n }\n </li>\n }\n </ol>\n</nav>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-breadcrumbs', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<nav aria-label=\"Breadcrumb\">\n <ol class=\"breadcrumb\">\n @for (crumb of crumbs; track crumb) {\n <li>\n <!-- If there is a router link or an onClick function then use a tag -->\n @if (crumb.routerLink) {\n <a\n uxFocusIndicator\n tabindex=\"0\"\n [routerLink]=\"crumb.routerLink\"\n [fragment]=\"crumb.fragment\"\n [queryParams]=\"crumb.queryParams\"\n (click)=\"clickCrumb($event, crumb)\"\n >\n {{ crumb.title }}\n </a>\n }\n @if (crumb.onClick && !crumb.routerLink) {\n <a uxFocusIndicator tabindex=\"0\" (click)=\"clickCrumb($event, crumb)\">\n {{ crumb.title }}\n </a>\n }\n <!-- If there is no router link or onClick function then display text in a span -->\n @if (!crumb.routerLink && !crumb.onClick) {\n <span>{{ crumb.title }}</span>\n }\n </li>\n }\n </ol>\n</nav>\n" }]
}], propDecorators: { crumbs: [{
type: Input
}] } });
class BreadcrumbsModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsModule, declarations: [BreadcrumbsComponent], imports: [AccessibilityModule, CommonModule, RouterModule], exports: [BreadcrumbsComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsModule, imports: [AccessibilityModule, CommonModule, RouterModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BreadcrumbsModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, RouterModule],
exports: [BreadcrumbsComponent],
declarations: [BreadcrumbsComponent],
}]
}] });
class ResizeService {
constructor() {
this._zone = inject(NgZone);
this._observer = new ResizeObserver(this.elementDidResize.bind(this));
this._targets = new WeakMap();
}
ngOnDestroy() {
this._observer.disconnect();
}
addResizeListener(target) {
this._zone.runOutsideAngular(() => this._observer.observe(target));
if (this._targets.has(target)) {
return this._targets.get(target);
}
else {
const emitter = new ReplaySubject();
this._targets.set(target, emitter);
return emitter;
}
}
removeResizeListener(target) {
this._observer.unobserve(target);
}
elementDidResize(entries) {
this._zone.run(() => {
for (const entry of entries) {
if (this._targets.has(entry.target)) {
const emitter = this._targets.get(entry.target);
emitter.next({
width: entry.target.offsetWidth,
height: entry.target.offsetHeight,
});
}
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeService, decorators: [{
type: Injectable
}] });
class ResizeDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._resizeService = inject(ResizeService);
this._ngZone = inject(NgZone);
/** Debounce the resize event emitter */
this.throttle = 0;
/** Emits whenever a resize event occurs */
this.uxResize = new EventEmitter();
/** Remove all subscriptions on component destroy */
this._onDestroy = new Subject();
}
ngOnInit() {
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy), debounceTime(this.throttle))
.subscribe((event) => this._ngZone.run(() => this.uxResize.emit(event)));
}
ngOnDestroy() {
this._resizeService.removeResizeListener(this._elementRef.nativeElement);
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ResizeDirective, isStandalone: false, selector: "[uxResize]", inputs: { throttle: "throttle" }, outputs: { uxResize: "uxResize" }, providers: [ResizeService], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxResize]',
providers: [ResizeService],
standalone: false,
}]
}], propDecorators: { throttle: [{
type: Input
}], uxResize: [{
type: Output
}] } });
class ResizeModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ResizeModule, declarations: [ResizeDirective], exports: [ResizeDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeModule, providers: [ResizeService] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizeModule, decorators: [{
type: NgModule,
args: [{
exports: [ResizeDirective],
declarations: [ResizeDirective],
providers: [ResizeService],
}]
}] });
class CardTabContentDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: CardTabContentDirective, isStandalone: false, selector: "[uxCardTabContent]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabContentDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxCardTabContent]',
standalone: false,
}]
}] });
class CardTabsService {
constructor() {
this.tab$ = new BehaviorSubject(null);
this.tabs$ = new BehaviorSubject([]);
this.position$ = new BehaviorSubject('top');
// when a tab is added or removed ensure we always select one if any are available
this._subscription = this.tabs$
.pipe(filter(tabs => !this.tab$.value || !tabs.find(tab => tab === this.tab$.value)))
.subscribe(tabs => this.tab$.next(tabs.length > 0 ? tabs[0] : null));
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
/**
* Add a tab to the list of tabs
*/
addTab(tab) {
this.tabs$.next([...this.tabs$.value, tab]);
}
/**
* Remove a tab from the list
*/
removeTab(tab) {
this.tabs$.next(this.tabs$.value.filter(_tab => _tab !== tab));
}
/**
* Select the tab
*/
select(tab) {
this.tab$.next(tab);
}
/**
* Set the position of the tab content
*/
setPosition(position) {
this.position$.next(position);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class CardTabComponent {
constructor() {
this._tabService = inject(CardTabsService);
this.active$ = this._tabService.tab$.pipe(map(tab => tab === this));
this._tabService.addTab(this);
}
ngOnDestroy() {
this._tabService.removeTab(this);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: CardTabComponent, isStandalone: false, selector: "ux-card-tab", queries: [{ propertyName: "content", first: true, predicate: CardTabContentDirective, descendants: true, read: TemplateRef }], ngImport: i0, template: "@if (active$ | async) {\n <ng-content></ng-content>\n}\n", dependencies: [{ kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-card-tab', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "@if (active$ | async) {\n <ng-content></ng-content>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { content: [{
type: ContentChild,
args: [CardTabContentDirective, { read: TemplateRef, static: false }]
}] } });
class CardTabsetComponent {
constructor() {
this.tabService = inject(CardTabsService);
this.offset = 0;
this.bounds = { lower: 0, upper: 0 };
}
set position(direction) {
this.tabService.setPosition(direction);
}
get position() {
return this.tabService.position$.getValue();
}
select(tab, element) {
// select the tab
this.tabService.select(tab);
// ensure the tab is moved into view if required
this.moveIntoView(element);
}
resize(dimensions) {
this._width = dimensions.width;
this._innerWidth = this.tablist.nativeElement.scrollWidth;
this.bounds.lower = 0;
this.bounds.upper = -(this._innerWidth - this._width);
}
previous() {
this.offset += this._width;
// ensure it remains within the allowed bounds
this.offset = Math.min(this.offset, this.bounds.lower);
}
next() {
this.offset -= this._width;
// ensure it remains within the allowed bounds
this.offset = Math.max(this.offset, this.bounds.upper);
}
moveIntoView(element) {
// if we dont have the dimensions we cant check
if (!this._width || !this._innerWidth) {
return;
}
// get the current element bounds
const { offsetLeft, offsetWidth } = element;
const { marginLeft, marginRight } = getComputedStyle(element);
// calculate the visible area
const viewportStart = Math.abs(this.offset);
const viewportEnd = viewportStart + this._width;
const cardWidth = parseFloat(marginLeft) + offsetWidth + parseFloat(marginRight);
// if we need to move to the left - figure out how much
if (offsetLeft < viewportStart) {
this.offset -= offsetLeft - parseFloat(marginLeft) - viewportStart;
}
// if we need to move to the right - figure out how much
if (offsetLeft + cardWidth > viewportEnd) {
this.offset -= offsetLeft + cardWidth - viewportEnd;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: CardTabsetComponent, isStandalone: false, selector: "ux-card-tabset", inputs: { position: "position" }, host: { properties: { "class": "this.position" } }, providers: [CardTabsService], viewQueries: [{ propertyName: "tablist", first: true, predicate: ["tablist"], descendants: true, static: true }], ngImport: i0, template: "@if (tabService.tab$ | async) {\n <div class=\"card-tab-content\" role=\"tabpanel\">\n <ng-content></ng-content>\n </div>\n}\n\n<div class=\"card-tabs\" #tabs>\n @if (offset < bounds.lower) {\n <button\n type=\"button\"\n class=\"card-tabs-paging-btn card-tabs-paging-btn-previous\"\n aria-label=\"Previous Tabs\"\n (click)=\"previous()\"\n >\n <ux-icon name=\"previous\"></ux-icon>\n </button>\n }\n\n <div\n class=\"card-tabs-list\"\n role=\"tablist\"\n #tablist\n (uxResize)=\"resize($event)\"\n [style.transform]=\"'translateX(' + offset + 'px)'\"\n >\n @for (tab of tabService.tabs$ | async; track tab) {\n <div\n class=\"card-tab\"\n role=\"tab\"\n tabindex=\"0\"\n #card\n [ngClass]=\"tabService.position$ | async\"\n [class.active]=\"tab.active$ | async\"\n [attr.aria-selected]=\"tab.active$ | async\"\n (click)=\"select(tab, card)\"\n (focus)=\"tabs.scrollLeft = 0\"\n (keydown.enter)=\"select(tab, card)\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.content\"></ng-container>\n </div>\n }\n </div>\n\n @if (offset > bounds.upper) {\n <button\n type=\"button\"\n class=\"card-tabs-paging-btn card-tabs-paging-btn-next\"\n aria-label=\"Next Tabs\"\n (click)=\"next()\"\n >\n <ux-icon name=\"next\"></ux-icon>\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ResizeDirective, selector: "[uxResize]", inputs: ["throttle"], outputs: ["uxResize"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsetComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-card-tabset', providers: [CardTabsService], standalone: false, template: "@if (tabService.tab$ | async) {\n <div class=\"card-tab-content\" role=\"tabpanel\">\n <ng-content></ng-content>\n </div>\n}\n\n<div class=\"card-tabs\" #tabs>\n @if (offset < bounds.lower) {\n <button\n type=\"button\"\n class=\"card-tabs-paging-btn card-tabs-paging-btn-previous\"\n aria-label=\"Previous Tabs\"\n (click)=\"previous()\"\n >\n <ux-icon name=\"previous\"></ux-icon>\n </button>\n }\n\n <div\n class=\"card-tabs-list\"\n role=\"tablist\"\n #tablist\n (uxResize)=\"resize($event)\"\n [style.transform]=\"'translateX(' + offset + 'px)'\"\n >\n @for (tab of tabService.tabs$ | async; track tab) {\n <div\n class=\"card-tab\"\n role=\"tab\"\n tabindex=\"0\"\n #card\n [ngClass]=\"tabService.position$ | async\"\n [class.active]=\"tab.active$ | async\"\n [attr.aria-selected]=\"tab.active$ | async\"\n (click)=\"select(tab, card)\"\n (focus)=\"tabs.scrollLeft = 0\"\n (keydown.enter)=\"select(tab, card)\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.content\"></ng-container>\n </div>\n }\n </div>\n\n @if (offset > bounds.upper) {\n <button\n type=\"button\"\n class=\"card-tabs-paging-btn card-tabs-paging-btn-next\"\n aria-label=\"Next Tabs\"\n (click)=\"next()\"\n >\n <ux-icon name=\"next\"></ux-icon>\n </button>\n }\n</div>\n" }]
}], propDecorators: { position: [{
type: HostBinding,
args: ['class']
}, {
type: Input
}], tablist: [{
type: ViewChild,
args: ['tablist', { static: true }]
}] } });
class CardTabsModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: CardTabsModule, declarations: [CardTabsetComponent, CardTabComponent, CardTabContentDirective], imports: [CommonModule, ResizeModule, IconModule], exports: [CardTabsetComponent, CardTabComponent, CardTabContentDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsModule, imports: [CommonModule, ResizeModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CardTabsModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, ResizeModule, IconModule],
declarations: [CardTabsetComponent, CardTabComponent, CardTabContentDirective],
exports: [CardTabsetComponent, CardTabComponent, CardTabContentDirective],
}]
}] });
class MenuDividerComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: MenuDividerComponent, isStandalone: false, selector: "ux-menu-divider", host: { attributes: { "role": "separator" } }, ngImport: i0, template: "", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuDividerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-menu-divider', changeDetection: ChangeDetectionStrategy.OnPush, host: {
role: 'separator',
}, standalone: false, template: "" }]
}] });
/**
* This is used to avoid having to do an `instanceof` check
* which would cause a circular dependency between the
* `MenuComponent` and `MenuItemComponent`
*/
var MenuItemType;
(function (MenuItemType) {
MenuItemType[MenuItemType["Default"] = 0] = "Default";
MenuItemType[MenuItemType["Custom"] = 1] = "Custom";
})(MenuItemType || (MenuItemType = {}));
const MENU_OPTIONS_TOKEN = new InjectionToken('MENU_OPTIONS_TOKEN');
let uniqueId$d = 0;
class MenuComponent {
constructor() {
this._options = inject(MENU_OPTIONS_TOKEN, { optional: true });
this._changeDetector = inject(ChangeDetectorRef);
/** A unique id for the component. */
this.id = `ux-menu-${++uniqueId$d}`;
/** Define the position of the menu */
this.placement = 'bottom';
/** Define the alignment of the menu */
this.alignment = 'start';
/** Define if we should animate the menu */
// eslint-disable-next-line no-prototype-builtins
this.animate = this._options && 'animate' in this._options ? this._options.animate : true;
/** Emit when the opening has begun (the opened EventEmitter waits until the animation has finished) */
this.opening = new EventEmitter();
/** Emit when the menu is opened */
this.opened = new EventEmitter();
/** Emit whenever closing has begun (the closed EventEmitter waits until animation has finished) */
this.closing = new EventEmitter();
/** Emit when the menu is closed */
this.closed = new EventEmitter();
/** Store the menu open state */
this.isMenuOpen = false;
/** Store the animation state */
this._isAnimating = false;
/** Determine if this is a submenu */
this._isSubMenu = false;
/** Whether this menu should close when it loses focus */
this._closeOnBlur = false;
/** Emit when the focused item changes (we use this as the key manager is not instantiated until a late lifecycle hook) */
this._activeItem$ = new BehaviorSubject(null);
/** Access allow a close event to propagate all the way up the submenus */
this._closeAll$ = new Subject();
/** Emit keyboard events */
this._onKeydown$ = new Subject();
/** Emit hover events */
this._isHovering$ = new BehaviorSubject(false);
/** Emit focus events */
this._isFocused$ = new BehaviorSubject(false);
/** Emit placement change */
this._placement$ = new BehaviorSubject('bottom');
this._alignment$ = new BehaviorSubject('start');
/** Access all child menu items for accessibility purposes */
this._items$ = new BehaviorSubject([]);
/** Automatically unsubscribe when the component is destroyed */
this._onDestroy$ = new Subject();
this._isTabPressed = false;
/** Create an internal querylist to store the menu items */
this._itemsList = new QueryList();
}
/** Get innerId for use for accessibility */
get innerId() {
return `${this.id}-menu`;
}
get _isExpanded() {
return this._menuItems.pipe(switchMap(items => merge(...items.map(item => item.isExpanded$))), takeUntil(this._onDestroy$));
}
get _menuItemClick() {
return this._menuItems.pipe(switchMap(items => merge(...items.map(item => item.onClick$))), takeUntil(this._onDestroy$));
}
/** Return only menu items an not custom tabbable items */
get _menuItems() {
return this._items$.pipe(map(items => items.filter(item => item.type === MenuItemType.Default)));
}
ngAfterContentInit() {
// initialise the query list with the items
this._items$.pipe(takeUntil(this._onDestroy$)).subscribe(items => {
// if no items has been marked as tabbable then this should be
if (!this._activeItem$.value && items.length > 0) {
this._activeItem$.next(items[0]);
}
this._itemsList.reset(items);
this._itemsList.notifyOnChanges();
});
// setup keyboard functionality
this._keyManager = new FocusKeyManager(this._itemsList)
.withVerticalOrientation()
.withTypeAhead()
.withWrap();
// emit the tabbable item on change
this._keyManager.change
.pipe(map(() => this._keyManager.activeItem), takeUntil(this._onDestroy$))
.subscribe(item => this._activeItem$.next(item));
}
ngOnChanges(changes) {
if (changes.placement && changes.placement.currentValue !== changes.placement.previousValue) {
this._placement$.next(changes.placement.currentValue);
}
if (changes.alignment && changes.alignment.currentValue !== changes.alignment.previousValue) {
this._alignment$.next(changes.alignment.currentValue);
}
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
this._closeAll$.complete();
this._isHovering$.complete();
this._isFocused$.complete();
this._activeItem$.complete();
this._items$.complete();
this._placement$.complete();
}
/** Register a menu item - we do this do avoid `@ContentChildren` detecting submenu items */
_addItem(item) {
if (!this.hasItem(item)) {
this._items$.next([...this._items$.value, item]);
}
}
/** Remove an item */
_removeItem(item) {
if (this.hasItem(item)) {
this._items$.next(this._items$.value.filter(_item => _item !== item));
}
}
/** Determine if an item exists */
hasItem(item) {
return !!this._items$.value.find(_item => _item === item);
}
/** Internal function to set the open state and run change detection */
_setMenuOpen(menuOpen) {
// store the open state
this.isMenuOpen = menuOpen;
// if we are closing the menu reset some values
if (!menuOpen) {
this._isHovering$.next(false);
this._isFocused$.next(false);
}
// the change detector is actually an instance of a ViewRef (which extends ChangeDetectorRef) when used within a component
// and the ViewRef contains the destroyed state of the component which is more reliable
// than setting a flag in ngOnDestroy as the component can be destroyed before
// the lifecycle hook is called
const viewRef = this._changeDetector;
// check for changes - required to show the menu as we are using `*ngIf`
if (!viewRef.destroyed) {
this._changeDetector.detectChanges();
}
// emit the closing event
menuOpen ? this.opening.emit() : this.closing.emit();
}
/** Close the menu if the focus event is null */
_focusChange(event) {
if (event === null && this._closeOnBlur && this._isTabPressed) {
this._closeAll$.next('tabout');
}
}
/** Track the animation state */
_onAnimationStart() {
this._isAnimating = true;
}
/** Track animation state and emit event when opening or closing */
_onAnimationDone() {
this._isAnimating = false;
if (this.isMenuOpen) {
this.opened.emit();
}
else {
this.closed.emit();
}
}
_closeMenu() {
this._setMenuOpen(false);
}
/** Forward any keyboard events to the key manage for accessibility */
_onKeydown(event) {
this._keyManager.setFocusOrigin('keyboard').onKeydown(event);
// emit the keydown event
this._onKeydown$.next(event);
}
_onHoverStart() {
this._isHovering$.next(true);
}
_onHoverEnd() {
this._isHovering$.next(false);
}
_onFocus() {
this._isFocused$.next(true);
}
_onBlur() {
this._isFocused$.next(false);
}
_onKeyDown(event) {
this._isTabPressed = event.keyCode === TAB;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MenuComponent, isStandalone: false, selector: "ux-menu", inputs: { id: "id", placement: "placement", alignment: "alignment", animate: "animate", menuClass: "menuClass" }, outputs: { opening: "opening", opened: "opened", closing: "closing", closed: "closed" }, host: { properties: { "attr.id": "this.id" } }, viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-template>\n @if (isMenuOpen) {\n <div\n class=\"ux-menu\"\n role=\"menu\"\n [id]=\"innerId\"\n [class.ux-sub-menu]=\"_isSubMenu\"\n [ngClass]=\"menuClass\"\n @menuAnimation\n [@.disabled]=\"!animate\"\n (@menuAnimation.start)=\"_onAnimationStart()\"\n (@menuAnimation.done)=\"_onAnimationDone()\"\n (mouseenter)=\"_onHoverStart()\"\n (mouseover)=\"_onHoverStart()\"\n (mouseleave)=\"_onHoverEnd()\"\n (focusin)=\"_onFocus()\"\n (focusout)=\"_onBlur()\"\n (keydown)=\"_onKeyDown($event)\"\n cdkMonitorSubtreeFocus\n [cdkTrapFocus]=\"_closeOnBlur\"\n (cdkFocusChange)=\"_focusChange($event)\"\n >\n <ng-content></ng-content>\n </div>\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: i1$1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: i1$1.CdkMonitorFocus, selector: "[cdkMonitorElementFocus], [cdkMonitorSubtreeFocus]", outputs: ["cdkFocusChange"], exportAs: ["cdkMonitorFocus"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], animations: [
trigger('menuAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'scaleY(0)' }),
animate('200ms ease-out', style({ opacity: 1, transform: 'none' })),
]),
transition(':leave', [
animate('200ms ease-out', style({ opacity: 0, transform: 'scaleY(0)' })),
]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-menu', changeDetection: ChangeDetectionStrategy.OnPush, animations: [
trigger('menuAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'scaleY(0)' }),
animate('200ms ease-out', style({ opacity: 1, transform: 'none' })),
]),
transition(':leave', [
animate('200ms ease-out', style({ opacity: 0, transform: 'scaleY(0)' })),
]),
]),
], standalone: false, template: "<ng-template>\n @if (isMenuOpen) {\n <div\n class=\"ux-menu\"\n role=\"menu\"\n [id]=\"innerId\"\n [class.ux-sub-menu]=\"_isSubMenu\"\n [ngClass]=\"menuClass\"\n @menuAnimation\n [@.disabled]=\"!animate\"\n (@menuAnimation.start)=\"_onAnimationStart()\"\n (@menuAnimation.done)=\"_onAnimationDone()\"\n (mouseenter)=\"_onHoverStart()\"\n (mouseover)=\"_onHoverStart()\"\n (mouseleave)=\"_onHoverEnd()\"\n (focusin)=\"_onFocus()\"\n (focusout)=\"_onBlur()\"\n (keydown)=\"_onKeyDown($event)\"\n cdkMonitorSubtreeFocus\n [cdkTrapFocus]=\"_closeOnBlur\"\n (cdkFocusChange)=\"_focusChange($event)\"\n >\n <ng-content></ng-content>\n </div>\n }\n</ng-template>\n" }]
}], propDecorators: { id: [{
type: Input
}, {
type: HostBinding,
args: ['attr.id']
}], placement: [{
type: Input
}], alignment: [{
type: Input
}], animate: [{
type: Input
}], menuClass: [{
type: Input
}], opening: [{
type: Output
}], opened: [{
type: Output
}], closing: [{
type: Output
}], closed: [{
type: Output
}], templateRef: [{
type: ViewChild,
args: [TemplateRef, { static: false }]
}] } });
class MenuInitialFocusDirective {
constructor() {
this._menu = inject(MenuComponent);
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._onDestroy = new Subject();
}
ngOnInit() {
this.ensureFocusable();
// Focus the host element when the parent menu is opened.
this._menu.opened.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this._elementRef.nativeElement.focus();
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Apply tabindex="0" to the element if it's not already focusable. */
ensureFocusable() {
if (this._elementRef.nativeElement.tabIndex >= 0) {
return;
}
this._renderer.setAttribute(this._elementRef.nativeElement, 'tabindex', '0');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuInitialFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuInitialFocusDirective, isStandalone: false, selector: "[uxMenuInitialFocus]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuInitialFocusDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuInitialFocus]',
standalone: false,
}]
}] });
class MenuTabbableItemDirective {
constructor() {
this._menu = inject(MenuComponent);
this._elementRef = inject(ElementRef);
this._focusIndicatorService = inject(FocusIndicatorService);
this._renderer = inject(Renderer2);
/** Define if this item is disabled or not */
this.disabled = false;
/** Indicate the type of the menu item */
this.type = MenuItemType.Default;
/** Automatically unsubscribe when directive is destroyed */
this._onDestroy$ = new Subject();
}
ngOnInit() {
// register this item in the MenuComponent
this._menu._addItem(this);
// we only want to show the focus indicator whenever the keyboard is used
this.focusIndicator = this._focusIndicatorService.monitor(this._elementRef.nativeElement);
// subscribe to active item changes
this._menu._activeItem$
.pipe(takeUntil(this._onDestroy$))
.subscribe(item => this.setTabIndex(item === this));
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
this.focusIndicator.destroy();
}
/** Focus this item with a given origin */
focus(origin) {
this.focusIndicator.focus(origin);
}
/** This function is built into the CDK manager to allow jumping to items based on text content */
getLabel() {
return this._elementRef.nativeElement.textContent.trim();
}
/** Forward any keyboard events to the MenuComponent for accessibility */
_onKeydown(event) {
this._menu._onKeydown(event);
}
/** Update the tab index on this item */
setTabIndex(isTabbable) {
this._renderer.setAttribute(this._elementRef.nativeElement, 'tabindex', isTabbable ? '0' : '-1');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuTabbableItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuTabbableItemDirective, isStandalone: false, selector: "[uxMenuTabbableItem]", inputs: { disabled: "disabled" }, host: { listeners: { "keydown": "_onKeydown($event)" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuTabbableItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuTabbableItem]',
standalone: false,
}]
}], propDecorators: { disabled: [{
type: Input
}], _onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
const FocusableItemToken = new InjectionToken('Focusable Option');
class MenuItemCustomControlDirective extends MenuTabbableItemDirective {
constructor() {
super();
this._focusableControl = inject(FocusableItemToken, { optional: true });
/** Indicate the type of the menu item */
this.type = MenuItemType.Custom;
}
ngOnInit() {
// register this item in the MenuComponent
super.ngOnInit();
this._menu.opened.pipe(takeUntil(this._onDestroy$)).subscribe(() => {
// remove any existing tab index on component instance and have it handled by this directive
this._focusableControl?.setInputTabIndex(-1);
});
}
/** Focus this item with a given origin */
focus(origin) {
super.focus(origin);
this._focusableControl
? this._focusableControl.focus(origin)
: this._elementRef.nativeElement.focus();
}
/** We want to remove the ability to shift+tab back into the parent element */
setTabIndex() {
super.setTabIndex(false);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuItemCustomControlDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuItemCustomControlDirective, isStandalone: false, selector: "[uxMenuItemCustomControl]", host: { attributes: { "role": "menuitem" }, properties: { "class.ux-menu-item": "true" } }, usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuItemCustomControlDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuItemCustomControl]',
host: {
'[class.ux-menu-item]': 'true',
role: 'menuitem',
},
standalone: false,
}]
}], ctorParameters: () => [] });
class MenuItemComponent {
constructor() {
this._menu = inject(MenuComponent);
this._elementRef = inject(ElementRef);
this._focusIndicatorService = inject(FocusIndicatorService);
this._renderer = inject(Renderer2);
/** Define the role of the element */
this.role = 'menuitem';
/** Emits when the menu item is clicked or the enter key is pressed. */
this.activate = new EventEmitter();
/** Indicate the type of the menu item */
this.type = MenuItemType.Default;
/** Store the current hover state */
this.isHovered$ = new BehaviorSubject(false);
/** Store the current focus state */
this.isFocused$ = new BehaviorSubject(false);
/** Store the current expanded state */
this.isExpanded$ = new BehaviorSubject(false);
/** Emit when an item is clicked */
this.onClick$ = new Subject();
/** Automatically unsubscribe from observables on destroy */
this._onDestroy$ = new Subject();
this._disabled = false;
this._closeOnSelect = true;
}
/** Define if this item is disabled or not */
set disabled(disabled) {
this._disabled = coerceBooleanProperty(disabled);
}
get disabled() {
return this._disabled;
}
/** Determine if the menu should close on item click/enter.*/
set closeOnSelect(value) {
this._closeOnSelect = coerceBooleanProperty(value);
}
get closeOnSelect() {
return this._closeOnSelect;
}
/** Access the open state */
get isOpen() {
return this._menu.isMenuOpen;
}
ngOnInit() {
// register this item in the MenuComponent
this._menu._addItem(this);
// we only want to show the focus indicator whenever the keyboard is used
this._focusIndicator = this._focusIndicatorService.monitor(this._elementRef.nativeElement);
// subscribe to active item changes
this._menu._activeItem$
.pipe(takeUntil(this._onDestroy$))
.subscribe(item => this.setTabIndex(item === this));
}
ngOnDestroy() {
this._menu._removeItem(this);
this.isHovered$.complete();
this.isExpanded$.complete();
this.isFocused$.complete();
this.onClick$.complete();
this._focusIndicator.destroy();
this._onDestroy$.next();
this._onDestroy$.complete();
}
focus(origin) {
this._focusIndicator.focus(origin);
}
/** This function is built into the CDK manager to allow jumping to items based on text content */
getLabel() {
return this._elementRef.nativeElement.textContent.trim();
}
_onMouseEnter() {
this.isHovered$.next(true);
}
_onMouseLeave() {
this.isHovered$.next(false);
}
_onFocus() {
this.isFocused$.next(true);
}
_onBlur() {
this.isFocused$.next(false);
}
_onClick(event) {
if (!this.disabled) {
if (this.closeOnSelect) {
this.onClick$.next(isKeyboardTrigger(event) ? 'keyboard' : 'mouse');
}
this.activate.emit(event);
}
}
/** Forward any keyboard events to the MenuComponent for accessibility */
_onKeydown(event) {
this._menu._onKeydown(event);
}
/** Update the tab index on this item */
setTabIndex(isTabbable) {
this._renderer.setAttribute(this._elementRef.nativeElement, 'tabindex', isTabbable ? '0' : '-1');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: MenuItemComponent, isStandalone: false, selector: "[uxMenuItem]", inputs: { disabled: "disabled", closeOnSelect: "closeOnSelect", role: "role" }, outputs: { activate: "activate" }, host: { listeners: { "mouseenter": "_onMouseEnter()", "mouseleave": "_onMouseLeave()", "focus": "_onFocus()", "blur": "_onBlur()", "click": "_onClick($event)", "keydown.enter": "_onClick($event)", "keydown": "_onKeydown($event)" }, properties: { "attr.disabled": "disabled ? \"disabled\" : null", "attr.role": "role", "class.disabled": "disabled", "class.ux-menu-item": "true", "class.open": "isOpen" } }, ngImport: i0, template: "<ng-content></ng-content>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuItemComponent, decorators: [{
type: Component,
args: [{ selector: '[uxMenuItem]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[attr.disabled]': 'disabled ? "disabled" : null',
'[attr.role]': 'role',
'[class.disabled]': 'disabled',
'[class.ux-menu-item]': 'true',
'[class.open]': 'isOpen',
}, standalone: false, template: "<ng-content></ng-content>\n" }]
}], propDecorators: { disabled: [{
type: Input
}], closeOnSelect: [{
type: Input
}], role: [{
type: Input
}], activate: [{
type: Output
}], _onMouseEnter: [{
type: HostListener,
args: ['mouseenter']
}], _onMouseLeave: [{
type: HostListener,
args: ['mouseleave']
}], _onFocus: [{
type: HostListener,
args: ['focus']
}], _onBlur: [{
type: HostListener,
args: ['blur']
}], _onClick: [{
type: HostListener,
args: ['click', ['$event']]
}, {
type: HostListener,
args: ['keydown.enter', ['$event']]
}], _onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class OverlayPlacementService {
/** Updates the position of the current menu. */
updatePosition(overlayRef, placement, alignment, customFallbackPlacement) {
const position = overlayRef.getConfig().positionStrategy;
const origin = this.getOrigin(placement, alignment);
const overlay = this.getOverlayPosition(placement, alignment);
position.withPositions(this.addPositions(origin, overlay, customFallbackPlacement));
}
/** Apply position to position strategy */
addPositions(origin, overlay, customFallbackPlacement) {
if (customFallbackPlacement) {
return [
{ ...origin.main, ...overlay.main },
this.getFallbackPosition(customFallbackPlacement),
];
}
else {
return [
{ ...origin.main, ...overlay.main },
{ ...origin.fallback, ...overlay.fallback },
{
...{
originX: origin.main.originX,
originY: this.invertHorizontalPosition(origin.main.originY),
},
...{
overlayX: overlay.main.overlayX,
overlayY: this.invertHorizontalPosition(overlay.main.overlayY),
},
},
{
...{
originX: origin.fallback.originX,
originY: this.invertHorizontalPosition(origin.fallback.originY),
},
...{
overlayX: overlay.fallback.overlayX,
overlayY: this.invertHorizontalPosition(overlay.fallback.overlayY),
},
},
];
}
}
/** Get the origin position based on the specified tooltip placement */
getOrigin(initialPlacement, alignment) {
// ensure placement is defined
const placement = initialPlacement || 'bottom';
let originPosition;
if (placement === 'top' || placement === 'bottom') {
originPosition = { originX: alignment, originY: placement };
}
if (placement === 'left') {
originPosition = { originX: 'start', originY: this.getVerticalAlignment(alignment) };
}
if (placement === 'right') {
originPosition = { originX: 'end', originY: this.getVerticalAlignment(alignment) };
}
const { x, y } = this.invertPosition(placement, originPosition.originX, originPosition.originY);
return {
main: originPosition,
fallback: { originX: x, originY: y },
};
}
/** Calculate the overlay position based on the specified tooltip placement */
getOverlayPosition(initialPlacement, alignment) {
// ensure placement is defined
const placement = initialPlacement || 'top';
let overlayPosition;
if (placement === 'top') {
overlayPosition = { overlayX: alignment, overlayY: 'bottom' };
}
if (placement === 'bottom') {
overlayPosition = { overlayX: alignment, overlayY: 'top' };
}
if (placement === 'left') {
overlayPosition = { overlayX: 'end', overlayY: this.getVerticalAlignment(alignment) };
}
if (placement === 'right') {
overlayPosition = { overlayX: 'start', overlayY: this.getVerticalAlignment(alignment) };
}
const { x, y } = this.invertPosition(placement, overlayPosition.overlayX, overlayPosition.overlayY);
return {
main: overlayPosition,
fallback: { overlayX: x, overlayY: y },
};
}
/** Convert the alignment property to a valid CDK alignment value */
getVerticalAlignment(alignment) {
switch (alignment) {
case 'start':
return 'top';
case 'end':
return 'bottom';
default:
return alignment;
}
}
/** Inverts an overlay position. */
invertPosition(placement, x, y) {
if (placement === 'top' || placement === 'bottom') {
if (y === 'top') {
y = 'bottom';
}
else if (y === 'bottom') {
y = 'top';
}
}
else {
if (x === 'end') {
x = 'start';
}
else if (x === 'start') {
x = 'end';
}
}
return { x, y };
}
invertHorizontalPosition(y) {
return y === 'top' ? 'bottom' : 'top';
}
getFallbackPosition(fallbackPlacement) {
switch (fallbackPlacement) {
case 'left':
return { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center' };
case 'right':
return { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center' };
case 'top':
return { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' };
case 'bottom':
return { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' };
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayPlacementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayPlacementService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverlayPlacementService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class MenuTriggerDirective {
constructor() {
this._overlay = inject(Overlay);
this._elementRef = inject(ElementRef);
this._viewContainerRef = inject(ViewContainerRef);
this._focusOrigin = inject(FocusIndicatorOriginService);
this._focusIndicatorService = inject(FocusIndicatorService);
this._overlayPlacement = inject(OverlayPlacementService);
this._parentMenu = inject(MenuComponent, { optional: true });
this._menuItem = inject(MenuItemComponent, { optional: true, self: true });
/** Determine if we should disable the trigger */
this.disabled = false;
/** Emit when the menu is closed */
this.closed = new EventEmitter();
/** Automatically unsubscribe on directive destroy */
this._onDestroy$ = new Subject();
/** Reference to the menu should close when it loses focus */
this._closeOnBlur = false;
this._debounceTime = 50;
}
/** Determine if the menu should close when it loses focus */
set closeOnBlur(value) {
this._closeOnBlur = coerceBooleanProperty(value);
}
get closeOnBlur() {
return this._closeOnBlur;
}
/** Get the aria controls for accessibility */
get ariaControls() {
return this.menu?.isMenuOpen ? this.menu?.innerId : null;
}
/** Determine if this triggers a submenu */
get _isSubmenuTrigger() {
return !!this._parentMenu;
}
/** Determine if this is the root trigger */
get _isRootTrigger() {
return !this._isSubmenuTrigger;
}
/** If this is a submenu we want to know when the mouse leaves the items or parent item */
get _menuShouldClose() {
if (!this._isSubmenuTrigger) {
return of();
}
// This combined observable will essentially check for all of the combinations of events that can cause a menu
// to remain open, for example:
//
// 1. Hovering over any item in the menu should keep the menu open
// 2. Having any item in the menu focused should keep the menu open
// 3. Having the parent menu item hovered should keep a submenu open
// 4. Having the parent menu item focused should keep a submenu open
// 5. Having a submenu open should keep the parent open (if the submenu meets one of the above conditions)
//
// We also debounce this because there is often a delay between a blur and a focus event or moving the mouse
// from a menu item to a sub menu item, so we add this buffer time to prevent the menu from closing unexpectedly
return combineLatest([
this.menu._isHovering$,
this.menu._isFocused$,
this._menuItem.isHovered$,
this.menu._isExpanded,
this._menuItem.isFocused$,
]).pipe(debounceTime(50), filter(([isHovered, isFocused, isItemHovered, isExpanded, isItemFocused]) => !isHovered && !isFocused && !isItemHovered && !isExpanded && !isItemFocused));
}
ngOnInit() {
// set up focus indicator handling
this._focusIndicator = this._focusIndicatorService.monitor(this._elementRef.nativeElement);
// if there is a parent menu then we should override the default initial
// position to be to the right rather than beneath. Note this gets called
// before ngOnInit in the MenuComponent so if the user specifies an explicit
// position then it will still take precendence
if (this._isSubmenuTrigger) {
this.menu._isSubMenu = true;
this.menu.placement = this.getSubMenuPlacement(this.menu.placement);
}
// listen for the menu to open (after animation so we can focus the first item)
this.menu.opened.pipe(takeUntil(this._onDestroy$)).subscribe(() => this.menuDidOpen());
// propagate the close event if it is triggered
this.menu._closeAll$.pipe(takeUntil(this._onDestroy$)).subscribe(origin => {
if (origin === 'tabout' && this._isRootTrigger) {
this.closeMenu('keyboard', true, true, true);
}
else {
this.closeMenu(origin, true);
}
});
// handle keyboard events in the menu
this.menu._onKeydown$
.pipe(takeUntil(this._onDestroy$))
.subscribe(event => this.onMenuKeydown(event));
combineLatest([this.menu._placement$, this.menu._alignment$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => {
if (this._isSubmenuTrigger) {
this.menu.placement = this.getSubMenuPlacement(this.menu.placement);
}
this.getOverlay(true);
});
}
ngOnDestroy() {
this.destroyMenu();
this._onDestroy$.next();
this._onDestroy$.complete();
}
/** Focus the next focusable element */
focusNextElement() {
// add elements we want to include in our selection
const focusableElements = 'a:not([disabled]), button:not([disabled]), input:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
if (document.activeElement) {
const focusable = document.querySelectorAll(focusableElements);
const suitableElements = [];
focusable.forEach(element => {
if ((element.offsetWidth > 0 ||
element.offsetHeight > 0 ||
element === document.activeElement) &&
!element.classList.contains('cdk-visually-hidden')) {
suitableElements.push(element);
}
});
const index = suitableElements.indexOf(document.activeElement);
if (index > -1) {
const nextElement = suitableElements[index + 1] || focusable[0];
nextElement.focus();
}
}
}
/** Open the menu */
openMenu() {
// if the menu is already open then do nothing
if (this.menu.isMenuOpen || this.disabled) {
return;
}
this.menu._closeOnBlur = this.closeOnBlur;
// get or create an overlayRef
const overlayRef = this.getOverlay();
const portal = this.getPortal();
// if the overlay is already attached do nothing
if (overlayRef.hasAttached()) {
return;
}
// attach the menu to the DOM
overlayRef.attach(portal);
// mark the menu as open
this.menu._setMenuOpen(true);
if (this._menuItem) {
// timer is needed because isExpanded$ will get set to false
// prematurely due to the debounceTime on the menuShouldClose.
timer(this._debounceTime)
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => this._menuItem.isExpanded$.next(true));
}
// listen for a menu item to be selected
this.menu._menuItemClick
.pipe(take(1), takeUntil(this._onDestroy$))
.subscribe(origin => this.closeMenu(origin, true));
// subscribe to any close events
this.didMenuClose()
.pipe(take(1), takeUntil(this._onDestroy$))
.subscribe(() => this.closeMenu());
// listen for the menu to animate closed then destroy it, if submenu wait for it to start closing to destroy.
if (this._isSubmenuTrigger) {
this.menu.closing
.pipe(take(1), takeUntil(this._onDestroy$))
.subscribe(() => this.destroyMenu());
}
else {
this.menu.closed
.pipe(take(1), takeUntil(this._onDestroy$))
.subscribe(() => this.destroyMenu());
}
}
/** Close a menu or submenu */
closeMenu(origin, closeParents = false, focusTrigger = true, focusNextElement = false) {
if (!this._overlayRef.hasAttached()) {
return;
}
// update the menu state
this.menu._setMenuOpen(false);
if (this._menuItem) {
this._menuItem.isExpanded$.next(false);
}
// if we should close parents then propagate the event
if (closeParents && this._parentMenu) {
this._parentMenu._closeAll$.next(origin);
}
// we should focus the trigger element if this is the root trigger unless otherwise specified
if (this._isRootTrigger && focusTrigger) {
this._focusIndicator.focus(origin);
}
if (focusNextElement) {
this.focusNextElement();
}
this.closed.emit();
return this.menu.closed;
}
/** Toggle the open state of a menu */
toggleMenu(event) {
// if this occurs on a submenu trigger then we can skip
if (this._isSubmenuTrigger) {
return;
}
if (!this.menu._isAnimating) {
// determine the focus origin based on whether or not a keyboard was used
const origin = event instanceof KeyboardEvent ? 'keyboard' : 'mouse';
// set the correct focus origin - if triggered by an event then use the source otherwise it was programmatic
this._focusOrigin.setOrigin(event ? origin : 'program');
// toggle the menu open state
this.menu.isMenuOpen ? this.closeMenu(origin, true) : this.openMenu();
}
// the enter key will trigger the click event and therefore set the wrong focus origin
// so we nee to ensure this doesn't happen
if (event) {
event.preventDefault();
}
}
/** Submenus should be opened by hovering on the menu item */
_onMouseEnter() {
if (this._isSubmenuTrigger && !this._parentMenu._isAnimating) {
this.openMenu();
}
}
_onMouseMove() {
if (this._isSubmenuTrigger && !this._parentMenu._isAnimating) {
setTimeout(() => {
this.openMenu();
}, this._debounceTime);
}
}
/** Pressing the escape key should close all menus */
_onEscape() {
if (this.menu.isMenuOpen) {
this.closeMenu();
// refocus the root trigger and show the focus ring
if (this._isRootTrigger) {
this._focusIndicator.focus('keyboard');
}
}
}
/** Handle keyboard events for opening submenus */
_onKeydown(event) {
// arrow key in the correct direction should open the menu
if ((this.menu.placement === 'right' && event.keyCode === RIGHT_ARROW) ||
(this.menu.placement === 'left' && event.keyCode === LEFT_ARROW) ||
(this.menu.placement === 'top' && event.keyCode === UP_ARROW) ||
(this.menu.placement === 'bottom' && event.keyCode === DOWN_ARROW)) {
this._focusOrigin.setOrigin('keyboard');
// if the menu was opened by a click but we subsequently use the arrow keys focus the first item
if (this.menu.isMenuOpen) {
this.menu._keyManager.setFocusOrigin('keyboard').setFirstItemActive();
}
else {
// otherwise open the menu
this.openMenu();
}
// prevent the browser from scrolling
event.preventDefault();
}
}
/** Blurring the trigger should check if the menu has focus and close it if not */
_onBlur() {
if (this.closeOnBlur) {
this.closeOnFocusout();
}
}
/** Remove the menu from the DOM */
destroyMenu() {
// if the menu has been destroyed already then do nothing
if (!this._overlayRef) {
return;
}
// remove the overlay
this._overlayRef.detach();
}
/** Create an overlay or return an existing instance */
getOverlay(recreateOverlay = false) {
// if we have already created the overlay then reuse it
if (this._overlayRef && !recreateOverlay) {
return this._overlayRef;
}
const strategy = this._overlay
.position()
.flexibleConnectedTo(this.parent ?? this._elementRef)
.withFlexibleDimensions(false)
.withPush(false)
.withTransformOriginOn('.ux-menu');
// otherwise create a new one
this._overlayRef = this._overlay.create({
hasBackdrop: !this._isSubmenuTrigger,
backdropClass: 'cdk-overlay-transparent-backdrop',
scrollStrategy: this._overlay.scrollStrategies.reposition({ scrollThrottle: 0 }),
positionStrategy: strategy,
});
this._overlayPlacement.updatePosition(this._overlayRef, this.menu.placement, this.menu.alignment, undefined);
return this._overlayRef;
}
/** Create a Template portal if one does not already exist (or the template has changed) */
getPortal() {
// if there is no portal or the templateRef has changed then create a new one
if (!this._portal || this.menu.templateRef !== this._portal.templateRef) {
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
}
return this._portal;
}
/** Get an observable that emits on any of the triggers that close a menu */
didMenuClose() {
return merge(this._overlayRef.backdropClick(), this._parentMenu ? this._parentMenu.closing : of(), this._menuShouldClose);
}
/** When the menu opens we want to focus the first item in the list */
menuDidOpen() {
// if the keyboard is used we should always focus and show the indicator
// regardless of it this is the root menu or not
if (this._focusOrigin.getOrigin() === 'keyboard') {
this.menu._keyManager.setFocusOrigin('keyboard').setFirstItemActive();
}
}
/** Handle keypresses in submenus where we may want to close them */
onMenuKeydown(event) {
// if we are a submenu and the user presses an arrow key in the opposite
// direction than it is positioned from its parents then we should close the menu
if (this._parentMenu) {
if ((this.menu.placement === 'right' && event.keyCode === LEFT_ARROW) ||
(this.menu.placement === 'left' && event.keyCode === RIGHT_ARROW) ||
(this.menu.placement === 'top' && event.keyCode === DOWN_ARROW) ||
(this.menu.placement === 'bottom' && event.keyCode === UP_ARROW)) {
this.closeMenu();
// refocus the parent menu item
this._menuItem.focus('keyboard');
}
}
}
/** Check whether the overlay has focus */
hasFocus() {
let check = false;
document.querySelectorAll('.cdk-overlay-container .ux-menu').forEach(el => {
if (el.contains(document.activeElement)) {
check = true;
}
});
return check;
}
/** Close the menu if there is no element focused */
closeOnFocusout() {
if (this.menu.isMenuOpen) {
setTimeout(() => {
if (!this.hasFocus()) {
this.closeMenu(undefined, true, false);
}
}, this._debounceTime);
}
}
getSubMenuPlacement(placement) {
return placement === 'left' ? 'left' : 'right';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuTriggerDirective, isStandalone: false, selector: "[uxMenuTriggerFor]", inputs: { menu: ["uxMenuTriggerFor", "menu"], disabled: "disabled", parent: ["uxMenuParent", "parent"], closeOnBlur: "closeOnBlur" }, outputs: { closed: "closed" }, host: { listeners: { "click": "toggleMenu($event)", "keydown.enter": "toggleMenu($event)", "keydown.space": "toggleMenu($event)", "mouseenter": "_onMouseEnter()", "mousemove": "_onMouseMove()", "document:keydown.escape": "_onEscape()", "keydown": "_onKeydown($event)", "blur": "_onBlur()" }, properties: { "attr.disabled": "disabled ? true : null", "attr.aria-haspopup": "!!menu", "attr.aria-expanded": "menu?.isMenuOpen", "attr.aria-controls": "ariaControls" } }, queries: [{ propertyName: "menuTriggers", predicate: i0.forwardRef(() => MenuTriggerDirective) }], exportAs: ["ux-menu-trigger"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuTriggerDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuTriggerFor]',
exportAs: 'ux-menu-trigger',
host: {
'[attr.disabled]': 'disabled ? true : null',
'[attr.aria-haspopup]': '!!menu',
'[attr.aria-expanded]': 'menu?.isMenuOpen',
'[attr.aria-controls]': 'ariaControls',
},
standalone: false,
}]
}], propDecorators: { menu: [{
type: Input,
args: ['uxMenuTriggerFor']
}], disabled: [{
type: Input
}], parent: [{
type: Input,
args: ['uxMenuParent']
}], closeOnBlur: [{
type: Input
}], closed: [{
type: Output
}], menuTriggers: [{
type: ContentChildren,
args: [forwardRef(() => MenuTriggerDirective)]
}], toggleMenu: [{
type: HostListener,
args: ['click', ['$event']]
}, {
type: HostListener,
args: ['keydown.enter', ['$event']]
}, {
type: HostListener,
args: ['keydown.space', ['$event']]
}], _onMouseEnter: [{
type: HostListener,
args: ['mouseenter']
}], _onMouseMove: [{
type: HostListener,
args: ['mousemove']
}], _onEscape: [{
type: HostListener,
args: ['document:keydown.escape']
}], _onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}], _onBlur: [{
type: HostListener,
args: ['blur']
}] } });
class MenuModule {
static forRoot(options) {
return {
ngModule: MenuModule,
providers: [{ provide: MENU_OPTIONS_TOKEN, useValue: options }],
};
}
/** Support options at a child module level (implementation is the same as `forRoot`) */
static forChild(options) {
return MenuModule.forRoot(options);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: MenuModule, declarations: [MenuComponent,
MenuTriggerDirective,
MenuItemComponent,
MenuDividerComponent,
MenuTabbableItemDirective,
MenuInitialFocusDirective,
MenuItemCustomControlDirective], imports: [A11yModule, AccessibilityModule, CommonModule, OverlayModule], exports: [MenuComponent,
MenuTriggerDirective,
MenuItemComponent,
MenuDividerComponent,
MenuTabbableItemDirective,
MenuInitialFocusDirective,
MenuItemCustomControlDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuModule, imports: [A11yModule, AccessibilityModule, CommonModule, OverlayModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuModule, decorators: [{
type: NgModule,
args: [{
declarations: [
MenuComponent,
MenuTriggerDirective,
MenuItemComponent,
MenuDividerComponent,
MenuTabbableItemDirective,
MenuInitialFocusDirective,
MenuItemCustomControlDirective,
],
imports: [A11yModule, AccessibilityModule, CommonModule, OverlayModule],
exports: [
MenuComponent,
MenuTriggerDirective,
MenuItemComponent,
MenuDividerComponent,
MenuTabbableItemDirective,
MenuInitialFocusDirective,
MenuItemCustomControlDirective,
],
}]
}] });
const CHECKBOX_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckboxComponent),
multi: true,
};
let uniqueCheckboxId = 0;
class CheckboxComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Provide a default unique id value for the checkbox */
this._checkboxId = `ux-checkbox-${++uniqueCheckboxId}`;
/** Determines if the checkbox should be checked, unchecked or indeterminate. */
this.id = this._checkboxId;
/** Determines if the checkbox should be checked, unchecked or indeterminate. */
this.value = false;
/** Specifies the tabindex of the input. */
this.tabindex = 0;
/** If set to `true` the checkbox will not toggle state when clicked. */
this.clickable = true;
/** If set to `true` the checkbox will be displayed without a border and background. */
this.simplified = false;
/**
* If `value` is set to the indeterminate value specified using this attribute, it will neither
* display the checkbox as checked or unchecked, and will instead show the indeterminate variation.
*/
this.indeterminateValue = -1;
/** Specify if the checkbox should be disabled. */
this.disabled = false;
/** Provide an aria label for the checkbox. */
this.ariaLabel = '';
/** Provide an aria-labelled by property for the checkbox. */
this.ariaLabelledby = null;
/** Emits when `value` has been changed. */
this.valueChange = new EventEmitter();
/** Determine if the underlying input component has been focused with the keyboard */
this._focused = false;
/** Used to inform Angular forms that the component has been touched */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
/** Used to inform Angular forms that the component value has changed */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
}
/** Toggle the current state of the checkbox */
toggle() {
if (this.disabled || !this.clickable) {
return;
}
if (this.value === this.indeterminateValue) {
this.value = true;
}
else {
// toggle the checked state
this.value = !this.value;
}
// emit the value
this.valueChange.emit(this.value);
// update the value if used within a form control
this.onChangeCallback(this.value);
// mark the component as touched
this.onTouchedCallback();
}
// Functions required to update ngModel
writeValue(value) {
if (value !== this.value) {
this.value = value;
this._changeDetector.markForCheck();
}
}
/** Allow Angular forms for provide us with a callback for when the input value changes */
registerOnChange(fn) {
this.onChangeCallback = fn;
}
/** Allow Angular forms for provide us with a callback for when the touched state changes */
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
/** Allow Angular forms to disable the component */
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
/** Focus the input element */
focus(origin) {
this._focusIndicator.focus(origin);
}
setInputTabIndex(tabindex) {
this.tabindex = tabindex;
this._changeDetector.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: CheckboxComponent, isStandalone: false, selector: "ux-checkbox", inputs: { id: "id", name: "name", value: "value", required: "required", tabindex: "tabindex", clickable: "clickable", simplified: "simplified", indeterminateValue: "indeterminateValue", disabled: "disabled", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"] }, outputs: { valueChange: "valueChange" }, providers: [
CHECKBOX_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: CheckboxComponent,
},
], viewQueries: [{ propertyName: "_focusIndicator", first: true, predicate: FocusIndicatorDirective, descendants: true }], ngImport: i0, template: "<label\n [attr.for]=\"(id || _checkboxId) + '-input'\"\n class=\"ux-checkbox\"\n [class.ux-checkbox-checked]=\"value === true\"\n [class.ux-checkbox-indeterminate]=\"value === indeterminateValue\"\n [class.ux-checkbox-simplified]=\"simplified\"\n [class.ux-checkbox-disabled]=\"disabled\"\n [class.ux-checkbox-focused]=\"_focused\"\n>\n <div class=\"ux-checkbox-container\">\n <input\n #input\n type=\"checkbox\"\n uxFocusIndicator\n class=\"ux-checkbox-input\"\n [id]=\"(id || _checkboxId) + '-input'\"\n [required]=\"required\"\n [checked]=\"value\"\n [attr.value]=\"value\"\n [disabled]=\"disabled\"\n [attr.name]=\"name\"\n [tabindex]=\"tabindex\"\n [indeterminate]=\"value === indeterminateValue\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-checked]=\"value === indeterminateValue ? 'mixed' : value\"\n (indicator)=\"_focused = $event\"\n (change)=\"$event.stopPropagation()\"\n (click)=\"toggle()\"\n />\n </div>\n\n <span class=\"ux-checkbox-label\">\n <ng-content></ng-content>\n </span>\n</label>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CheckboxComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-checkbox', providers: [
CHECKBOX_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: CheckboxComponent,
},
], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<label\n [attr.for]=\"(id || _checkboxId) + '-input'\"\n class=\"ux-checkbox\"\n [class.ux-checkbox-checked]=\"value === true\"\n [class.ux-checkbox-indeterminate]=\"value === indeterminateValue\"\n [class.ux-checkbox-simplified]=\"simplified\"\n [class.ux-checkbox-disabled]=\"disabled\"\n [class.ux-checkbox-focused]=\"_focused\"\n>\n <div class=\"ux-checkbox-container\">\n <input\n #input\n type=\"checkbox\"\n uxFocusIndicator\n class=\"ux-checkbox-input\"\n [id]=\"(id || _checkboxId) + '-input'\"\n [required]=\"required\"\n [checked]=\"value\"\n [attr.value]=\"value\"\n [disabled]=\"disabled\"\n [attr.name]=\"name\"\n [tabindex]=\"tabindex\"\n [indeterminate]=\"value === indeterminateValue\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-checked]=\"value === indeterminateValue ? 'mixed' : value\"\n (indicator)=\"_focused = $event\"\n (change)=\"$event.stopPropagation()\"\n (click)=\"toggle()\"\n />\n </div>\n\n <span class=\"ux-checkbox-label\">\n <ng-content></ng-content>\n </span>\n</label>\n" }]
}], propDecorators: { id: [{
type: Input
}], name: [{
type: Input
}], value: [{
type: Input
}], required: [{
type: Input
}], tabindex: [{
type: Input
}], clickable: [{
type: Input
}], simplified: [{
type: Input
}], indeterminateValue: [{
type: Input
}], disabled: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], valueChange: [{
type: Output
}], _focusIndicator: [{
type: ViewChild,
args: [FocusIndicatorDirective]
}] } });
class CheckboxModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CheckboxModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: CheckboxModule, declarations: [CheckboxComponent], imports: [AccessibilityModule, FormsModule], exports: [CheckboxComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CheckboxModule, imports: [AccessibilityModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CheckboxModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, FormsModule],
exports: [CheckboxComponent],
declarations: [CheckboxComponent],
}]
}] });
/**
* Type representing a color, including its descriptive name.
*/
class ColorPickerColor {
/**
* Hex value of the color, e.g. `#ffffff`.
*/
get hex() {
return this._originalHexValue ? this._originalHexValue : this._color.toHex();
}
/**
* RGBA value of the color, e.g. `rgba(255, 255, 255, 1)`.
*/
get rgba() {
return this._originalRgbaValue ? this._originalRgbaValue : this._color.toRgba();
}
get r() {
return parseInt(this._color.getRed());
}
get g() {
return parseInt(this._color.getGreen());
}
get b() {
return parseInt(this._color.getBlue());
}
get a() {
return parseFloat(this._color.getAlpha());
}
constructor(name, value, inputMode) {
this.name = name;
this._color = ThemeColor.parse(value);
// Preserve the format entered by the user if it's valid
if (inputMode === 'hex') {
this._originalHexValue = value;
}
else if (inputMode === 'rgba') {
this._originalRgbaValue = value;
}
}
toString() {
return this._color.toRgba();
}
}
let uniqueTooltipId = 0;
class TooltipComponent {
constructor() {
this._changeDetectorRef = inject(ChangeDetectorRef);
/** Define a unique id for each tooltip */
this.id = `ux-tooltip-${++uniqueTooltipId}`;
/** Define the tooltip role */
this.role = 'tooltip';
/** Allow a custom class to be added to the tooltip to allow custom styling */
this.customClass = '';
/** Emit when the tooltip need to update it's position */
this.reposition$ = new Subject();
/** The name of the css class to use for the tooltip direction */
this._positionClass = '';
}
/** Indicates whether or not the content is a string or a TemplateRef */
get isTemplateRef() {
return this.content instanceof TemplateRef;
}
get positionClass() {
return this._positionClass;
}
set positionClass(positionClass) {
this._positionClass = positionClass;
this._changeDetectorRef.detectChanges();
}
/** Cleanup after the component is destroyed */
ngOnDestroy() {
this.reposition$.complete();
}
/** Inform the parent directive that it needs to recalulate the position */
reposition() {
this.reposition$.next();
}
/** This will update the content of the tooltip and trigger change detection */
setContent(content) {
this.content = content;
this._changeDetectorRef.markForCheck();
}
/** This will update the tooltip placement and trigger change detection */
setPlacement(placement) {
if (!placement) {
return;
}
this.placement = placement;
this._changeDetectorRef.markForCheck();
}
/** This will update the tooltip alignment and trigger change detection */
setAlignment(alignment) {
if (!alignment) {
return;
}
this.alignment = alignment;
this._changeDetectorRef.markForCheck();
}
/** This will set a custom class on the tooltip and trigger change detection */
setClass(customClass) {
if (!customClass) {
return;
}
this.customClass = customClass;
this._changeDetectorRef.markForCheck();
}
/** Updates the context used by the TemplateRef */
setContext(context) {
if (!context) {
return;
}
this.context = context;
this._changeDetectorRef.markForCheck();
}
/** Specify the tooltip role attribute */
setRole(role) {
if (!role) {
return;
}
this.role = role;
this._changeDetectorRef.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TooltipComponent, isStandalone: false, selector: "ux-tooltip", inputs: { content: "content", context: "context", placement: "placement", alignment: "alignment" }, ngImport: i0, template: "<div class=\"tooltip in\" [id]=\"id\" [attr.role]=\"role\" [ngClass]=\"[positionClass, customClass]\">\n <div class=\"tooltip-arrow\"></div>\n <div class=\"tooltip-inner\" (cdkObserveContent)=\"reposition()\">\n @if (!isTemplateRef) {\n {{ content }}\n }\n @if (isTemplateRef) {\n <ng-container\n [ngTemplateOutlet]=\"$any(content)\"\n [ngTemplateOutletContext]=\"context\"\n ></ng-container>\n }\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$1.CdkObserveContent, selector: "[cdkObserveContent]", inputs: ["cdkObserveContentDisabled", "debounce"], outputs: ["cdkObserveContent"], exportAs: ["cdkObserveContent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-tooltip', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"tooltip in\" [id]=\"id\" [attr.role]=\"role\" [ngClass]=\"[positionClass, customClass]\">\n <div class=\"tooltip-arrow\"></div>\n <div class=\"tooltip-inner\" (cdkObserveContent)=\"reposition()\">\n @if (!isTemplateRef) {\n {{ content }}\n }\n @if (isTemplateRef) {\n <ng-container\n [ngTemplateOutlet]=\"$any(content)\"\n [ngTemplateOutletContext]=\"context\"\n ></ng-container>\n }\n </div>\n</div>\n" }]
}], propDecorators: { content: [{
type: Input
}], context: [{
type: Input
}], placement: [{
type: Input
}], alignment: [{
type: Input
}] } });
class TooltipService {
constructor() {
this.shown$ = new Subject();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipService, decorators: [{
type: Injectable
}] });
class TooltipDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._viewContainerRef = inject(ViewContainerRef);
this._overlay = inject(Overlay);
this._scrollDispatcher = inject(ScrollDispatcher);
this._changeDetectorRef = inject(ChangeDetectorRef);
this._renderer = inject(Renderer2);
this._tooltipService = inject(TooltipService);
this._overlayFallback = inject(OverlayPlacementService);
/** All the user to add a custom class to the tooltip */
this.customClass = '';
/** All the user to add a role to the tooltip - default is tooltip */
this.role = 'tooltip';
/** Provide the TemplateRef a context object */
this.context = {};
/** Delay the showing of the tooltip by a number of miliseconds */
this.delay = 0;
/** Programmatically show and hide the tooltip */
this.isOpen = false;
/** Customize how the tooltip should be positioned relative to the element */
this.placement = 'top';
/** Customize the position of the callout */
this.alignment = 'center';
/** Specify which events should show the tooltip */
this.showTriggers = ['mouseenter', 'focus'];
/** Specify which events should hide the tooltip */
this.hideTriggers = ['mouseleave', 'blur'];
/** Emits an event when the tooltip is shown */
this.shown = new EventEmitter();
/** Emits a event when the tooltip is hidden */
this.hidden = new EventEmitter();
/** Allow two way binding to track the visibility of the tooltip */
this.isOpenChange = new EventEmitter();
/** Keep track of the tooltip visibility */
this.isVisible = false;
/** The name of the css class to use for the tooltip direction */
this.positionClass = this.placement;
/** Define the overlay class */
this._overlayClass = 'ux-tooltip-pane';
/** This will emit when the directive is destroyed allowing us to unsubscribe all subscriptions automatically */
this._onDestroy = new Subject();
/** Store the timeout interval for cancelation */
this._showTimeoutId = null;
/** Internally store the type of this component - usual for distinctions when extending this class */
this._type = 'tooltip';
}
/** Set up the triggers and bind to the show/hide events to keep visibility in sync */
ngOnInit() {
// set up show and hide event triggers
fromEvent(this._elementRef.nativeElement, 'click')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onClick.bind(this));
fromEvent(this._elementRef.nativeElement, 'mouseenter')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onMouseEnter.bind(this));
fromEvent(this._elementRef.nativeElement, 'mouseleave')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onMouseLeave.bind(this));
fromEvent(this._elementRef.nativeElement, 'focus')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onFocus.bind(this));
fromEvent(this._elementRef.nativeElement, 'blur')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onBlur.bind(this));
// when any other tooltips open hide this one
this._tooltipService.shown$
.pipe(filter(() => this._type === 'tooltip'), filter(tooltip => tooltip !== this._instance), takeUntil(this._onDestroy))
.subscribe(this.hide.bind(this));
// if the tooltip should be initially visible then open it
if (this.isOpen) {
this.show();
}
}
/**
* We need to send input changes to the tooltip component
* We can't use setters as they may trigger before tooltip initialised and can't resend once initialised
**/
ngOnChanges(changes) {
// we can ignore the first change as it's handled in ngOnInit
if (changes.isOpen &&
!changes.isOpen.firstChange &&
changes.isOpen.currentValue !== this.isVisible) {
changes.isOpen.currentValue ? this.show() : this.hide();
}
// destroy the overlay ref so a new correctly positioned instance will be created next time
if (changes.placement) {
this.destroyOverlay();
}
if (this._instance && changes.placement) {
this._instance.setPlacement(changes.placement.currentValue);
}
if (this._instance && changes.alignment) {
this._instance.setAlignment(changes.alignment.currentValue);
}
if (this._instance && changes.content) {
this._instance.setContent(changes.content.currentValue);
}
if (this._instance && changes.customClass) {
this._instance.setClass(changes.customClass.currentValue);
}
if (this._instance && changes.context) {
this._instance.setContext(changes.context.currentValue);
}
if (this._instance && changes.role) {
this._instance.setContext(changes.role.currentValue);
}
}
/** Ensure we clean up after ourselves */
ngOnDestroy() {
// ensure we close the tooltip when the host is destroyed
if (this._overlayRef) {
this._overlayRef.dispose();
this._instance = null;
}
// clear any pending timeouts
this.cancelTooltip();
// emit this event to automatically unsubscribe from all subscriptions
this._onDestroy.next();
this._onDestroy.complete();
}
/** Make the tooltip open */
show() {
// if the tooltip is disabled then do nothing
if (this.disabled || this.isVisible || this._showTimeoutId || !this.content) {
return;
}
// delay the show by the delay amount
this._showTimeoutId = window.setTimeout(() => {
// create the tooltip and get the overlay ref
const overlayRef = this.createOverlay();
// create the portal to create the tooltip component
this._portal = this.createPortal();
this._instance = this.createInstance(overlayRef);
// watch for any changes to the content
this._instance.reposition$
.pipe(takeUntil(this._onDestroy))
.subscribe(this.reposition.bind(this));
// store the visible state
this.isVisible = true;
// ensure the overlay has the correct initial position
this.reposition();
// emit the show events
this.shown.emit();
this.isOpenChange.next(true);
// clear the interval id
this._showTimeoutId = null;
// emit the show event to close any other tooltips
this._tooltipService.shown$.next(this._instance);
// ensure change detection is run
this._changeDetectorRef.detectChanges();
}, this.delay);
}
/** If a tooltip exists and is visible, hide it */
hide() {
// if we are waiting to show a tooltip then cancel the pending timeout
if (this._showTimeoutId) {
window.clearTimeout(this._showTimeoutId);
this._showTimeoutId = null;
return;
}
// if the tooltip is hidden then do nothing
if (!this.isVisible) {
return;
}
if (this._overlayRef && this._overlayRef.hasAttached()) {
this._overlayRef.detach();
}
this.setAriaDescribedBy(null);
this._instance = null;
// store the visible state
this.isVisible = false;
// emit the hide events
this.hidden.emit();
this.isOpenChange.next(false);
// ensure change detection is run
this._changeDetectorRef.detectChanges();
}
/** Toggle the visibility of the tooltip */
toggle() {
this.isVisible ? this.hide() : this.show();
}
/** Recalculate the position of the popover */
reposition() {
if (this.isVisible && this._overlayRef) {
this._overlayRef.updatePosition();
}
}
/** Create an instance from the overlay ref - allows overriding and additional logic here */
createInstance(overlayRef) {
const instance = overlayRef.attach(this._portal).instance;
// supply the tooltip with the correct properties
instance.setContent(this.content);
instance.setPlacement(this.placement);
instance.setAlignment(this.alignment);
instance.setClass(this.customClass);
instance.setContext(this.context);
instance.setRole(this.role);
// Update the aria-describedby attribute
this.setAriaDescribedBy(instance.id);
return instance;
}
/** Create the component portal - allows overriding to allow other portals eg. popovers */
createPortal() {
return this._portal || new ComponentPortal(TooltipComponent, this._viewContainerRef);
}
/** Create the overlay and set up the scroll handling behavior */
createOverlay() {
// if the tooltip has already been created then just return the existing instance
if (this._overlayRef) {
return this._overlayRef;
}
const strategy = this._overlay
.position()
.flexibleConnectedTo(this._elementRef)
.withFlexibleDimensions(false)
.withPush(false);
this._overlayRef = this._overlay.create({
positionStrategy: strategy,
panelClass: this._overlayClass,
scrollStrategy: this._overlay.scrollStrategies.reposition({ scrollThrottle: 0 }),
hasBackdrop: false,
});
this._overlayFallback.updatePosition(this._overlayRef, this.placement, this.alignment, this.fallbackPlacement);
strategy.positionChanges.subscribe(positionChange => {
const currentPosition = positionChange.connectionPair;
this.getPositionClass(currentPosition);
});
return this._overlayRef;
}
/** Recreate the overlay ref using the updated origin and overlay positions */
destroyOverlay() {
// destroy the existing overlay
if (this._overlayRef && this._overlayRef.hasAttached()) {
this._overlayRef.detach();
}
if (this._overlayRef) {
this._overlayRef.dispose();
this._overlayRef = null;
}
this.isVisible = false;
}
getPositionClass(currentPosition) {
let positionClass = this.placement;
if (currentPosition.originX === 'center') {
positionClass = currentPosition.originY === 'top' ? 'top' : 'bottom';
}
else if (currentPosition.originY === 'center') {
positionClass = currentPosition.originX === 'start' ? 'left' : 'right';
}
this._instance.positionClass = positionClass;
this._changeDetectorRef.detectChanges();
}
/**
* Simple utility method - because IE doesn't support array.includes
* And it isn't included in the core-js/es6 polyfills which are the
* only ones required by Angular and guaranteed to be there
**/
includes(array, value) {
return Array.isArray(array) && !!array.find(item => item === value);
}
/** Handle the click event - show or hide accordingly */
onClick() {
// if its not visible and click is a show trigger open it
if (!this.isVisible &&
this.includes(this.showTriggers, 'click') &&
this._showTimeoutId === null) {
return this.show();
}
// if its visible and click is a hide trigger close it
if (this.isVisible &&
this.includes(this.hideTriggers, 'click') &&
this._showTimeoutId === null) {
return this.hide();
}
// if its not visible and click is a hide trigger close it and there is a pending tooltip
if (!this.isVisible &&
this.includes(this.hideTriggers, 'click') &&
this._showTimeoutId !== null) {
return this.cancelTooltip();
}
}
/** Handle the mouse enter event - show or hide accordingly */
onMouseEnter() {
// this is an show only trigger - if already open or it isn't a trigger do nothing
if (this.isVisible || !this.includes(this.showTriggers, 'mouseenter')) {
return;
}
// otherwise open the tooltip
this.show();
}
/** Handle the mouse leave event - show or hide accordingly */
onMouseLeave() {
// If the tooltip is pending then cancel showing it
if (!this.isVisible &&
this.includes(this.hideTriggers, 'mouseleave') &&
this._showTimeoutId !== null) {
return this.cancelTooltip();
}
// if the tooltip is not visible or mouseleave isn't a hide trigger then do nothing
if (!this.isVisible || !this.includes(this.hideTriggers, 'mouseleave')) {
return;
}
// otherwise close the tooltip
this.hide();
}
/** Handle the focus event - show or hide accordingly */
onFocus() {
// this is an show only trigger - if already open or it isn't a trigger do nothing
if (this.isVisible || !this.includes(this.showTriggers, 'focus')) {
return;
}
// otherwise open the tooltip
this.show();
}
/** Handle the blur event - show or hide accordingly */
onBlur() {
// If the tooltip is pending then cancel showing it
if (!this.isVisible &&
this.includes(this.hideTriggers, 'blur') &&
this._showTimeoutId !== null) {
return this.cancelTooltip();
}
// this is an hide only trigger - if not open or it isn't a trigger do nothing
if (!this.isVisible || !this.includes(this.hideTriggers, 'blur')) {
return;
}
// otherwise close the tooltip
this.hide();
}
/** Programmatically update the aria-describedby property */
setAriaDescribedBy(id) {
if (id === null) {
this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby');
}
else {
this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', id);
}
}
/** Cancel any pending tooltip (waiting on delay ellapsing) */
cancelTooltip() {
if (this._showTimeoutId !== null) {
window.clearTimeout(this._showTimeoutId);
this._showTimeoutId = null;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TooltipDirective, isStandalone: false, selector: "[uxTooltip]", inputs: { content: ["uxTooltip", "content"], disabled: ["tooltipDisabled", "disabled"], customClass: ["tooltipClass", "customClass"], role: ["tooltipRole", "role"], context: ["tooltipContext", "context"], delay: ["tooltipDelay", "delay"], isOpen: "isOpen", placement: "placement", fallbackPlacement: "fallbackPlacement", alignment: "alignment", showTriggers: "showTriggers", hideTriggers: "hideTriggers" }, outputs: { shown: "shown", hidden: "hidden", isOpenChange: "isOpenChange" }, exportAs: ["ux-tooltip"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTooltip]',
exportAs: 'ux-tooltip',
standalone: false,
}]
}], propDecorators: { content: [{
type: Input,
args: ['uxTooltip']
}], disabled: [{
type: Input,
args: ['tooltipDisabled']
}], customClass: [{
type: Input,
args: ['tooltipClass']
}], role: [{
type: Input,
args: ['tooltipRole']
}], context: [{
type: Input,
args: ['tooltipContext']
}], delay: [{
type: Input,
args: ['tooltipDelay']
}], isOpen: [{
type: Input
}], placement: [{
type: Input
}], fallbackPlacement: [{
type: Input
}], alignment: [{
type: Input
}], showTriggers: [{
type: Input
}], hideTriggers: [{
type: Input
}], shown: [{
type: Output
}], hidden: [{
type: Output
}], isOpenChange: [{
type: Output
}] } });
// Values corresponding to stylesheet
const BUTTON_MARGIN = 8;
const BUTTON_WIDTHS = {
sm: 26,
md: 32,
lg: 40,
};
let uniqueId$c = 0;
class ColorPickerComponent {
constructor() {
this.id = `ux-color-picker-${uniqueId$c++}`;
/** The style of the color swatch buttons. */
this.buttonStyle = 'circle';
/** Whether to show tooltips above the color swatch buttons. These contain the color name if provided; otherwise the color hex/RGBA value. */
this.showTooltips = false;
/** Whether to show the hex/RGBA input panel. */
this.showInput = false;
/** The default input mode to display in the input panel. The user can switch modes using the toggle button. */
this.inputMode = 'hex';
/** Defines a function that returns an aria-label for ColorPickerColor. */
this.colorAriaLabel = this.getColorAriaLabel;
/** Defines a function that returns an aria-label for the button that switches input modes. */
this.switchModeAriaLabel = this.getSwitchModeAriaLabel;
/** Define a function that returns an aria-label for the input control. */
this.inputAriaLabel = this.getInputAriaLabel;
/** Emitted when the user changes the selected color, either by clicking a color swatch button, or entering a valid color value into the input panel text field. */
this.selectedChange = new EventEmitter();
/** Emitted when the user changes the colour input mode */
this.inputModeChange = new EventEmitter();
/** Emitted when the user presses enter in the input panel text field. This can be used to commit a color change and/or close a popup. */
this.inputSubmit = new EventEmitter();
this.cssWidth = 'auto';
this.colors = [];
this.selected$ = new BehaviorSubject(null);
this.columns$ = new BehaviorSubject(-1);
this.buttonSize$ = new BehaviorSubject('md');
this.inputPatterns = {
hex: /^#(?:[\da-fA-F]{3}){1,2}$/,
rgba: /^(?:rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\))|(?:rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*\d(\.\d+)?\))$/,
};
this._onDestroy = new Subject();
}
/**
* The collection of colors to display in the color swatch.
*
* Colors can be specified either as a string, which is the hex or RGBA value of the color; or as a `ColorPickerColor` object,
* which allows a name to be associated with the color. See below for details of the `ColorPickerColor` class.
* This property is either a one-dimensional or two-dimensional array. If a two-dimensional array is provided,
* the colors will be split into rows, providing more control over the appearance of the swatch.
*/
set inputColors(colors) {
let normalizedColors;
// If it's a 1d array, convert it to 2d
if (colors.length === 0 || !Array.isArray(colors[0])) {
normalizedColors = [colors];
}
else {
normalizedColors = colors;
}
// Convert any string colors to ColorPickerColor
this.colors = normalizedColors.map(row => {
return row.map(color => color instanceof ColorPickerColor ? color : new ColorPickerColor(color, color));
});
}
/**
* The currently selected color. If this is one of the `colors` in the colors collection, it will be visually
* highlighted in the swatch. It will also be shown in the input panel, if enabled (see showInput).
* Note that this will always be a `ColorPickerColor` object, even if plain strings are provided to the colors property.
* See below for details of the `ColorPickerColor` class.
*/
set selected(selected) {
this.selected$.next(selected);
}
/**
* The number of columns to display in the color swatch. Set this to -1 if the width should be specified by a stylesheet
* instead, e.g. to provide a responsive layout.
*/
set columns(columns) {
this.columns$.next(columns);
}
/** The size of the color swatch buttons. Three size variants are currently supported. */
set buttonSize(buttonSize) {
this.buttonSize$.next(buttonSize);
}
ngOnInit() {
// Skip emitting the initial selectedChange
this.selected$.pipe(pairwise(), takeUntil(this._onDestroy)).subscribe(([prev, curr]) => {
if (prev) {
this.selectedChange.emit(curr);
}
});
// Set the width based on column count and button size
combineLatest([this.columns$, this.buttonSize$])
.pipe(takeUntil(this._onDestroy))
.subscribe(([columns, buttonSize]) => {
if (columns > 0) {
const w = columns * (BUTTON_WIDTHS[buttonSize] + 2 * BUTTON_MARGIN);
this.cssWidth = `${w}px`;
}
else {
this.cssWidth = 'auto';
}
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onFocus() {
// Forward focus to the color grid
this.tabbableList.focusTabbableItem();
}
updateColorValue(input, mode) {
if (this.inputPatterns[mode].test(input)) {
this.selected$.next(new ColorPickerColor('Custom', input, mode));
}
}
toggleColorEntryType() {
// update the input mode
this.inputMode = this.inputMode === 'hex' ? 'rgba' : 'hex';
// emit the new input mode
this.inputModeChange.emit(this.inputMode);
// get the current color value if there is one
const color = this.selected$.value;
// if there is no selected color property then skip
if (color) {
// get the new color value
const newColor = this.inputMode === 'hex' ? color.hex : color.rgba;
// update the selected color value based on the input mode
this.updateColorValue(newColor, this.inputMode);
// forcibly update the validation status of ngModel to prevent any
// incorrect error states in the underlying form control
// (running change detection will not suffice)
this.inputFormControl.control.setValue(newColor);
}
}
getColorAriaLabel(color) {
return `Select color ${color.name}`;
}
getSwitchModeAriaLabel(mode) {
return `Switch input mode to ${mode === 'hex' ? 'RGBA' : 'hex'}`;
}
getInputAriaLabel(mode) {
return `Edit ${mode} color value`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ColorPickerComponent, isStandalone: false, selector: "ux-color-picker", inputs: { id: "id", inputColors: ["colors", "inputColors"], selected: "selected", columns: "columns", buttonStyle: "buttonStyle", buttonSize: "buttonSize", showTooltips: "showTooltips", showInput: "showInput", inputMode: "inputMode", colorAriaLabel: "colorAriaLabel", switchModeAriaLabel: "switchModeAriaLabel", inputAriaLabel: "inputAriaLabel" }, outputs: { selectedChange: "selectedChange", inputModeChange: "inputModeChange", inputSubmit: "inputSubmit" }, host: { attributes: { "tabindex": "0" }, listeners: { "focus": "onFocus($event)" }, properties: { "attr.id": "this.id", "style.width": "this.cssWidth" } }, viewQueries: [{ propertyName: "inputFormControl", first: true, predicate: ["inputField"], descendants: true }, { propertyName: "tabbableList", first: true, predicate: TabbableListDirective, descendants: true }], exportAs: ["ux-color-picker"], ngImport: i0, template: "<div class=\"ux-color-picker\" uxTabbableList direction=\"horizontal\">\n <div class=\"ux-color-picker-swatch\">\n @for (row of colors; track row) {\n <div class=\"ux-color-picker-swatch-row\">\n @for (color of row; track color) {\n <div\n #colorPickerColor\n class=\"ux-color-picker-color\"\n [class.ux-small]=\"(buttonSize$ | async) === 'sm'\"\n [class.ux-large]=\"(buttonSize$ | async) === 'lg'\"\n [class.ux-circle]=\"buttonStyle === 'circle'\"\n [class.ux-selected]=\"color === (selected$ | async)\"\n >\n <button\n type=\"button\"\n uxFocusIndicator\n [attr.aria-label]=\"colorAriaLabel(color)\"\n [attr.aria-selected]=\"color === (selected$ | async)\"\n class=\"btn btn-icon\"\n [uxColorContrast]=\"color.hex\"\n [style.background-color]=\"color.rgba\"\n (click)=\"selected$.next(color)\"\n uxTabbableListItem\n [uxTooltip]=\"color.name\"\n [tooltipDisabled]=\"!showTooltips\"\n >\n <ux-icon name=\"checkmark\"></ux-icon>\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n @if (showInput) {\n <div class=\"ux-color-picker-input-panel\">\n <div class=\"ux-color-picker-input-header\">\n <div\n class=\"ux-color-picker-preview\"\n [style.background-color]=\"(selected$ | async).rgba\"\n [class.ux-circle]=\"buttonStyle === 'circle'\"\n ></div>\n @if (inputMode === 'hex') {\n <label attr.for=\"{{ id }}-input-field\">HEX</label>\n }\n @if (inputMode === 'rgba') {\n <label attr.for=\"{{ id }}-input-field\">RGBA</label>\n }\n <button\n type=\"button\"\n [attr.aria-label]=\"switchModeAriaLabel(inputMode)\"\n class=\"btn btn-link btn-icon button-secondary ux-color-picker-input-toggle\"\n (click)=\"toggleColorEntryType(); $event.stopPropagation()\"\n >\n <ux-icon name=\"chevron-right\"></ux-icon>\n </button>\n </div>\n <div\n class=\"ux-color-picker-input\"\n [class.has-error]=\"inputField.errors\"\n [class.has-feedback]=\"inputField.errors\"\n >\n <input\n type=\"text\"\n attr.id=\"{{ id }}-input-field\"\n [attr.aria-description]=\"inputAriaLabel(inputMode)\"\n class=\"form-control\"\n #inputField=\"ngModel\"\n [ngModel]=\"(selected$ | async)[inputMode]\"\n (ngModelChange)=\"updateColorValue($event, inputMode)\"\n [pattern]=\"inputPatterns[inputMode].source\"\n (keyup.enter)=\"inputSubmit.emit()\"\n />\n <ux-icon class=\"form-control-feedback\" name=\"alert\"></ux-icon>\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: ColorContrastDirective, selector: "[uxColorContrast]", inputs: ["uxColorContrast", "lightColor", "darkColor"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-color-picker', exportAs: 'ux-color-picker', host: {
tabindex: '0',
}, standalone: false, template: "<div class=\"ux-color-picker\" uxTabbableList direction=\"horizontal\">\n <div class=\"ux-color-picker-swatch\">\n @for (row of colors; track row) {\n <div class=\"ux-color-picker-swatch-row\">\n @for (color of row; track color) {\n <div\n #colorPickerColor\n class=\"ux-color-picker-color\"\n [class.ux-small]=\"(buttonSize$ | async) === 'sm'\"\n [class.ux-large]=\"(buttonSize$ | async) === 'lg'\"\n [class.ux-circle]=\"buttonStyle === 'circle'\"\n [class.ux-selected]=\"color === (selected$ | async)\"\n >\n <button\n type=\"button\"\n uxFocusIndicator\n [attr.aria-label]=\"colorAriaLabel(color)\"\n [attr.aria-selected]=\"color === (selected$ | async)\"\n class=\"btn btn-icon\"\n [uxColorContrast]=\"color.hex\"\n [style.background-color]=\"color.rgba\"\n (click)=\"selected$.next(color)\"\n uxTabbableListItem\n [uxTooltip]=\"color.name\"\n [tooltipDisabled]=\"!showTooltips\"\n >\n <ux-icon name=\"checkmark\"></ux-icon>\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n @if (showInput) {\n <div class=\"ux-color-picker-input-panel\">\n <div class=\"ux-color-picker-input-header\">\n <div\n class=\"ux-color-picker-preview\"\n [style.background-color]=\"(selected$ | async).rgba\"\n [class.ux-circle]=\"buttonStyle === 'circle'\"\n ></div>\n @if (inputMode === 'hex') {\n <label attr.for=\"{{ id }}-input-field\">HEX</label>\n }\n @if (inputMode === 'rgba') {\n <label attr.for=\"{{ id }}-input-field\">RGBA</label>\n }\n <button\n type=\"button\"\n [attr.aria-label]=\"switchModeAriaLabel(inputMode)\"\n class=\"btn btn-link btn-icon button-secondary ux-color-picker-input-toggle\"\n (click)=\"toggleColorEntryType(); $event.stopPropagation()\"\n >\n <ux-icon name=\"chevron-right\"></ux-icon>\n </button>\n </div>\n <div\n class=\"ux-color-picker-input\"\n [class.has-error]=\"inputField.errors\"\n [class.has-feedback]=\"inputField.errors\"\n >\n <input\n type=\"text\"\n attr.id=\"{{ id }}-input-field\"\n [attr.aria-description]=\"inputAriaLabel(inputMode)\"\n class=\"form-control\"\n #inputField=\"ngModel\"\n [ngModel]=\"(selected$ | async)[inputMode]\"\n (ngModelChange)=\"updateColorValue($event, inputMode)\"\n [pattern]=\"inputPatterns[inputMode].source\"\n (keyup.enter)=\"inputSubmit.emit()\"\n />\n <ux-icon class=\"form-control-feedback\" name=\"alert\"></ux-icon>\n </div>\n </div>\n }\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}, {
type: HostBinding,
args: ['attr.id']
}], inputColors: [{
type: Input,
args: ['colors']
}], selected: [{
type: Input
}], columns: [{
type: Input
}], buttonStyle: [{
type: Input
}], buttonSize: [{
type: Input
}], showTooltips: [{
type: Input
}], showInput: [{
type: Input
}], inputMode: [{
type: Input
}], colorAriaLabel: [{
type: Input
}], switchModeAriaLabel: [{
type: Input
}], inputAriaLabel: [{
type: Input
}], selectedChange: [{
type: Output
}], inputModeChange: [{
type: Output
}], inputSubmit: [{
type: Output
}], cssWidth: [{
type: HostBinding,
args: ['style.width']
}], inputFormControl: [{
type: ViewChild,
args: ['inputField', { static: false }]
}], tabbableList: [{
type: ViewChild,
args: [TabbableListDirective]
}], onFocus: [{
type: HostListener,
args: ['focus', ['$event']]
}] } });
let uniqueId$b = 0;
const NUMBER_PICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberPickerComponent),
multi: true,
};
class NumberPickerComponent {
constructor() {
this._formGroup = inject(FormGroupDirective, { optional: true });
this._changeDetector = inject(ChangeDetectorRef);
this._step = 1;
this._disabled = false;
this._value = 0;
this._focused = false;
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._propagateChange = (_) => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._touchedChange = () => { };
/** Sets the id of the number picker. The child input will have this value with a -input suffix as its id. */
this.id = `ux-number-picker-${uniqueId$b++}`;
/** Define the precision of floating point values */
this.precision = Number.MAX_SAFE_INTEGER.toString().length - 1;
/** If two way binding is used this value will be updated any time the number picker value changes. */
this.valueChange = new EventEmitter();
/** Store the current valid state */
this._valid = true;
/** This is a flag to indicate when the component has been destroyed to avoid change detection being made after the component
* is no longer instantiated. A workaround for Angular Forms bug (https://github.com/angular/angular/issues/27803) */
this._isDestroyed = false;
this._readonly = false;
}
/** Specified if this is a readonly input. */
set readonly(value) {
this._readonly = coerceBooleanProperty(value);
}
get readonly() {
return this._readonly;
}
/** Sets the value displayed in the number picker component. */
get value() {
return this._value;
}
set value(value) {
if (this._value !== value) {
this._value = value;
this._valid = this.isValid();
}
}
/** Defines the minimum value the number picker can set. */
get min() {
return this._min;
}
set min(value) {
this._min = coerceNumberProperty(value);
}
/** Defines the maximum value the number picker can set. */
get max() {
return this._max;
}
set max(value) {
this._max = coerceNumberProperty(value);
}
/** Defines the amount the number picker should increase or decrease when the buttons or arrow keys are used. */
set step(value) {
if (typeof value === 'function') {
this._step = value;
}
else {
this._step = coerceNumberProperty(value);
}
}
/** Indicate if the number picker is disabled or not. */
get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
}
get inputId() {
return this.id + '-input';
}
ngOnChanges() {
this._valid = this.isValid();
}
ngOnDestroy() {
this._isDestroyed = true;
}
getStep(direction) {
return typeof this._step === 'number' ? this._step : this._step(this.value, direction);
}
increment(event) {
if (event) {
event.preventDefault();
}
if (!this.disabled && !this.readonly) {
this.value = Math.max(Math.min(this.value + this.getStep(StepDirection.Increment), this.getMaxValueForComparison()), this.getMinValueForComparison());
// account for javascripts terrible handling of floating point numbers
this.value = parseFloat(this.value.toPrecision(this.precision));
// emit the value to the Output and Angular forms
this._emitValueChange(this.value);
}
}
decrement(event) {
if (event) {
event.preventDefault();
}
if (!this.disabled && !this.readonly) {
this.value = Math.min(Math.max(this.value - this.getStep(StepDirection.Decrement), this.getMinValueForComparison()), this.getMaxValueForComparison());
// account for javascripts terrible handling of floating point numbers
this.value = parseFloat(this.value.toPrecision(this.precision));
// emit the value to the Output and Angular forms
this._emitValueChange(this.value);
}
}
isValid() {
return (this.value >= this.getMinValueForComparison() && this.value <= this.getMaxValueForComparison());
}
onScroll(event) {
if (!this._focused) {
return;
}
// get the distance scrolled
const scrollValue = event.deltaY || event.wheelDelta;
// increment or decrement accordingly
scrollValue < 0 ? this.increment(event) : this.decrement(event);
}
writeValue(value) {
if (value !== undefined) {
this._value = value;
this._valid = this.isValid();
// if the component is not destroyed then run change detection
// workaround for Angular bug (https://portal.digitalsafe.net/browse/EL-3694)
if (!this._isDestroyed) {
this._changeDetector.detectChanges();
}
}
}
registerOnChange(fn) {
this._propagateChange = fn;
}
registerOnTouched(fn) {
this._touchedChange = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
/** Set the value and emit the change to the output and Angular forms. */
_emitValueChange(value) {
// This is a workaround for angular bug https://github.com/angular/angular/issues/12540
if (value === this._lastValue) {
return;
}
this._lastValue = value;
this.valueChange.emit(value);
this._propagateChange(value);
}
getMaxValueForComparison() {
return this.max === undefined || this.max === null ? Infinity : this.max;
}
getMinValueForComparison() {
return this.min === undefined || this.min === null ? -Infinity : this.min;
}
onFocusIn() {
this._focused = true;
}
onFocusOut() {
this._focused = false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: NumberPickerComponent, isStandalone: false, selector: "ux-number-picker, ux-number-picker-inline", inputs: { id: "id", ariaLabel: ["aria-label", "ariaLabel"], labelledBy: ["aria-labelledby", "labelledBy"], precision: "precision", placeholder: "placeholder", required: "required", readonly: "readonly", value: "value", min: "min", max: "max", step: "step", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "focusin": "onFocusIn()", "focusout": "onFocusOut()" }, properties: { "class.ux-number-picker-invalid": "!_valid && !disabled && !_formGroup" } }, providers: [NUMBER_PICKER_VALUE_ACCESSOR], usesOnChanges: true, ngImport: i0, template: "<input\n type=\"number\"\n [id]=\"inputId\"\n role=\"spinbutton\"\n class=\"form-control number-picker-input\"\n [(ngModel)]=\"value\"\n (ngModelChange)=\"_emitValueChange($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [required]=\"required\"\n [attr.placeholder]=\"placeholder\"\n [readonly]=\"readonly\"\n (blur)=\"_touchedChange()\"\n (keydown.ArrowDown)=\"decrement($event)\"\n (keydown.ArrowUp)=\"increment($event)\"\n (wheel)=\"onScroll($event)\"\n step=\"any\"\n [disabled]=\"disabled\"\n [attr.aria-valuemin]=\"min\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-valuemax]=\"max\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"labelledBy\"\n/>\n\n<div class=\"number-picker-controls\">\n <div\n class=\"number-picker-control number-picker-control-up\"\n (click)=\"increment($event); _touchedChange()\"\n [class.disabled]=\"disabled || value >= max || readonly\"\n >\n <ux-icon name=\"up\"></ux-icon>\n </div>\n\n <div\n class=\"number-picker-control number-picker-control-down\"\n (click)=\"decrement($event); _touchedChange()\"\n [class.disabled]=\"disabled || value <= min || readonly\"\n >\n <ux-icon name=\"down\"></ux-icon>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-number-picker, ux-number-picker-inline', providers: [NUMBER_PICKER_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[class.ux-number-picker-invalid]': '!_valid && !disabled && !_formGroup',
}, standalone: false, template: "<input\n type=\"number\"\n [id]=\"inputId\"\n role=\"spinbutton\"\n class=\"form-control number-picker-input\"\n [(ngModel)]=\"value\"\n (ngModelChange)=\"_emitValueChange($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [required]=\"required\"\n [attr.placeholder]=\"placeholder\"\n [readonly]=\"readonly\"\n (blur)=\"_touchedChange()\"\n (keydown.ArrowDown)=\"decrement($event)\"\n (keydown.ArrowUp)=\"increment($event)\"\n (wheel)=\"onScroll($event)\"\n step=\"any\"\n [disabled]=\"disabled\"\n [attr.aria-valuemin]=\"min\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-valuemax]=\"max\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"labelledBy\"\n/>\n\n<div class=\"number-picker-controls\">\n <div\n class=\"number-picker-control number-picker-control-up\"\n (click)=\"increment($event); _touchedChange()\"\n [class.disabled]=\"disabled || value >= max || readonly\"\n >\n <ux-icon name=\"up\"></ux-icon>\n </div>\n\n <div\n class=\"number-picker-control number-picker-control-down\"\n (click)=\"decrement($event); _touchedChange()\"\n [class.disabled]=\"disabled || value <= min || readonly\"\n >\n <ux-icon name=\"down\"></ux-icon>\n </div>\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], labelledBy: [{
type: Input,
args: ['aria-labelledby']
}], precision: [{
type: Input
}], placeholder: [{
type: Input
}], required: [{
type: Input
}], readonly: [{
type: Input
}], valueChange: [{
type: Output
}], value: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], step: [{
type: Input
}], disabled: [{
type: Input
}], onFocusIn: [{
type: HostListener,
args: ['focusin']
}], onFocusOut: [{
type: HostListener,
args: ['focusout']
}] } });
var StepDirection;
(function (StepDirection) {
StepDirection[StepDirection["Increment"] = 0] = "Increment";
StepDirection[StepDirection["Decrement"] = 1] = "Decrement";
})(StepDirection || (StepDirection = {}));
class NumberPickerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerModule, declarations: [NumberPickerComponent], imports: [CommonModule, IconModule, FormsModule], exports: [NumberPickerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerModule, imports: [CommonModule, IconModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NumberPickerModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, IconModule, FormsModule],
exports: [NumberPickerComponent],
declarations: [NumberPickerComponent],
}]
}] });
class TooltipModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TooltipModule, declarations: [TooltipComponent, TooltipDirective], imports: [CommonModule, OverlayModule, ObserversModule$1], exports: [TooltipDirective, TooltipComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipModule, providers: [TooltipService], imports: [CommonModule, OverlayModule, ObserversModule$1] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TooltipModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, OverlayModule, ObserversModule$1],
exports: [TooltipDirective, TooltipComponent],
declarations: [TooltipComponent, TooltipDirective],
providers: [TooltipService],
}]
}] });
class ColorPickerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerModule, declarations: [ColorPickerComponent], imports: [AccessibilityModule,
CommonModule,
FormsModule,
NumberPickerModule,
TooltipModule,
IconModule], exports: [ColorPickerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerModule, imports: [AccessibilityModule,
CommonModule,
FormsModule,
NumberPickerModule,
TooltipModule,
IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColorPickerModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
FormsModule,
NumberPickerModule,
TooltipModule,
IconModule,
],
exports: [ColorPickerComponent],
declarations: [ColorPickerComponent],
}]
}] });
class ColumnSortingDirective {
constructor() {
/** Emit the current sort state for all columns within the table */
this.events = new Subject();
/** Store the current sort state for all columns within the table */
this.order = [];
}
ngOnDestroy() {
this.events.complete();
}
/** Toggle the sorting state of a column */
toggleColumn(sorting) {
// apply sorting based on the single or multiple sort
this.order = this.singleSort
? this.toggleSingleColumn(sorting)
: this.toggleMultipleColumn(sorting);
// emit the latest order
this.events.next(this.order);
return this.order;
}
/** Explicitly set the column state */
setColumnState(key, state) {
// check if the sorting has actually changed
if (this.order.find(column => column.key === key && column.state === state)) {
return;
}
// if only one column can be sorted and the current column has a sort direction remove all others
if (this.singleSort && state !== ColumnSortingState.NoSort) {
this.order = [];
}
else {
// remove the item from the state if present
this.order = this.order.filter(column => column.key !== key);
}
// if the column has active sorting then we should add it to the array again
if (state === ColumnSortingState.Ascending || state === ColumnSortingState.Descending) {
this.order = [...this.order, { key, state }];
}
}
/** Toggle the sorting state of a column when using single select */
toggleSingleColumn(sorting) {
return sorting.state === ColumnSortingState.NoSort
? []
: [{ key: sorting.key, state: sorting.state }];
}
/** Toggle the sorting state of a column when using multiple select */
toggleMultipleColumn(sorting) {
// reorder columns here
const idx = this.order.findIndex(column => column.key === sorting.key);
// if wasn't previously selected add to list and it is being sorted
if (idx === -1 && sorting.state !== ColumnSortingState.NoSort) {
return [...this.order, { key: sorting.key, state: sorting.state }];
}
// if we are sorting it change the sorting order
if (sorting.state === ColumnSortingState.Ascending ||
sorting.state === ColumnSortingState.Descending) {
return [
...this.order.filter(_column => _column.key !== sorting.key),
{ key: sorting.key, state: sorting.state },
];
}
// Otherwise remove the item
return this.order.filter(_column => _column.key !== sorting.key);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ColumnSortingDirective, isStandalone: false, selector: "[uxColumnSorting]", inputs: { singleSort: "singleSort", sortIndicator: "sortIndicator" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxColumnSorting]',
standalone: false,
}]
}], propDecorators: { singleSort: [{
type: Input
}], sortIndicator: [{
type: Input
}] } });
var ColumnSortingState;
(function (ColumnSortingState) {
ColumnSortingState["Ascending"] = "ascending";
ColumnSortingState["Descending"] = "descending";
ColumnSortingState["NoSort"] = "none";
})(ColumnSortingState || (ColumnSortingState = {}));
class ColumnSortingComponent {
constructor() {
this._sorter = inject(ColumnSortingDirective);
this._changeDetector = inject(ChangeDetectorRef);
/** Defines the sorting order of a column: `NoSort`, `Ascending` or `Descending`. */
this.state = ColumnSortingState.NoSort;
/** Determine if a column can have a `NoSort` state */
this.allowNoSort = true;
/** Specifies name of the ascending icon */
this.ascendingIcon = 'ascend';
/** Specifies name of the descending icon */
this.descendingIcon = 'descend';
/**
* Changes the state of the sorting on the column between `NoSort`, `Ascending` and `Descending`.
* This returns an array of objects for each column being sorted containing `key: string` and `state: ColumnSortingState`.
* State can be used to find the current sorting state of the column eg. `(state === ColumnSortingState.Ascending)`.
* The `ColumnSortingOrder` interface has been provided for objects in the array.
*/
this.stateChange = new EventEmitter();
/** Emit whenever the order changes */
this.orderChange = new EventEmitter();
/** Expose the sorting state enum to the view */
this.ColumnSortingState = ColumnSortingState;
/** Unsubscribe from all observables on component destroy */
this._onDestroy$ = new Subject();
}
/** Access the custom sort indicator if one was provided */
get _sortIndicator() {
return this._sorter.sortIndicator;
}
ngOnInit() {
// listen for changes triggered by the directive
this._sorter.events
.pipe(takeUntil(this._onDestroy$))
.subscribe(columns => this.updateState(columns));
}
ngOnChanges(changes) {
// if the state input is changed then apply the change
if (changes.state && changes.state.currentValue !== changes.state.previousValue) {
this._sorter.setColumnState(this.key, this.state);
}
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
/** Toggle the sorting state of a column - this is designed to be programmatically called by the consuming component */
changeState() {
switch (this.state) {
case ColumnSortingState.Ascending:
this.state = ColumnSortingState.Descending;
break;
case ColumnSortingState.Descending:
this.state = this.allowNoSort ? ColumnSortingState.NoSort : ColumnSortingState.Ascending;
break;
default:
this.state = ColumnSortingState.Ascending;
}
// change detection should be run
this._changeDetector.markForCheck();
// inform parent (internally we use a ReadonlyArray but are returning a standard array to prevent breaking changes to the public API)
return this._sorter.toggleColumn({ key: this.key, state: this.state });
}
/** Update the state based on column order */
updateState(columns) {
// if we are sorting this column then find the matching data
const columnIdx = columns.findIndex(_column => _column.key === this.key);
// if we are not sorting this column then mark it as NoSort
if (columnIdx === -1) {
this.state = ColumnSortingState.NoSort;
}
// only store the number if we have 2 or more columns being sorted
this.order = columns.length < 2 || columnIdx === -1 ? null : columnIdx + 1;
// emit the latest order value
if (typeof this.order === 'number') {
this.orderChange.emit(this.order);
}
// change detection should be run
this._changeDetector.markForCheck();
// Emit the latest change
this.stateChange.emit(this.state);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ColumnSortingComponent, isStandalone: false, selector: "ux-column-sorting", inputs: { state: "state", key: "key", order: "order", allowNoSort: "allowNoSort", ascendingIcon: "ascendingIcon", descendingIcon: "descendingIcon" }, outputs: { stateChange: "stateChange", orderChange: "orderChange" }, exportAs: ["ux-column-sorting"], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ux-column-sorting\"\n [class.ux-column-sorting-hidden]=\"state === ColumnSortingState.NoSort\"\n>\n <!-- The default sort indicator -->\n @if (!_sortIndicator) {\n <ux-icon\n class=\"ux-column-sorting-icon\"\n [class.column-sorting-icon-hidden]=\"state === ColumnSortingState.NoSort\"\n [name]=\"state === ColumnSortingState.Descending ? descendingIcon : ascendingIcon\"\n >\n </ux-icon>\n <p class=\"ux-column-sorting-number\" aria-hidden=\"true\">{{ order }}</p>\n }\n\n <!-- Custom sort indicator -->\n @if (_sortIndicator) {\n <ng-container\n [ngTemplateOutlet]=\"_sortIndicator\"\n [ngTemplateOutletContext]=\"{ state: state, order: order }\"\n >\n </ng-container>\n }\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-column-sorting', exportAs: 'ux-column-sorting', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n class=\"ux-column-sorting\"\n [class.ux-column-sorting-hidden]=\"state === ColumnSortingState.NoSort\"\n>\n <!-- The default sort indicator -->\n @if (!_sortIndicator) {\n <ux-icon\n class=\"ux-column-sorting-icon\"\n [class.column-sorting-icon-hidden]=\"state === ColumnSortingState.NoSort\"\n [name]=\"state === ColumnSortingState.Descending ? descendingIcon : ascendingIcon\"\n >\n </ux-icon>\n <p class=\"ux-column-sorting-number\" aria-hidden=\"true\">{{ order }}</p>\n }\n\n <!-- Custom sort indicator -->\n @if (_sortIndicator) {\n <ng-container\n [ngTemplateOutlet]=\"_sortIndicator\"\n [ngTemplateOutletContext]=\"{ state: state, order: order }\"\n >\n </ng-container>\n }\n</div>\n" }]
}], propDecorators: { state: [{
type: Input
}], key: [{
type: Input
}], order: [{
type: Input
}], allowNoSort: [{
type: Input
}], ascendingIcon: [{
type: Input
}], descendingIcon: [{
type: Input
}], stateChange: [{
type: Output
}], orderChange: [{
type: Output
}] } });
class ColumnSortingModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingModule, declarations: [ColumnSortingComponent, ColumnSortingDirective], imports: [CommonModule, IconModule], exports: [ColumnSortingComponent, ColumnSortingDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingModule, imports: [CommonModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnSortingModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, IconModule],
exports: [ColumnSortingComponent, ColumnSortingDirective],
declarations: [ColumnSortingComponent, ColumnSortingDirective],
}]
}] });
class ConduitSubject {
constructor(conduit, _zone, zoneId) {
this.conduit = conduit;
this._zone = _zone;
this.zoneId = zoneId;
this._onDestroy = new Subject();
// store the target subject object
this._subject = conduit.subject;
// check if there are any conduits that have supplied an initial value
this.getInitialValue();
// subscribe to changes to the source subject
this._subject
.pipe(distinctUntilChanged(conduit.changeDetection), takeUntil(this._onDestroy))
.subscribe(this.onOutput.bind(this));
// subscribe to the zone events and root zone events
_zone
.getEvents()
.pipe(filter(event => event.conduit.id === conduit.id), takeUntil(this._onDestroy))
.subscribe(this.onInput.bind(this));
}
/** Check all allow inputs to see if there is a value we should initially set the conduit to */
getInitialValue() {
// if we do not accept inputs then do nothing
if (this.conduit.acceptsInput === false) {
return;
}
// return all subjects that are 1) Not itself 2) In a zone that is listed in acceptsInput 3) Have a currentValue set
const subjects = this._zone.getSubjects().filter(subject => {
// If this is itself or if it has not value to give us then do nothing
if (subject === this ||
subject.conduit.id !== this.conduit.id ||
!('currentValue' in subject.conduit)) {
return false;
}
// if acceptsInput is true then we return every time
if (this.conduit.acceptsInput === true) {
return true;
}
if (Array.isArray(this.conduit.acceptsInput)) {
return this.conduit.acceptsInput.indexOf(subject.zoneId) !== -1;
}
});
// if there are no matches then do nothing
if (subjects.length === 0) {
return;
}
// otherwise sort by the last modified field
subjects.sort((subjectOne, subjectTwo) => subjectOne.conduit.lastModified.getTime() < subjectTwo.conduit.lastModified.getTime() ? 1 : -1);
// get the most recent value
this._subject.next(subjects[0].conduit.currentValue);
}
/** This will be triggered when a conduits value has changed */
onInput(event) {
// if we dont accept input or we emitted this value then do nothing
if (this.conduit.acceptsInput === false || event.conduit === this.conduit) {
return;
}
// check if the conduit produces output - if not we only do something if we are in the same zone
if (event.conduit.producesOutput === false && event.zoneId !== this.zoneId) {
return;
}
// check if we only accept inputs from specific zones
if (Array.isArray(this.conduit.acceptsInput)) {
// check if the event came from an acceptable zone
if (!this.conduit.acceptsInput.find(zone => zone === event.zoneId)) {
return;
}
}
// if required transform the value
const outputValue = this.conduit.map ? this.conduit.map(event.value) : event.value;
// update the subject
this._subject.next(outputValue);
}
/** This will be fired when this conduit emits a new value */
onOutput(value) {
// store the most recent value and when it was modified - can be used for any new conduits to lookup a value
this.conduit.currentValue = value;
this.conduit.lastModified = new Date();
// check if this should produce output
if (this.conduit.producesOutput) {
this._zone.emit({ conduit: this.conduit, zoneId: this.zoneId, value });
}
}
/** Unsubscribe once this subject is destroyed */
destroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
}
class ConduitZone {
/** Create a global subject store */
static { this.subjects = []; }
/** Expose an event stream of new values */
static { this.events = new Subject(); }
ngOnDestroy() {
// find all conduit subjects that are part of this zone
ConduitZone.subjects
.filter(_subject => _subject.zoneId === this._zoneId)
.forEach(_subject => this.unregisterConduit(_subject.conduit));
}
/** Store reference to the repository and begin watching for and emitting changes */
registerConduit(conduit) {
ConduitZone.subjects.push(new ConduitSubject(conduit, this, this._zoneId));
}
/** Destroy a conduit */
unregisterConduit(conduit) {
const subject = this.getConduitSubject(conduit.subject);
if (subject) {
// remove the subject from the internal list of conduit subjects
ConduitZone.subjects = ConduitZone.subjects.filter(_subject => _subject !== subject);
// perform all unsubscriptions
subject.destroy();
}
}
/** Provide the zone with an ID */
setZoneId(zoneId) {
this._zoneId = zoneId;
}
/** Emit a value to all zones for checking */
emit(event) {
ConduitZone.events.next(event);
}
/** Retrieve a conduit subsject object from the rxjs subject */
getConduitSubject(subject) {
return ConduitZone.subjects.find(_subject => _subject.conduit.subject === subject);
}
/** Get all subjects from all zones */
getSubjects() {
return ConduitZone.subjects;
}
/** Alter the properties of a conduit dynamically */
setConduitProperties(subject, properties) {
// find the conduit with the matching subject
const conduitSubject = this.getSubjects().find(_conduit => _conduit.conduit.subject === subject);
// if a match was found update the properties
if (conduitSubject) {
// update each specified property
for (const prop in properties) {
conduitSubject.conduit[prop] = properties[prop];
}
}
}
/** Programmatically create a conduit at runtime */
createConduit(subject, properties) {
// register the conduit with the zone
this.registerConduit({ ...properties, subject });
}
/** Register all conduits in a component */
registerConduits(component) {
if (Array.isArray(component._conduits)) {
component._conduits.forEach((conduit) => this.registerConduit({ ...conduit, subject: component[conduit.propertyKey] }));
}
}
/** Register all conduits in a component */
unregisterConduits(component) {
if (Array.isArray(component._conduits)) {
component._conduits.forEach((conduit) => this.unregisterConduit(conduit));
}
}
/** Return the global event stream */
getEvents() {
return ConduitZone.events;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitZone, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitZone }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitZone, decorators: [{
type: Injectable
}] });
class ConduitComponent {
constructor() {
this._zone = inject(ConduitZone, { optional: true });
}
/** We need to register the conduits with the zone when the component is initialised */
ngOnInit() {
// register the conduit in the zone and ensure it gets the correct instance of the target
this._zone.registerConduits(this);
}
/** We need to unregister the conduits when the component is destroyed */
ngOnDestroy() {
this._zone.unregisterConduits(this);
}
/** Alter the properties of a conduit dynamically */
setConduitProperties(subject, properties) {
this._zone.setConduitProperties(subject, properties);
}
/** Programmatically create a conduit at runtime */
createConduit(subject, properties) {
this._zone.createConduit(subject, properties);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ConduitComponent, isStandalone: false, selector: "ux-conduit", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitComponent, decorators: [{
type: Directive,
args: [{
selector: 'ux-conduit',
standalone: false,
}]
}] });
class ConduitZoneComponent extends ConduitComponent {
ngOnInit() {
this._zone.setZoneId(this.zoneId);
super.ngOnInit();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitZoneComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ConduitZoneComponent, isStandalone: false, selector: "ux-conduit-zone", usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitZoneComponent, decorators: [{
type: Directive,
args: [{
selector: 'ux-conduit-zone',
standalone: false,
}]
}] });
const defaultConduitProps = {
acceptsInput: true,
producesOutput: true,
};
/** Expose the property that conduits will be stored in */
const CONDUITS = '_conduits';
/** Create the conduit property decorator */
function Conduit(properties) {
return (target, propertyKey) => {
if (typeof properties === 'function') {
properties = properties.call(null);
}
// if the target does not already have a conduit list then create one
// eslint-disable-next-line no-prototype-builtins
if (!target.hasOwnProperty(CONDUITS)) {
Object.defineProperty(target, CONDUITS, { value: [] });
}
// add the conduit to the list ensuring all required properties are provided
target[CONDUITS].push({
...defaultConduitProps,
...properties,
target,
propertyKey,
});
};
}
/**
* This module is not required to be imported but is required
* by the Angular compiler, otherwise it will complain that
* ConduitZoneComponent is not part of an NgModule and will
* fail to build
*/
class ConduitModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ConduitModule, declarations: [ConduitComponent, ConduitZoneComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ConduitModule, decorators: [{
type: NgModule,
args: [{
declarations: [
ConduitComponent,
ConduitZoneComponent,
],
}]
}] });
class DragService {
constructor() {
/** Emit when dragging begins */
this.onDragStart = new Subject();
/** Emit when dragging moves */
this.onDrag = new Subject();
/** Emit when dragging ends */
this.onDragEnd = new Subject();
/** Emit when the user is dragging over the drop area */
this.onDropEnter = new Subject();
/** Emit when the user is dragging out of the drop area */
this.onDropLeave = new Subject();
/** Emit when a drop occurs */
this.onDrop = new Subject();
}
/** Destroy all observables */
ngOnDestroy() {
this.onDragStart.complete();
this.onDrag.complete();
this.onDragEnd.complete();
this.onDrop.complete();
this.onDropEnter.complete();
this.onDropLeave.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class DragDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._ngZone = inject(NgZone);
this._renderer = inject(Renderer2);
this._scrollDispatcher = inject(ScrollDispatcher$1);
this._drag = inject(DragService);
/** Detemine if we should show a clone when dragging */
this.clone = false;
/** Allow the dragging to be enabled/disabled */
this.draggable = true;
/** Emit an event when dragging starts */
this.onDragStart = new EventEmitter();
/** Emit an event when the mouse moves while dragging */
this.onDrag = new EventEmitter();
/** Emit an event when the document scrolls while dragging */
this.onDragScroll = new EventEmitter();
/** Emit an event when the dragging finishes */
this.onDragEnd = new EventEmitter();
/** Emit when the user drops an item in a drop area */
this.onDrop = new EventEmitter();
/** Emit when the user drags over a drop area */
this.onDropEnter = new EventEmitter();
/** Emit when the user drags out of a drop area */
this.onDropLeave = new EventEmitter();
/** Store the dragging state */
this._isDragging = false;
/** The offset in pixels currently being scrolled while dragging */
this._scrollOffset = { x: 0, y: 0 };
/** Create an observable from the mouse down event */
this._mousedown$ = fromEvent(this._elementRef.nativeElement, 'mousedown');
/** Create an observable from the mouse move event */
this._mousemove$ = fromEvent(document, 'mousemove');
/** Create an observable from the mouse up event */
this._mouseup$ = fromEvent(document, 'mouseup');
/** Use an observable to unsubscribe from all subscriptions */
this._onDestroy = new Subject();
// ensure all mouse down events on the object are captured
this._mousedown$
.pipe(filter(() => this.draggable), takeUntil(this._onDestroy))
.subscribe(this.dragStart.bind(this));
// emit the outputs when drag events occur
this._drag.onDragStart
.pipe(filter(() => this._isDragging), takeUntil(this._onDestroy))
.subscribe((dragEvent) => this.onDragStart.emit(dragEvent.event));
this._drag.onDrag
.pipe(filter(() => this._isDragging), takeUntil(this._onDestroy))
.subscribe((dragEvent) => this.onDrag.emit(dragEvent.event));
this._drag.onDragEnd
.pipe(filter(event => this._isDragging || (this.model && this.model === event.data)), takeUntil(this._onDestroy))
.subscribe(() => this.onDragEnd.emit());
this._drag.onDrop
.pipe(filter(() => this._isDragging), takeUntil(this._onDestroy))
.subscribe((event) => this.onDrop.emit(event));
this._drag.onDropEnter
.pipe(filter(() => this._isDragging), takeUntil(this._onDestroy))
.subscribe(() => this.onDropEnter.emit());
this._drag.onDropLeave
.pipe(filter(() => this._isDragging), takeUntil(this._onDestroy))
.subscribe(() => this.onDropLeave.emit());
}
/** Emit events and create clone when drag starts */
dragStart(event) {
event.preventDefault();
// start listening for scroll events on the nearest scrollable ancestor element
this.createScrollEventListener();
if (this.clone) {
// clone the node
this.cloneNode(event);
}
// apply a class to the element being dragged
this._renderer.addClass(this._elementRef.nativeElement, 'ux-drag-dragging');
// store the dragging state
this._isDragging = true;
// emit the drag start event
this._ngZone.run(() => this._drag.onDragStart.next({ event, group: this.group, data: this.model }));
this._mousemove$
.pipe(takeUntil(this._mouseup$), takeUntil(this._onDestroy))
.subscribe(this.dragMove.bind(this));
// When dragging stops emit the drag end
this._mouseup$.pipe(first()).subscribe(this.dragEnd.bind(this));
}
/** Emit event and update clone position when dragging moves */
dragMove(event) {
event.preventDefault();
// scroll the viewport if needed
this.updateScrolling(event);
if (this._clone) {
this.updateNodePosition(event);
}
// emit the drag start event
this._ngZone.run(() => this._drag.onDrag.next({ event, group: this.group, data: this.model }));
}
/** Emit event and destroy clone when dragging ends */
dragEnd() {
// if the drag ended outside of the viewport, stop the scrolling interval
this.stopScrolling();
this.removeScrollEventListener();
// if there was a clone, remove it
if (this._clone) {
this._renderer.removeChild(document.body, this._clone);
this._clone = null;
}
// remove the dragging class
this._renderer.removeClass(this._elementRef.nativeElement, 'ux-drag-dragging');
// emit the on drag end output
this._ngZone.run(() => this._drag.onDragEnd.next({ group: this.group, data: this.model }));
// store the dragging state
this._isDragging = false;
}
/** Emit the onDragScroll event */
scroll() {
const offsetX = this._scrollParent.scrollLeft - this._scrollPosition.left;
const offsetY = this._scrollParent.scrollTop - this._scrollPosition.top;
this.onDragScroll.emit({ offsetX, offsetY });
this._scrollPosition = {
top: this._scrollParent.scrollTop,
left: this._scrollParent.scrollLeft,
};
}
/** Create an exact clone of an element */
cloneNode(event) {
// duplicate the node
this._clone = this._elementRef.nativeElement.cloneNode(true);
// store the position within the draggable element
const { top, left, width } = this._elementRef.nativeElement.getBoundingClientRect();
this._offset = { x: event.clientX - left, y: event.clientY - top };
// inline all styles so it looks identical regardless of its position in the DOM
this.inlineStyles(this._elementRef.nativeElement, this._clone);
// IE doesn't always calculate the correct width value using getComputedStyles... use bounding client value instead
this._renderer.setStyle(this._clone, 'width', width + 'px');
// ensure we can easily position the node an it is above all other elements
this._renderer.setAttribute(this._clone, 'aria-hidden', 'true');
this._renderer.setStyle(this._clone, 'position', 'absolute');
this._renderer.setStyle(this._clone, 'z-index', '99999');
// apply a class to allow custom styling
this._renderer.addClass(this._clone, 'ux-drag-dragging-clone');
// insert the cloned element
this._renderer.appendChild(document.body, this._clone);
// set the cloned element initial position
this.updateNodePosition(event);
}
/** Position the clone relative to the mouse */
updateNodePosition(event) {
this._renderer.setStyle(this._clone, 'left', event.pageX - this._offset.x + 'px');
this._renderer.setStyle(this._clone, 'top', event.pageY - this._offset.y + 'px');
}
/** Inline all styles to ensure styling is consistent regardless of its position in the dom */
inlineStyles(source, target) {
// get all the computed styles from the source element
const styles = getComputedStyle(source);
// inline every specified style
for (let idx = 0; idx < styles.length; idx++) {
const style = styles.item(idx);
if (style !== undefined) {
this._renderer.setStyle(target, styles[idx], styles[style]);
}
}
// ensure we dont capture any move events
this._renderer.setStyle(target, 'pointer-events', 'none');
// do the same for all the child elements
for (let idx = 0; idx < source.children.length; idx++) {
this.inlineStyles(source.children[idx], target.children[idx]);
}
}
/** Unsubscribe from all subscriptions */
ngOnDestroy() {
this.stopScrolling();
this.removeScrollEventListener();
this._onDestroy.next();
this._onDestroy.complete();
}
updateScrolling(event) {
this._scrollOffset = this.getScrollOffsets(this._scrollParent, event);
if (this._scrollOffset.x === 0 && this._scrollOffset.y === 0) {
this.stopScrolling();
}
else {
this.startScrolling(this._scrollParent);
}
}
getScrollParent() {
// Get the nearest ancestor element with the cdkScrollable directive applied
const containers = this._scrollDispatcher.getAncestorScrollContainers(this._elementRef);
if (containers.length > 0) {
return containers[containers.length - 1].getElementRef().nativeElement;
}
return document.documentElement;
}
createScrollEventListener() {
// get the nearest scrollable ancestor
this._scrollParent = this.getScrollParent();
// save the current scroll position to allow calculation of the scroll delta
this._scrollPosition = {
top: this._scrollParent.scrollTop,
left: this._scrollParent.scrollLeft,
};
// start listening for scroll events
const target = this._scrollParent === document.documentElement ? 'document' : this._scrollParent;
this._scrollListener = this._renderer.listen(target, 'scroll', this.scroll.bind(this));
}
removeScrollEventListener() {
// remove the scroll event listener
if (this._scrollListener) {
this._scrollListener();
this._scrollListener = null;
}
}
getScrollOffsets(scrollElement, event) {
let scrollX = 0;
let scrollY = 0;
// scroll by at least this much so that it still scrolls if the pointer is exactly at the edge of the scroll element
const minScroll = 5;
const isRoot = scrollElement === document.documentElement;
const bounds = scrollElement.getBoundingClientRect();
const pointerOffsetX = isRoot ? event.clientX : event.clientX - bounds.x;
const pointerOffsetY = isRoot ? event.clientY : event.clientY - bounds.y;
if (pointerOffsetX <= 0 && scrollElement.scrollLeft > 0) {
scrollX = Math.min(pointerOffsetX, -minScroll);
}
else if (pointerOffsetX >= scrollElement.clientWidth &&
scrollElement.scrollLeft + scrollElement.clientWidth < scrollElement.scrollWidth) {
scrollX = Math.max(pointerOffsetX - scrollElement.clientWidth, minScroll);
}
if (pointerOffsetY <= 0 && scrollElement.scrollTop > 0) {
scrollY = Math.min(pointerOffsetY, -minScroll);
}
else if (pointerOffsetY >= scrollElement.clientHeight &&
scrollElement.scrollTop + scrollElement.clientHeight < scrollElement.scrollHeight) {
scrollY = Math.max(pointerOffsetY - scrollElement.clientHeight, minScroll);
}
return { x: scrollX, y: scrollY };
}
startScrolling(scrollElement) {
if (!this._scrollIntervalHandle) {
this._scrollIntervalHandle = window.setInterval(() => this.performScroll(scrollElement), 100);
}
}
stopScrolling() {
if (this._scrollIntervalHandle) {
clearInterval(this._scrollIntervalHandle);
this._scrollIntervalHandle = 0;
}
}
performScroll(scrollElement) {
scrollElement.scrollLeft += this._scrollOffset.x;
scrollElement.scrollTop += this._scrollOffset.y;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DragDirective, isStandalone: false, selector: "[uxDrag]", inputs: { clone: "clone", group: "group", model: "model", draggable: "draggable" }, outputs: { onDragStart: "onDragStart", onDrag: "onDrag", onDragScroll: "onDragScroll", onDragEnd: "onDragEnd", onDrop: "onDrop", onDropEnter: "onDropEnter", onDropLeave: "onDropLeave" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxDrag]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { clone: [{
type: Input
}], group: [{
type: Input
}], model: [{
type: Input
}], draggable: [{
type: Input
}], onDragStart: [{
type: Output
}], onDrag: [{
type: Output
}], onDragScroll: [{
type: Output
}], onDragEnd: [{
type: Output
}], onDrop: [{
type: Output
}], onDropEnter: [{
type: Output
}], onDropLeave: [{
type: Output
}] } });
class DropDirective {
constructor() {
this._dragService = inject(DragService);
/** Define whether or not dropping is enabled */
this.dropDisabled = false;
/** Emit the model of the item dropped */
this.onDrop = new EventEmitter();
/** Determine whether or not the mouse is within the drop region */
this.isMouseOver = false;
/** Determine whether or not we are currently dragging an item */
this.isDragging = false;
/** Ensure we destroy all subscriptions */
this._onDestroy = new Subject();
// subscribe to drag events
this._dragService.onDragStart
.pipe(tap(event => (this._group = event.group)), filter(event => this.isDropAllowed(event.group)), takeUntil(this._onDestroy))
.subscribe(this.onDragStart.bind(this));
this._dragService.onDragEnd
.pipe(filter(event => this.isDropAllowed(event.group)), takeUntil(this._onDestroy))
.subscribe(this.onDragEnd.bind(this));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Update the mouse over state */
onMouseOver() {
if (this.isDropAllowed(this._group)) {
this.isMouseOver = true;
// emit that we are over a drop area
this._dragService.onDropEnter.next();
}
}
/** Update the mouse over state */
onMouseLeave() {
// always ensure this value is reset
this.isMouseOver = false;
// only emit the dropd leave event when appropriate
if (this.isDropAllowed(this._group)) {
this._dragService.onDropLeave.next();
}
}
/** Update the dragging state */
onDragStart() {
this.isDragging = true;
}
/** Update the dragging state */
onDragEnd(event) {
// update the dragging state
this.isDragging = false;
// clear the cached group
this._group = null;
// if the mouse is over and it is in an allowed group emit the dop event
if (this.isMouseOver && this.isDropAllowed(event.group)) {
this.onDrop.emit(event.data);
this._dragService.onDrop.next(event.data);
}
}
/** Determine whether or not the event is part of the specified groups */
isDropAllowed(group) {
// if dropping is disabled then it is never allowed
if (this.dropDisabled) {
return false;
}
// if no group specified allow all groups
if (!this.group) {
return true;
}
// if it is an array then ensure it is allowed
if (Array.isArray(this.group)) {
return !!this.group.find(_group => _group === group);
}
return this.group === group;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DropDirective, isStandalone: false, selector: "[uxDrop]", inputs: { group: "group", dropDisabled: "dropDisabled" }, outputs: { onDrop: "onDrop" }, host: { listeners: { "mouseenter": "onMouseOver()", "mouseleave": "onMouseLeave()" }, properties: { "class.ux-drop-hover": "isMouseOver && isDragging && !dropDisabled" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DropDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxDrop]',
host: {
'[class.ux-drop-hover]': 'isMouseOver && isDragging && !dropDisabled',
},
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { group: [{
type: Input
}], dropDisabled: [{
type: Input
}], onDrop: [{
type: Output
}], onMouseOver: [{
type: HostListener,
args: ['mouseenter']
}], onMouseLeave: [{
type: HostListener,
args: ['mouseleave']
}] } });
class DragModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DragModule, declarations: [DragDirective, DropDirective], exports: [DragDirective, DropDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DragModule, decorators: [{
type: NgModule,
args: [{
exports: [DragDirective, DropDirective],
declarations: [DragDirective, DropDirective],
}]
}] });
var DashboardStackMode;
(function (DashboardStackMode) {
DashboardStackMode[DashboardStackMode["Regular"] = 0] = "Regular";
DashboardStackMode[DashboardStackMode["Stacked"] = 1] = "Stacked";
/** Determine the mode automatically based on dashboard width. */
DashboardStackMode[DashboardStackMode["Auto"] = 2] = "Auto";
})(DashboardStackMode || (DashboardStackMode = {}));
class DashboardService {
get options() {
return this.options$.getValue();
}
get widgets() {
return this.widgets$.getValue();
}
get stacked() {
return this.stacked$.getValue();
}
get dimensions() {
return this.dimensions$.getValue();
}
get columnWidth() {
return this.dimensions.width / this.options.columns;
}
constructor() {
this._rowHeight = 0;
this.initialized$ = new BehaviorSubject(false);
this.widgets$ = new BehaviorSubject([]);
this.options$ = new BehaviorSubject(defaultOptions);
this.dimensions$ = new BehaviorSubject({});
this.height$ = this.dimensions$.pipe(tick(), map(dimensions => dimensions.height), distinctUntilChanged());
this.placeholder$ = new BehaviorSubject({
visible: false,
x: 0,
y: 0,
width: 0,
height: 0,
});
this.layout$ = new BehaviorSubject([]);
this.stacked$ = new BehaviorSubject(false);
this.isDragging$ = new BehaviorSubject(null);
this.isGrabbing$ = new BehaviorSubject(null);
this.userLayoutChange$ = new Subject();
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
combineLatest(this.layout$, this.widgets$, this.initialized$)
.pipe(tick(), filter(([layout, widgets, initialized]) => layout && widgets.length > 0 && initialized), takeUntil(this._onDestroy))
.subscribe(([layout]) => {
this.setLayoutData(layout);
this.renderDashboard();
});
this.stacked$
.pipe(distinctUntilChanged(), filter(stacked => stacked === true), takeUntil(this._onDestroy))
.subscribe(this.updateWhenStacked.bind(this));
this.widgets$.pipe(tick(), takeUntil(this._onDestroy)).subscribe(() => this.renderDashboard());
this.dimensions$
.pipe(tick(), takeUntil(this._onDestroy))
.subscribe(() => this.renderDashboard());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Add a widget to the dashboard
* @param widget The widget component to add to the dashboard
*/
addWidget(widget) {
this.widgets$.next([...this.widgets$.getValue(), widget]);
}
/**
* Remove a widget from the dashboard
* @param widget The widget to remove
*/
removeWidget(widget) {
this.widgets$.next(this.widgets$.getValue().filter(_widget => _widget !== widget));
}
/**
* Indicate that the dashboard element has been resized
* @param width The width of the dashboard element in px
* @param height The height of the dashboard element in px
*/
setDimensions(width = this.dimensions.width, height = this.dimensions.height) {
if (this.dimensions.width !== width || this.dimensions.height !== height) {
this.dimensions$.next({ width, height });
}
}
/**
* Produce an object containing all the required layout data.
* This can be useful for exporting/saving a layout
*/
getLayoutData() {
return this.widgets.map(widget => {
return {
id: widget.id,
col: widget.getColumn(),
row: widget.getRow(),
colSpan: widget.getColumnSpan(),
rowSpan: widget.getRowSpan(),
minColSpan: widget.minColSpan,
minRowSpan: widget.minRowSpan,
};
});
}
/**
* Position widgets programatically
*/
setLayoutData(widgets) {
// iterate through each widget data and find a match
widgets.forEach(widget => {
// find the matching widget
const target = this.widgets.find(_widget => _widget.id === widget.id);
if (target) {
target.setColumn(widget.col);
target.setRow(widget.row);
target.setColumnSpan(widget.colSpan);
target.setRowSpan(widget.rowSpan);
target.minColSpan = widget.minColSpan ?? 1;
target.minRowSpan = widget.minRowSpan ?? 1;
}
});
}
/**
* Update the positions and sizes of the widgets
*/
renderDashboard() {
// get the dimensions of the dashboard
this._rowHeight = this.options.rowHeight || this.columnWidth;
// ensure the column width is not below the min widths
this.stacked$.next(this.columnWidth < this.options.minWidth);
this.setDashboardLayout();
// iterate through each widget and set the size - except the one being resized
this.widgets
.filter(widget => !this._actionWidget || widget !== this._actionWidget.widget)
.forEach(widget => widget.render());
}
/**
* Determine where widgets should be positioned based on their positions, width and the size of the container
*/
setDashboardLayout() {
// find any widgets that do not currently have a position set
this.widgets
.filter(widget => widget.getColumn() === undefined || widget.getRow() === undefined)
.forEach(widget => this.setWidgetPosition(widget));
this.setDashboardHeight();
}
updateWhenStacked() {
// iterate through each widget set it's stacked state and retain the rowSpan
this.getWidgetsByOrder().forEach((widget, idx, widgets) => {
const widgetsAbove = widgets.slice(0, idx);
const row = widgetsAbove.reduce((currentRow, _widget) => currentRow + _widget.getRowSpan(), 0);
widget.setColumn(0);
widget.setRow(row);
});
}
/** Get widgets in the order they visually appear as the widgets array order does not reflect this */
getWidgetsByOrder() {
return [...this.widgets].sort((w1, w2) => {
const w1Position = w1.getColumn(DashboardStackMode.Regular) +
w1.getRow(DashboardStackMode.Regular) * this.options.columns;
const w2Position = w2.getColumn(DashboardStackMode.Regular) +
w2.getRow(DashboardStackMode.Regular) * this.options.columns;
if (w1Position < w2Position) {
return -1;
}
if (w1Position > w2Position) {
return 1;
}
return 0;
});
}
/**
* Find a position that a widget can fit in the dashboard
* @param widget The widget to try and position
*/
setWidgetPosition(widget) {
// find a position for the widget
let position = 0;
let success = false;
// repeat until a space is found
while (!success) {
// get a position to try
const column = position % this.options.columns;
const row = Math.floor(position / this.options.columns);
// check the current position
if (this.getPositionAvailable(column, row, widget.getColumnSpan(), widget.getRowSpan())) {
success = true;
widget.setColumn(column);
widget.setRow(row);
return;
}
if (column === 0 && widget.colSpan > this.options.columns) {
throw new Error('Dashboard widgets have a colSpan greater than the max number of dashboard columns!');
}
position++;
}
}
/**
* Check if a position in the dashboard is vacant or not
*/
getPositionAvailable(column, row, columnSpan, rowSpan, ignoreWidget) {
// get a list of grid spaces that are populated
const spaces = this.getOccupiedSpaces();
// check if the block would still be in bounds
if (column + columnSpan > this.options.columns) {
return false;
}
// check each required position
for (let x = column; x < column + columnSpan; x++) {
for (let y = row; y < row + rowSpan; y++) {
if (spaces.find(block => block.column === x && block.row === y && block.widget !== ignoreWidget)) {
return false;
}
}
}
return true;
}
getOccupiedSpaces() {
// find all spaces that are currently occupied
return this.widgets
.filter(widget => widget.getColumn() !== undefined && widget.getRow() !== undefined)
.reduce((value, widget) => {
this.forEachBlock(widget, (column, row) => value.push({ widget, column, row }));
return value;
}, []);
}
/**
* Begin resizing a widget
* @param action The the widget to resize
*/
onResizeStart(action) {
this.cacheWidgets();
// store the mouse event
this._event = action.event;
this._actionWidget = action;
// bring the widget to the font
this.bringToFront(action.widget);
}
onResizeDrag(action) {
const mousePosX = this._event.pageX - pageXOffset;
const mousePosY = this._event.pageY - pageYOffset;
// if there was no movement then do nothing
if (action.event.x === mousePosX && action.event.y === mousePosY) {
return;
}
// update the stored mouse event
this._event = action.event;
// get handle for direction
const { handle } = action;
// get the bounds of the handle
const bounds = handle.getBoundingClientRect();
// get the center of the handle
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
// get the current mouse position
const mouseX = mousePosX - centerX;
const mouseY = mousePosY - centerY;
// store the new proposed dimensions for the widget
const dimensions = {
x: action.widget.x,
y: action.widget.y,
width: action.widget.width,
height: action.widget.height,
};
// update widget based on the handle being dragged
switch (action.direction) {
case ActionDirection.Right:
dimensions.width += mouseX;
break;
case ActionDirection.Left:
dimensions.x += mouseX;
dimensions.width -= mouseX;
if (dimensions.width < this.options.minWidth) {
const difference = this.options.minWidth - dimensions.width;
dimensions.x -= difference;
dimensions.width += difference;
}
break;
case ActionDirection.Bottom:
dimensions.height += mouseY;
break;
case ActionDirection.Top:
dimensions.y += mouseY;
dimensions.height -= mouseY;
if (dimensions.height < this.options.minHeight) {
const difference = this.options.minHeight - dimensions.height;
dimensions.y -= difference;
dimensions.height += difference;
}
break;
// Support resizing on multiple axis simultaneously
case ActionDirection.TopLeft:
dimensions.x += mouseX;
dimensions.width -= mouseX;
if (dimensions.width < this.options.minWidth) {
const difference = this.options.minWidth - dimensions.width;
dimensions.x -= difference;
dimensions.width += difference;
}
dimensions.y += mouseY;
dimensions.height -= mouseY;
if (dimensions.height < this.options.minHeight) {
const difference = this.options.minHeight - dimensions.height;
dimensions.y -= difference;
dimensions.height += difference;
}
break;
case ActionDirection.TopRight:
dimensions.width += mouseX;
dimensions.y += mouseY;
dimensions.height -= mouseY;
if (dimensions.height < this.options.minHeight) {
const difference = this.options.minHeight - dimensions.height;
dimensions.y -= difference;
dimensions.height += difference;
}
break;
case ActionDirection.BottomLeft:
dimensions.height += mouseY;
dimensions.x += mouseX;
dimensions.width -= mouseX;
if (dimensions.width < this.options.minWidth) {
const difference = this.options.minWidth - dimensions.width;
dimensions.x -= difference;
dimensions.width += difference;
}
break;
case ActionDirection.BottomRight:
dimensions.height += mouseY;
dimensions.width += mouseX;
break;
}
const currentWidth = action.widget.x + action.widget.width;
const currentHeight = action.widget.y + action.widget.height;
// ensure values are within the dashboard bounds
if (dimensions.x < 0) {
dimensions.x = 0;
dimensions.width = currentWidth;
}
if (dimensions.y < 0) {
dimensions.y = 0;
dimensions.height = currentHeight;
}
if (dimensions.x + dimensions.width > this.dimensions.width) {
dimensions.width = this.dimensions.width - dimensions.x;
}
// if the proposed width is smaller than allowed then reset width to minimum and ignore x changes
if (dimensions.width < this.options.minWidth) {
dimensions.x = action.widget.x;
dimensions.width = this.options.minWidth;
}
// if the proposed height is smaller than allowed then reset height to minimum and ignore y changes
if (dimensions.height < this.options.minHeight) {
dimensions.y = action.widget.y;
dimensions.height = this.options.minHeight;
}
const minWidth = this.getColumnWidth() * action.widget.minColSpan - this.getColumnWidth() / 2 + 1;
const isBelowMinWidth = dimensions.width < minWidth;
if (isBelowMinWidth) {
dimensions.x = action.widget.x;
dimensions.width = minWidth;
}
const minHeight = this.options.rowHeight * action.widget.minRowSpan - this.options.rowHeight / 2 + 1;
const isBelowMinHeight = dimensions.height < minHeight;
if (isBelowMinHeight) {
dimensions.y = action.widget.y;
dimensions.height = minHeight;
}
// update the widget actual values
action.widget.setBounds(dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// update placeholder position and value
this.setPlaceholderBounds(true, dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// show the widget positions if the current positions and sizes were to persist
this.updateWidgetPositions(action.widget);
}
onResizeEnd() {
const placeholder = this.placeholder$.getValue();
// commit resize changes
this.commitWidgetChanges();
// hide placeholder
placeholder.visible = false;
// update the placeholder
this.placeholder$.next(placeholder);
this._actionWidget = null;
this._event = null;
// ensure any vacant upper spaces are filled where required
this.shiftWidgetsUp();
// update dashboard height
this.setDashboardHeight();
// emit information about the layout
this.layout$.next(this.getLayoutData());
this.userLayoutChange$.next(this.getLayoutData());
}
onDragStart(action) {
this.onResizeStart(action);
// store the starting placeholder position
this.setWidgetOrigin();
this.cacheWidgets();
// emit the widget we are dragging
this.isDragging$.next(action.widget);
}
onDragEnd() {
this.onResizeEnd();
this._widgetOrigin = {};
this.isDragging$.getValue().sendToBack();
this.isDragging$.next(null);
this.userLayoutChange$.next(this.getLayoutData());
}
onDrag(action) {
// if there was no movement then do nothing
if (action.event.clientX === this._event.clientX &&
action.event.clientY === this._event.clientY) {
return;
}
// get the current mouse position
const mouseX = action.event.clientX - this._event.clientX;
const mouseY = action.event.clientY - this._event.clientY;
// store the latest event
this._event = action.event;
this.moveWidget(action.widget, mouseX, mouseY);
}
onDragScroll(widget, event) {
this.moveWidget(widget, event.offsetX, event.offsetY);
}
getRowHeight() {
return this._rowHeight;
}
cacheWidgets() {
this._cache = this.widgets.map(widget => ({
id: widget.id,
column: widget.getColumn(),
row: widget.getRow(),
columnSpan: widget.getColumnSpan(),
rowSpan: widget.getRowSpan(),
}));
// return a new array of the cache for custom caching
return [...this._cache];
}
restoreWidgets(ignoreActionWidget = false, cache = this._cache, restoreSize = false) {
cache
.filter(widget => !ignoreActionWidget || widget.id !== this._actionWidget.widget.id)
.forEach(widget => {
const match = this.widgets.find(wgt => wgt.id === widget.id);
if (match) {
match.setColumn(widget.column);
match.setRow(widget.row);
if (restoreSize) {
match.setColumnSpan(widget.columnSpan);
match.setRowSpan(widget.rowSpan);
}
}
});
}
/**
* Return the set of widgets which overlap the given dashboard region.
*/
getOverlappingWidgets(region, actionWidget) {
const widgetsToMove = [];
// check if there are any widgets overlapping widgets
for (let row = region.row; row < region.row + region.rowSpan; row++) {
for (let column = region.column; column < region.column + region.columnSpan; column++) {
// store reference to any widgets that need moved
this.getOccupiedSpaces()
.filter(space => space.column === column && space.row === row && space.widget !== actionWidget)
.forEach(space => widgetsToMove.push(space.widget));
}
}
// remove any duplicates
return widgetsToMove.filter((wgt, idx, array) => array.indexOf(wgt) === idx);
}
/**
* Resolve any overlapping widgets after a widget changes rowSpan/colSpan.
*/
resizeWidget(widget) {
// make widget action and origin widget, direction is irrelevant to this function so set to 0
this._actionWidget = { widget, direction: 0 };
this._widgetOrigin = widget;
const widgetRegion = {
row: widget.row,
column: widget.col,
rowSpan: widget.rowSpan,
columnSpan: widget.colSpan,
};
let done = false;
const ITERATION_LIMIT = 100;
for (let i = 0; i <= ITERATION_LIMIT; i++) {
// Check for overlapping widgets and move them. This may need several iterations.
this.shiftWidgetsFromRegion(widgetRegion, widget);
done = this.getOverlappingWidgets(widgetRegion, widget).length === 0;
if (done) {
break;
}
}
if (!done) {
throw new Error('Unable to resolve overlapping widgets!');
}
this.shiftWidgetsUp();
// clear the action and origin widget once we are done
this._actionWidget = undefined;
this._widgetOrigin = undefined;
}
/**
* Move any widgets which intersect with the given dashboard region.
*/
shiftWidgetsFromRegion(region, actionWidget, validatePosition) {
const widgetsToMove = this.getOverlappingWidgets(region, actionWidget);
// if no widgets need moved then we can stop here
if (widgetsToMove.length === 0) {
return;
}
// create a duplicate we can use to keep track of which have been moved
const unmovedWidgets = widgetsToMove.slice();
// attempt to move any widgets to the previous widget position
widgetsToMove.forEach(widget => {
// get a grid off all occupied spaces - taking into account the placeholder and ignoring widgets that need moved
const grid = this.getOccupiedSpaces().filter(space => !unmovedWidgets.find(wgt => wgt === space.widget));
// iterate each free block
for (let row = this._widgetOrigin.row; row < this._widgetOrigin.row + this._widgetOrigin.rowSpan; row++) {
for (let column = this._widgetOrigin.column; column < this._widgetOrigin.column + this._widgetOrigin.columnSpan; column++) {
// determine if the block can fit in this space
const requiredSpaces = this.getRequiredSpacesFromPoint(widget, column, row);
// check if widget would fit in space
const available = requiredSpaces.every(space => {
return (!grid.find(gridSpace => gridSpace.column === space.column && gridSpace.row === space.row) && space.column < this.getColumnCount());
});
if (available) {
widget.setColumn(column);
widget.setRow(row);
unmovedWidgets.splice(unmovedWidgets.findIndex(wgt => wgt === widget), 1);
return;
}
}
}
// if we get to here then we can't simply swap the positions - next try moving right
if (this.canWidgetMoveRight(widget, true)) {
// after the shift check if placeholder position is still valid
validatePosition?.(ActionDirection.Right);
return;
}
// next try moving left
if (this.canWidgetMoveLeft(widget, true)) {
// after the shift check if placeholder position is still valid
validatePosition?.(ActionDirection.Left);
return;
}
// determine the distance that the widget needs to be moved down
const distance = this._actionWidget.widget.getRow() -
widget.getRow() +
this._actionWidget.widget.getRowSpan();
// as a last resort move the widget downwards
this.moveWidgetDown(widget, distance);
});
}
/**
* When dragging any widgets that need to be moved should be moved to an appropriate position
*/
shiftWidgets() {
const placeholder = this.placeholder$.getValue();
this.shiftWidgetsFromRegion(placeholder, this._actionWidget.widget, this.validatePlaceholderPosition.bind(this));
}
/**
* After shifts have taken place we should verify the place holder position is still valid
* @param shiftDirection - the position widgets were shifted
*/
validatePlaceholderPosition(shiftDirection) {
const placeholder = this.placeholder$.getValue();
// check if the placeholder is over a widget
if (this.getWidgetsAtPosition(placeholder.column, placeholder.row, true).length > 0) {
// move the placeholder the opposite direction
switch (shiftDirection) {
case ActionDirection.Left:
this.setPlaceholderBounds(placeholder.visible, placeholder.x + this.getColumnWidth(), placeholder.y, placeholder.width, placeholder.height);
break;
case ActionDirection.Right:
this.setPlaceholderBounds(placeholder.visible, placeholder.x - this.getColumnWidth(), placeholder.y, placeholder.width, placeholder.height);
break;
}
// validate this new position again
this.validatePlaceholderPosition(shiftDirection);
}
}
/**
* Determine if a widget can be moved left - or if it can move the widgets to the right to make space for the widget
*/
canWidgetMoveLeft(widget, performMove = false, shift = 0) {
const actionWgt = this._widgetOrigin;
let colShift = shift;
// check if the widget is the action widget or occupies the first column
if (widget === this._actionWidget.widget || widget.getColumn() === 0) {
return false;
}
// if value has been provided skip this step
if (colShift === 0) {
// work out how far the widget is planning to move left
if (actionWgt.row !== widget.getRow()) {
// if the widgets aren't on the same row work out the difference
if (actionWgt.column === widget.getColumn()) {
// if the widgets occupy the same column then shift the widget of the action widget
colShift = actionWgt.columnSpan;
}
else {
// else work out the exact number of spaces it will move left
const widgetDifference = actionWgt.column - widget.getColumn();
colShift = widget.getColumnSpan() - widgetDifference;
}
}
else {
// if they are on the same row then move one row
colShift = 1;
}
}
if (isNaN(colShift) || colShift === 0) {
return;
}
// find the positions required
const targetSpaces = this.getOccupiedSpaces()
.filter(space => space.widget === widget)
.map(space => {
return { column: space.column - colShift, row: space.row, widget: space.widget };
});
// check if any of the target spaces are out of bounds
if (targetSpaces.find(space => space.column < 0)) {
return false;
}
// check if there are widget in the required positions and if so, can they move right?
const moveable = targetSpaces.every(space => this.getWidgetsAtPosition(space.column, space.row)
.filter(wgt => wgt !== space.widget)
.every(wgt => this.canWidgetMoveLeft(wgt, false, colShift)));
if (performMove && moveable) {
// move all widgets to the left
targetSpaces.forEach(space => this.getWidgetsAtPosition(space.column, space.row)
.filter(wgt => wgt !== space.widget)
.forEach(wgt => this.canWidgetMoveLeft(wgt, true, colShift)));
// find the target column
const column = targetSpaces.reduce((target, space) => Math.min(target, space.column), Infinity);
// move current widget to the left
if (column !== Infinity) {
widget.setColumn(column);
}
}
return moveable;
}
/**
* Determine if a widget can be moved right - or if it can move the widgets to the right to make space for the widget
*/
canWidgetMoveRight(widget, performMove = false, shift = 0) {
const actionWgt = this._widgetOrigin;
let colShift = shift;
// check if the widget is the dragging widget or the widget occupies the final column
if (widget === this._actionWidget.widget ||
widget.getColumn() + widget.getColumnSpan() === this.options.columns) {
return false;
}
// if value has been provided skip this step
if (colShift === 0) {
// work out how far the widget is planning to move right
if (actionWgt.row !== widget.getRow()) {
// if the widgets aren't on the same row work out the difference
if (actionWgt.column === widget.getColumn()) {
// if the widgets occupy the same column then shift the widget of the action widget
colShift = actionWgt.columnSpan;
}
else {
// else work out the exact number of spaces it will move right
const widgetDifference = widget.getColumn() - actionWgt.column;
colShift = actionWgt.columnSpan - widgetDifference;
}
}
else {
// if they are on the same row then move one row
colShift = 1;
}
}
if (isNaN(colShift) || colShift === 0) {
return;
}
// find the positions required
const targetSpaces = this.getOccupiedSpaces()
.filter(space => space.widget === widget)
.map(space => {
return { column: space.column + colShift, row: space.row, widget: space.widget };
});
// check if any of the target spaces are out of bounds
if (targetSpaces.find(space => space.column >= this.getColumnCount())) {
return false;
}
// check if there are widget in the required positions and if so, can they move right?
const moveable = targetSpaces.every(space => this.getWidgetsAtPosition(space.column, space.row)
.filter(wgt => wgt !== space.widget)
.every(wgt => this.canWidgetMoveRight(wgt, false, colShift)));
if (performMove && moveable) {
// move all widgets to the right
targetSpaces.forEach(space => this.getWidgetsAtPosition(space.column, space.row)
.filter(wgt => wgt !== space.widget)
.forEach(wgt => this.canWidgetMoveRight(wgt, true, colShift)));
// move current widget to the right
widget.setColumn(widget.getColumn() + colShift);
}
return moveable;
}
/**
* Store the initial position of the widget being dragged
*/
setWidgetOrigin() {
this._widgetOrigin = {
column: this._actionWidget.widget.getColumn(),
row: this._actionWidget.widget.getRow(),
columnSpan: this._actionWidget.widget.getColumnSpan(),
rowSpan: this._actionWidget.widget.getRowSpan(),
};
}
/**
* Calculate all the required positions is a widget was to be positioned at a particular point
*/
getRequiredSpacesFromPoint(widget, column, row) {
const spaces = [];
for (let y = row; y < row + widget.getRowSpan(); y++) {
for (let x = column; x < column + widget.getColumnSpan(); x++) {
spaces.push({ column: x, row: y, widget });
}
}
return spaces;
}
/**
* Position widgets based on the position of the placeholder - this is temporary until confirmed
*/
updateWidgetPositions(widget) {
const placeholder = this.placeholder$.getValue();
// check all spaces the placeholder will occupy and move any widget currently in them down
for (let column = placeholder.column; column < placeholder.column + placeholder.columnSpan; column++) {
for (let row = placeholder.row; row < placeholder.row + placeholder.rowSpan; row++) {
this.getWidgetsAtPosition(column, row, true)
.filter(wgt => wgt !== widget)
.forEach(wgt => this.moveWidgetDown(wgt));
}
}
// update the height of the dashboard
this.setDashboardHeight();
// if we arent dragging the top handle then fill spaces
if (this._actionWidget.direction !== ActionDirection.Top &&
this._actionWidget.direction !== ActionDirection.TopLeft &&
this._actionWidget.direction !== ActionDirection.TopRight) {
this.shiftWidgetsUp();
}
}
/**
* Determine if a widget is occupying a specific row and column
* @param column The columns to check if occupied
* @param row The row to check if occupied
* @param ignoreResizing Whether or not to ignore the widget currently being resized
*/
getWidgetsAtPosition(column, row, ignoreResizing = false) {
return this.getOccupiedSpaces()
.filter(space => space.column === column && space.row === row)
.filter(space => (this._actionWidget && space.widget !== this._actionWidget.widget) || !ignoreResizing)
.map(space => space.widget);
}
/**
* Update the placeholder visibility, position and size
*/
setPlaceholderBounds(visible, x, y, width, height) {
const placeholder = this.placeholder$.getValue();
placeholder.visible = visible;
placeholder.column = this.getPlaceholderColumn(x, width);
placeholder.row = this.getPlaceholderRow(y, height);
placeholder.columnSpan = this.getPlaceholderColumnSpan(width);
placeholder.rowSpan = this.getPlaceholderRowSpan(height);
// calculate the maximum number of rows
const rowCount = this.widgets
.filter(widget => widget !== this._actionWidget.widget)
.reduce((previous, widget) => Math.max(widget.getRow() + widget.getRowSpan(), previous), 0);
// constrain maximum placeholder row
placeholder.row = Math.min(placeholder.row, rowCount);
placeholder.x = placeholder.column * this.getColumnWidth() + this.options.padding;
placeholder.y = placeholder.row * this._rowHeight + this.options.padding;
placeholder.width = placeholder.columnSpan * this.getColumnWidth() - this.options.padding * 2;
placeholder.height = placeholder.rowSpan * this._rowHeight - this.options.padding * 2;
// set the values of the widget to match the values of the placeholder - however do not render the changes
this._actionWidget.widget.setColumn(placeholder.column, false);
this._actionWidget.widget.setRow(placeholder.row, false);
this._actionWidget.widget.setColumnSpan(placeholder.columnSpan, false);
this._actionWidget.widget.setRowSpan(placeholder.rowSpan, false);
// update the placeholder
this.placeholder$.next(placeholder);
}
/**
* Get the placeholder column position
*/
getPlaceholderColumn(x, width) {
const column = this.getColumnFromPx(x, this._actionWidget.direction === ActionDirection.Move
? Rounding.RoundUpOverHalf
: Rounding.RoundDown);
const columnSpan = Math.floor(width / this.getColumnWidth());
const upperLimit = this.getColumnCount() - columnSpan;
// if we arent dragging left then just return the column
if (this._actionWidget.direction !== ActionDirection.Left &&
this._actionWidget.direction !== ActionDirection.TopLeft &&
this._actionWidget.direction !== ActionDirection.BottomLeft) {
return Math.max(Math.min(column, upperLimit), 0);
}
// get any overflow
const overflow = width % this.getColumnWidth();
return x <= 0 || overflow === 0 || columnSpan === 0 || overflow > this.getColumnWidth() / 2
? Math.max(Math.min(column, upperLimit), 0)
: Math.max(Math.min(column + 1, upperLimit), 0);
}
/**
* Get the column span of the placeholder
*/
getPlaceholderColumnSpan(width) {
const columnSpan = this.getColumnFromPx(width);
// if we arent dragging right or left then just return the column span
if (this._actionWidget.direction !== ActionDirection.Right &&
this._actionWidget.direction !== ActionDirection.TopRight &&
this._actionWidget.direction !== ActionDirection.BottomRight &&
this._actionWidget.direction !== ActionDirection.Left &&
this._actionWidget.direction !== ActionDirection.TopLeft &&
this._actionWidget.direction !== ActionDirection.BottomLeft) {
return this.getColumnFromPx(width, Rounding.RoundUpOverHalf);
}
// get the current column span and any overflow
const overflow = width % this.getColumnWidth();
return columnSpan > 0 && overflow > this.getColumnWidth() / 2
? Math.max(columnSpan + 1, 1)
: Math.max(columnSpan, 1);
}
/**
* Get the row position of the placeholder
*/
getPlaceholderRow(y, height) {
const row = this.getRowFromPx(y, this._actionWidget.direction === ActionDirection.Move
? Rounding.RoundUpOverHalf
: Rounding.RoundDown);
const rowSpan = Math.ceil(height / this._rowHeight);
// if we arent dragging up then just return the row
if (this._actionWidget.direction !== ActionDirection.Top &&
this._actionWidget.direction !== ActionDirection.TopLeft &&
this._actionWidget.direction !== ActionDirection.TopRight) {
return Math.max(row, 0);
}
// get any overflow
const overflow = height < this._rowHeight ? 0 : height % this._rowHeight;
return y <= 0 || rowSpan === 0 || overflow === 0 || overflow > this._rowHeight / 2
? Math.max(row, 0)
: Math.max(row + 1, 0);
}
/**
* Get the row span of the placeholder
*/
getPlaceholderRowSpan(height) {
const rowSpan = this.getRowFromPx(height);
// if we arent dragging up or down then just return the column span
if (this._actionWidget.direction !== ActionDirection.Top &&
this._actionWidget.direction !== ActionDirection.TopLeft &&
this._actionWidget.direction !== ActionDirection.TopRight &&
this._actionWidget.direction !== ActionDirection.Bottom &&
this._actionWidget.direction !== ActionDirection.BottomLeft &&
this._actionWidget.direction !== ActionDirection.BottomRight) {
return Math.max(rowSpan, 1);
}
// get the current column span and any overflow
const overflow = height % this._rowHeight;
return overflow > this._rowHeight / 2 ? Math.max(rowSpan + 1, 1) : Math.max(rowSpan, 1);
}
getColumnFromPx(x, rounding = Rounding.RoundDown) {
const column = Math.floor(x / Math.floor(this.getColumnWidth()));
const overflow = x % Math.floor(this.getColumnWidth());
const half = this.getColumnWidth() / 2;
switch (rounding) {
case Rounding.RoundDown:
return column;
case Rounding.RoundDownBelowHalf:
return overflow < half ? column : column + 1;
case Rounding.RoundUpOverHalf:
return overflow > half ? column + 1 : column;
case Rounding.RoundUp:
return overflow > 0 ? column + 1 : column;
}
}
getRowFromPx(y, rounding = Rounding.RoundDown) {
const row = Math.floor(y / Math.floor(this._rowHeight));
const overflow = y % Math.floor(this._rowHeight);
const half = this._rowHeight / 2;
switch (rounding) {
case Rounding.RoundDown:
return row;
case Rounding.RoundDownBelowHalf:
return overflow < half ? row : row + 1;
case Rounding.RoundUpOverHalf:
return overflow > half ? row + 1 : row;
case Rounding.RoundUp:
return overflow > 0 ? row + 1 : row;
}
}
commitWidgetChanges() {
const placeholder = this.placeholder$.getValue();
// check that we have all the values we need
if (placeholder.column === undefined ||
placeholder.row === undefined ||
placeholder.columnSpan === undefined ||
placeholder.rowSpan === undefined) {
return;
}
if (this._actionWidget) {
this._actionWidget.widget.setColumn(placeholder.column);
this._actionWidget.widget.setRow(placeholder.row);
this._actionWidget.widget.setColumnSpan(placeholder.columnSpan);
this._actionWidget.widget.setRowSpan(placeholder.rowSpan);
}
// reset all placeholder values
placeholder.column = undefined;
placeholder.row = undefined;
placeholder.columnSpan = undefined;
placeholder.rowSpan = undefined;
// emit the new placeholder values
this.placeholder$.next(placeholder);
}
/**
* Get the current column width
*/
getColumnWidth() {
return Math.floor(this.columnWidth);
}
/**
* Calculate the number of rows populated with widgets
*/
getRowCount() {
return this.widgets.reduce((previous, widget) => Math.max(widget.getRow() + widget.getRowSpan(), previous), 0);
}
/**
* Set the height of the dashboard container element
*/
setDashboardHeight() {
// size the dashboard container to ensure all rows fit
let rowCount = this.getRowCount();
// if we should show an empty row increment the row count by 1
if (this.options.emptyRow) {
rowCount++;
}
this.setDimensions(undefined, rowCount * this._rowHeight);
}
/**
* Orders the z-index of all widgets to move the active one to the front
* @param widget The widget that should be brought to the front
*/
bringToFront(widget) {
this.widgets.forEach(_widget => _widget === widget ? _widget.bringToFront() : _widget.sendToBack());
}
/**
* Move a widget down - if widgets are in the position below, then move them down further
* @param widget The widget to move downwards
*/
moveWidgetDown(widget, distance = 1) {
// stop if a negative number is passed through
if (distance < 0) {
return;
}
// move the widget down one position
widget.setRow(widget.getRow(DashboardStackMode.Auto) + distance);
// check every space the widget occupies for collisions
this.forEachBlock(widget, (column, row) => this.getWidgetsAtPosition(column, row, true)
.filter(wgt => wgt !== widget)
.forEach(wgt => this.moveWidgetDown(wgt, distance)));
}
/**
* Widgets should not be allowed to have a vacant space above them - if there is one they should move upwards to fill it
*/
shiftWidgetsUp() {
// check whether or not changes have been made - if so we need to repeat until stable
let stable = true;
// iterate each widget and
this.widgets.forEach(widget => {
const widgetIsOnTopRow = widget.getRow() === 0;
const widgetIsBeingResized = this._actionWidget?.widget === widget;
const widgetShouldBeAutoPositioned = widget.autoPositioning || this.stacked;
const widgetIsBeingMoved = !widgetShouldBeAutoPositioned && this.isDragging$.value?.id === widget.id;
if (widgetIsOnTopRow ||
widgetIsBeingResized ||
widgetIsBeingMoved ||
(!widgetShouldBeAutoPositioned && !this._cache)) {
return;
}
if (!widgetShouldBeAutoPositioned) {
const cachedVersionOfWidget = this._cache.find(cachedWidget => cachedWidget.id === widget.id);
const isPreviousPositionAvailable = this.getPositionAvailable(cachedVersionOfWidget.column, cachedVersionOfWidget.row, cachedVersionOfWidget.columnSpan, cachedVersionOfWidget.rowSpan, widget);
if (isPreviousPositionAvailable && widget.row !== cachedVersionOfWidget.row) {
widget.setRow(cachedVersionOfWidget.row);
stable = false;
}
return;
}
if (this.getPositionAvailable(widget.getColumn(), widget.getRow() - 1, widget.getColumnSpan(), 1)) {
widget.setRow(widget.getRow() - 1);
stable = false;
}
});
// if changes occurred then we should repeat the process
if (!stable) {
this.shiftWidgetsUp();
return true;
}
return false;
}
/**
* Iterate over each space a widget occupied
* @param widget The widget to determine spaces
* @param callback The function to be called for each space, should expect a column and row argument witht he context being the widget
*/
forEachBlock(widget, callback) {
for (let row = widget.getRow(); row < widget.getRow() + widget.getRowSpan(); row++) {
for (let column = widget.getColumn(); column < widget.getColumn() + widget.getColumnSpan(); column++) {
callback.call(widget, column, row);
}
}
}
getWidgetBelow(widget) {
const target = this.getWidgetsAtPosition(widget.getColumn(), widget.getRow() + widget.getRowSpan(), true);
return target.length > 0 ? target[0] : null;
}
/**
* Returns the number of columns available
*/
getColumnCount() {
return this.stacked ? 1 : this.options.columns;
}
onShiftStart(widget) {
this.onDragStart({ direction: ActionDirection.Move, widget });
}
/** Programmatically move a widget in a given direction */
onShift(widget, direction) {
// get the current mouse position
let deltaX = 0, deltaY = 0;
// move based on the direction
switch (direction) {
case ActionDirection.Top:
deltaY = -this.getRowHeight();
break;
case ActionDirection.Right:
deltaX = this.getColumnWidth();
break;
case ActionDirection.Bottom: {
deltaY = this.getRowHeight();
break;
}
case ActionDirection.Left:
deltaX = -this.getColumnWidth();
break;
}
const dimensions = {
x: widget.x + deltaX,
y: widget.y + deltaY,
width: widget.width,
height: widget.height,
};
// update placeholder position and value
this.setPlaceholderBounds(false, dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// update widget position
const { x, y } = this.placeholder$.value;
// move the widget to the placeholder position
widget.setBounds(x - this.options.padding, y - this.options.padding, dimensions.width, dimensions.height);
// update the height of the dashboard
this.setDashboardHeight();
}
onShiftEnd() {
// show the widget positions if the current positions and sizes were to persist
this.shiftWidgets();
// the height of the dashboard may have changed after moving widgets
this.setDashboardHeight();
// reset all properties
this.onDragEnd();
}
/** Programmatically resize a widget in a given direction */
onResize(widget, direction) {
// perform the resizing
let deltaX = 0, deltaY = 0;
// move based on the direction
switch (direction) {
case ActionDirection.Top:
deltaY = -this.getRowHeight();
break;
case ActionDirection.Right:
deltaX = this.getColumnWidth();
break;
case ActionDirection.Bottom:
deltaY = this.getRowHeight();
break;
case ActionDirection.Left:
deltaX = -this.getColumnWidth();
break;
}
const dimensions = {
x: widget.x,
y: widget.y,
width: widget.width + deltaX,
height: widget.height + deltaY,
};
const currentWidth = widget.x + widget.width;
const currentHeight = widget.y + widget.height;
// ensure values are within the dashboard bounds
if (dimensions.x < 0) {
dimensions.x = 0;
dimensions.width = currentWidth;
}
if (dimensions.y < 0) {
dimensions.y = 0;
dimensions.height = currentHeight;
}
if (dimensions.x + dimensions.width > this.getColumnWidth() * this.getColumnCount()) {
dimensions.width = widget.width;
}
// if the proposed width is smaller than allowed then reset width to minimum and ignore x changes
if (dimensions.width < this.getColumnWidth()) {
dimensions.x = widget.x;
dimensions.width = this.getColumnWidth();
}
// if the proposed height is smaller than allowed then reset height to minimum and ignore y changes
if (dimensions.height < this.getRowHeight()) {
dimensions.y = widget.y;
dimensions.height = this.getRowHeight();
}
const minWidth = this.getColumnWidth() * widget.minColSpan;
const isBelowMinWidth = dimensions.width < minWidth;
if (isBelowMinWidth) {
dimensions.x = widget.x;
dimensions.width = minWidth;
}
const minHeight = this.options.rowHeight * widget.minRowSpan;
const isBelowMinHeight = dimensions.height < minHeight;
if (isBelowMinHeight) {
dimensions.y = widget.y;
dimensions.height = minHeight;
}
// move the widget to the placeholder position
widget.setBounds(dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// update placeholder position and value
this.setPlaceholderBounds(false, dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// the height of the dashboard may have changed after moving widgets
this.setDashboardHeight();
}
getSurroundingWidgets(widget, direction) {
let widgets = [];
for (let column = widget.getColumn(); column < widget.getColumn() + widget.getColumnSpan(); column++) {
switch (direction) {
case ActionDirection.Top:
widgets = [...widgets, ...this.getWidgetsAtPosition(column, widget.getRow() - 1)];
break;
case ActionDirection.Bottom:
widgets = [
...widgets,
...this.getWidgetsAtPosition(column, widget.getRow() + widget.getRowSpan()),
];
break;
}
}
return widgets;
}
moveWidget(widget, offsetX, offsetY) {
const dimensions = {
x: widget.x + offsetX,
y: widget.y + offsetY,
width: widget.width,
height: widget.height,
};
this.restoreWidgets(true);
// update widget position
widget.setBounds(dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// update placeholder position and value
this.setPlaceholderBounds(true, dimensions.x, dimensions.y, dimensions.width, dimensions.height);
// show the widget positions if the current positions and sizes were to persist
this.shiftWidgets();
this.setDashboardHeight();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
const defaultOptions = {
columns: 5,
padding: 5,
minWidth: 100,
minHeight: 100,
emptyRow: true,
};
var ActionDirection;
(function (ActionDirection) {
ActionDirection[ActionDirection["Top"] = 0] = "Top";
ActionDirection[ActionDirection["TopRight"] = 1] = "TopRight";
ActionDirection[ActionDirection["Right"] = 2] = "Right";
ActionDirection[ActionDirection["BottomRight"] = 3] = "BottomRight";
ActionDirection[ActionDirection["Bottom"] = 4] = "Bottom";
ActionDirection[ActionDirection["BottomLeft"] = 5] = "BottomLeft";
ActionDirection[ActionDirection["Left"] = 6] = "Left";
ActionDirection[ActionDirection["TopLeft"] = 7] = "TopLeft";
ActionDirection[ActionDirection["Move"] = 8] = "Move";
})(ActionDirection || (ActionDirection = {}));
var Rounding;
(function (Rounding) {
Rounding[Rounding["RoundDown"] = 0] = "RoundDown";
Rounding[Rounding["RoundDownBelowHalf"] = 1] = "RoundDownBelowHalf";
Rounding[Rounding["RoundUp"] = 2] = "RoundUp";
Rounding[Rounding["RoundUpOverHalf"] = 3] = "RoundUpOverHalf";
})(Rounding || (Rounding = {}));
class DashboardWidgetComponent {
/** Defines the column the widget is placed in */
set col(col) {
if (col !== null && col !== undefined) {
this.setColumn(coerceNumberProperty(col));
this.dashboardService.renderDashboard();
}
}
get col() {
return this.getColumn();
}
/** Defines the row the widget is placed in */
set row(row) {
if (row !== null && row !== undefined) {
this.setRow(coerceNumberProperty(row));
this.dashboardService.renderDashboard();
}
}
get row() {
return this.getRow();
}
/** Defines the number of columns this widget should occupy. */
get colSpan() {
return this.getColumnSpan();
}
set colSpan(colSpan) {
if (colSpan !== null && colSpan !== undefined) {
this.setColumnSpan(coerceNumberProperty(colSpan));
}
}
/** Defines the number of rows this widget should occupy. */
get rowSpan() {
return this.getRowSpan();
}
set rowSpan(rowSpan) {
if (rowSpan !== null && rowSpan !== undefined) {
this.setRowSpan(coerceNumberProperty(rowSpan));
}
}
/** Defines the minimum number of columns this widget should occupy. */
get minColSpan() {
return this._minColSpan;
}
set minColSpan(minColumns) {
this._minColSpan = coerceNumberProperty(minColumns);
}
/** Defines the minimum number of rows this widget should occupy. */
get minRowSpan() {
return this._minRowSpan;
}
set minRowSpan(minRows) {
this._minRowSpan = coerceNumberProperty(minRows);
}
/** Defines whether or not this widget will be automatically repositioned */
set autoPositioning(autoPositioning) {
this._autoPositioning = coerceBooleanProperty(autoPositioning);
}
get autoPositioning() {
return this._autoPositioning;
}
constructor() {
this.dashboardService = inject(DashboardService);
/** Defines whether or not this widget can be resized. */
this.resizable = false;
/** Defines a function that returns an aria label for the widget */
this.widgetAriaLabel = this.getDefaultAriaLabel;
this.x = 0;
this.y = 0;
this.width = 100;
this.height = 100;
this.padding = 0;
this.zIndex = null;
this.role = 'group';
this.isDragging = false;
this.isGrabbing = false;
this.isResizing = false;
this.isDraggable = false;
this._column = { regular: undefined, stacked: undefined };
this._row = { regular: undefined, stacked: undefined };
this._columnSpan = { regular: 1, stacked: 1 };
this._rowSpan = { regular: 1, stacked: 1 };
this._minColSpan = 1;
this._minRowSpan = 1;
this._autoPositioning = true;
this._onDestroy = new Subject();
// subscribe to option changes
this.dashboardService.options$.pipe(takeUntil(this._onDestroy)).subscribe(() => this.update());
// every time the layout changes we want to update the aria label
this.dashboardService.layout$
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this.ariaLabel = this.getAriaLabel()));
// allow widget movements to be animated
this.dashboardService.isDragging$
.pipe(takeUntil(this._onDestroy), map(widget => widget === this))
.subscribe(isDragging => (this.isDragging = isDragging));
// allow widget movements to be animated
this.dashboardService.isGrabbing$
.pipe(takeUntil(this._onDestroy), map(widget => widget === this))
.subscribe(isGrabbing => (this.isGrabbing = isGrabbing));
}
ngOnInit() {
this._columnSpan.regular = this.colSpan;
this._rowSpan.regular = this.rowSpan;
this._rowSpan.stacked = this.rowSpan;
this._row.regular = this.row;
this._row.stacked = this.row;
this._column.regular = this.col;
this._column.stacked = this.col;
if (!this.id) {
console.warn('Dashboard Widget is missing an ID.');
// set random id - keeps things working but prevents exporting of positions
this.id = Math.floor(Math.random() * 100000).toString();
}
}
ngAfterViewInit() {
// add the widget to the dashboard
this.dashboardService.addWidget(this);
// apply the current options
this.update();
}
ngOnChanges(changes) {
if (this.hasPosition() && (changes.colSpan || changes.rowSpan)) {
this.dashboardService.resizeWidget(this);
this.dashboardService.renderDashboard();
}
}
/**
* If component is removed, then unregister it from the service
*/
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.dashboardService.removeWidget(this);
}
/**
* Apply the current dashboard options
*/
update() {
// get the current options at the time
const { padding, columns } = this.dashboardService.options;
this.padding = padding;
this._columnSpan.stacked = columns;
}
/**
* Set the actual position and size values
*/
render() {
this.x = this.getColumn() * this.dashboardService.getColumnWidth();
this.y = this.getRow() * this.dashboardService.getRowHeight();
this.width = this.getColumnSpan() * this.dashboardService.getColumnWidth();
this.height = this.getRowSpan() * this.dashboardService.getRowHeight();
}
getColumn(mode = DashboardStackMode.Auto) {
switch (mode) {
case DashboardStackMode.Auto:
return this.getStackableValue(this._column);
case DashboardStackMode.Regular:
return this._column.regular;
case DashboardStackMode.Stacked:
return this._column.stacked;
}
}
getRow(mode = DashboardStackMode.Auto) {
switch (mode) {
case DashboardStackMode.Auto:
return this.getStackableValue(this._row);
case DashboardStackMode.Regular:
return this._row.regular;
case DashboardStackMode.Stacked:
return this._row.stacked;
}
}
setColumn(column, render = true) {
this.setStackableValue(this._column, column);
if (render) {
this.render();
}
}
setRow(row, render = true) {
this.setStackableValue(this._row, row);
if (render) {
this.render();
}
}
getColumnSpan() {
return this.getStackableValue(this._columnSpan);
}
getRowSpan() {
return this.getStackableValue(this._rowSpan);
}
setColumnSpan(columnSpan, render = true) {
if (columnSpan >= this.minColSpan) {
this.setStackableValue(this._columnSpan, columnSpan);
if (render) {
this.render();
}
}
}
setRowSpan(rowSpan, render = true) {
if (rowSpan >= this.minRowSpan) {
this.setStackableValue(this._rowSpan, rowSpan);
if (render) {
this.render();
}
}
}
bringToFront() {
this.zIndex = 1;
}
sendToBack() {
this.zIndex = null;
}
setBounds(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
dragstart(handle, event, direction) {
this.isResizing = true;
this.dashboardService.isGrabbing$.next(null);
this.dashboardService.onResizeStart({ widget: this, direction, event, handle });
}
drag(handle, event, direction) {
this.dashboardService.onResizeDrag({ widget: this, direction, event, handle });
}
dragend() {
this.isResizing = false;
this.dashboardService.onResizeEnd();
}
getAriaLabel() {
if (this.widgetAriaLabel && typeof this.widgetAriaLabel === 'string') {
return this.widgetAriaLabel;
}
else if (this.widgetAriaLabel && typeof this.widgetAriaLabel === 'function') {
return this.widgetAriaLabel(this);
}
return this.ariaLabel;
}
getDefaultAriaLabel(widget) {
let options = '';
if (widget.resizable && widget.isDraggable) {
options = 'It can be moved and resized.';
}
else if (widget.resizable) {
options = 'It can be resized.';
}
else if (widget.isDraggable) {
options = 'It can be moved.';
}
return `${widget.name} panel in row ${widget.getRow()}, column ${widget.getColumn()}, is ${widget.getColumnSpan()} columns wide and ${widget.getRowSpan()} rows high. ${options}`;
}
/**
* Allows automatic setting of stackable value
* @param property The current StackableValue object
* @param value The value to set in the appropriate field
*/
setStackableValue(property, value) {
if (this.dashboardService.stacked) {
property.stacked = value;
}
else {
property.regular = value;
}
}
/**
* Return the appropriate value from a stackable value
* @param property The Stackable value object
*/
getStackableValue(property) {
return this.dashboardService.stacked ? property.stacked : property.regular;
}
/** Determine if the layout has been performed yet */
hasPosition() {
return this.getColumn() !== undefined && this.getRow() !== undefined;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DashboardWidgetComponent, isStandalone: false, selector: "ux-dashboard-widget", inputs: { id: "id", name: "name", col: "col", row: "row", colSpan: "colSpan", rowSpan: "rowSpan", minColSpan: "minColSpan", minRowSpan: "minRowSpan", resizable: "resizable", autoPositioning: "autoPositioning", widgetAriaLabel: "widgetAriaLabel", role: "role" }, host: { properties: { "style.left.px": "this.x", "style.top.px": "this.y", "style.width.px": "this.width", "style.height.px": "this.height", "style.padding.px": "this.padding", "style.z-index": "this.zIndex", "attr.aria-label": "this.ariaLabel", "attr.role": "this.role", "class.dragging": "this.isDragging", "class.grabbing": "this.isGrabbing", "class.resizing": "this.isResizing" } }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"widget-content widget-col-span-{{ getColumnSpan() }} widget-row-span-{{ getRowSpan() }}\"\n>\n <ng-content></ng-content>\n</div>\n\n<div\n uxDrag\n #handleTop\n class=\"resizer-handle handle-top\"\n (onDragStart)=\"dragstart(handleTop, $event, 0)\"\n (onDrag)=\"drag(handleTop, $event, 0)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [hidden]=\"!resizable\"\n></div>\n\n<div\n uxDrag\n #handleTopRight\n class=\"resizer-handle handle-top-right\"\n (onDragStart)=\"dragstart(handleTopRight, $event, 1)\"\n (onDrag)=\"drag(handleTopRight, $event, 1)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleRight\n class=\"resizer-handle handle-right\"\n (onDragStart)=\"dragstart(handleRight, $event, 2)\"\n (onDrag)=\"drag(handleRight, $event, 2)\"\n (onDragEnd)=\"dragend()\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleBottomRight\n class=\"resizer-handle handle-bottom-right\"\n (onDragStart)=\"dragstart(handleBottomRight, $event, 3)\"\n (onDrag)=\"drag(handleBottomRight, $event, 3)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleBottom\n class=\"resizer-handle handle-bottom\"\n (onDragStart)=\"dragstart(handleBottom, $event, 4)\"\n (onDrag)=\"drag(handleBottom, $event, 4)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [hidden]=\"!resizable\"\n></div>\n\n<div\n uxDrag\n #handleBottomLeft\n class=\"resizer-handle handle-bottom-left\"\n (onDragStart)=\"dragstart(handleBottomLeft, $event, 5)\"\n (onDrag)=\"drag(handleBottomLeft, $event, 5)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleLeft\n class=\"resizer-handle handle-left\"\n (onDragStart)=\"dragstart(handleLeft, $event, 6)\"\n (onDrag)=\"drag(handleLeft, $event, 6)\"\n (onDragEnd)=\"dragend()\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleTopLeft\n class=\"resizer-handle handle-top-left\"\n (onDragStart)=\"dragstart(handleTopLeft, $event, 7)\"\n (onDrag)=\"drag(handleTopLeft, $event, 7)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n", dependencies: [{ kind: "directive", type: DragDirective, selector: "[uxDrag]", inputs: ["clone", "group", "model", "draggable"], outputs: ["onDragStart", "onDrag", "onDragScroll", "onDragEnd", "onDrop", "onDropEnter", "onDropLeave"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardWidgetComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-dashboard-widget', standalone: false, template: "<div\n class=\"widget-content widget-col-span-{{ getColumnSpan() }} widget-row-span-{{ getRowSpan() }}\"\n>\n <ng-content></ng-content>\n</div>\n\n<div\n uxDrag\n #handleTop\n class=\"resizer-handle handle-top\"\n (onDragStart)=\"dragstart(handleTop, $event, 0)\"\n (onDrag)=\"drag(handleTop, $event, 0)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [hidden]=\"!resizable\"\n></div>\n\n<div\n uxDrag\n #handleTopRight\n class=\"resizer-handle handle-top-right\"\n (onDragStart)=\"dragstart(handleTopRight, $event, 1)\"\n (onDrag)=\"drag(handleTopRight, $event, 1)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleRight\n class=\"resizer-handle handle-right\"\n (onDragStart)=\"dragstart(handleRight, $event, 2)\"\n (onDrag)=\"drag(handleRight, $event, 2)\"\n (onDragEnd)=\"dragend()\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleBottomRight\n class=\"resizer-handle handle-bottom-right\"\n (onDragStart)=\"dragstart(handleBottomRight, $event, 3)\"\n (onDrag)=\"drag(handleBottomRight, $event, 3)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [style.right.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleBottom\n class=\"resizer-handle handle-bottom\"\n (onDragStart)=\"dragstart(handleBottom, $event, 4)\"\n (onDrag)=\"drag(handleBottom, $event, 4)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [hidden]=\"!resizable\"\n></div>\n\n<div\n uxDrag\n #handleBottomLeft\n class=\"resizer-handle handle-bottom-left\"\n (onDragStart)=\"dragstart(handleBottomLeft, $event, 5)\"\n (onDrag)=\"drag(handleBottomLeft, $event, 5)\"\n (onDragEnd)=\"dragend()\"\n [style.bottom.px]=\"padding\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleLeft\n class=\"resizer-handle handle-left\"\n (onDragStart)=\"dragstart(handleLeft, $event, 6)\"\n (onDrag)=\"drag(handleLeft, $event, 6)\"\n (onDragEnd)=\"dragend()\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n\n<div\n uxDrag\n #handleTopLeft\n class=\"resizer-handle handle-top-left\"\n (onDragStart)=\"dragstart(handleTopLeft, $event, 7)\"\n (onDrag)=\"drag(handleTopLeft, $event, 7)\"\n (onDragEnd)=\"dragend()\"\n [style.top.px]=\"padding\"\n [style.left.px]=\"padding\"\n [hidden]=\"!resizable || (dashboardService.stacked$ | async)\"\n></div>\n" }]
}], ctorParameters: () => [], propDecorators: { id: [{
type: Input
}], name: [{
type: Input
}], col: [{
type: Input
}], row: [{
type: Input
}], colSpan: [{
type: Input
}], rowSpan: [{
type: Input
}], minColSpan: [{
type: Input
}], minRowSpan: [{
type: Input
}], resizable: [{
type: Input
}], autoPositioning: [{
type: Input
}], widgetAriaLabel: [{
type: Input
}], x: [{
type: HostBinding,
args: ['style.left.px']
}], y: [{
type: HostBinding,
args: ['style.top.px']
}], width: [{
type: HostBinding,
args: ['style.width.px']
}], height: [{
type: HostBinding,
args: ['style.height.px']
}], padding: [{
type: HostBinding,
args: ['style.padding.px']
}], zIndex: [{
type: HostBinding,
args: ['style.z-index']
}], ariaLabel: [{
type: HostBinding,
args: ['attr.aria-label']
}], role: [{
type: HostBinding,
args: ['attr.role']
}, {
type: Input
}], isDragging: [{
type: HostBinding,
args: ['class.dragging']
}], isGrabbing: [{
type: HostBinding,
args: ['class.grabbing']
}], isResizing: [{
type: HostBinding,
args: ['class.resizing']
}] } });
class DashboardGrabHandleService {
constructor() {
this._dashboard = inject(DashboardService);
/** Self-registered drag handles in the dashboard. */
this._handles = [];
/** Automatically unsubscribe from all observables when destroyed */
this._onDestroy = new Subject();
// if a drag is performed by the mouse we should update the focusable item to be the first again
this._dashboard.layout$
.pipe(takeUntil(this._onDestroy), filter(() => !this._dashboard.isGrabbing$.value))
.subscribe(() => this.setFirstItemFocusable());
}
/** Perform unsubscriptions */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Register a new grab handle. */
addHandle(handle) {
this._handles = this.getHandlesInOrder([...this._handles, handle]);
// we want to make the first item focusable (raf to avoid expression changed error)
requestAnimationFrame(() => this.ensureFocusable());
}
/** Unregister a removed grab handle. */
removeHandle(handle) {
this._handles = this._handles.filter(h => h !== handle);
// Make sure there is still a focusable handle
this.ensureFocusable();
}
/** Make the first visual item in the list focusable */
setFirstItemFocusable() {
this.setItemFocus(0, false);
}
/** Set an item at a given index focused */
setItemFocus(index, focusElement = true) {
// if the list is empty then do nothing
if (!this._handles || this._handles.length === 0) {
return;
}
// check if the index is out of bounds
if (index < 0) {
return this.setItemFocus(0);
}
if (index > this._handles.length - 1) {
return this.setItemFocus(this._handles.length - 1);
}
// try focusing a specific index
this.getHandlesInOrder().forEach((handle, idx) => idx === index ? handle.focus(focusElement) : handle.blur());
// for safety we want to ensure one of the items is definitely still focusabled
this.ensureFocusable();
}
/** Focus the previous grab handle */
setPreviousItemFocus(handle) {
this.setItemFocus(this.getHandleIndex(handle) - 1);
}
/** Focus the next grab handle */
setNextItemFocus(handle) {
this.setItemFocus(this.getHandleIndex(handle) + 1);
}
/** Focus the grab handle on the widget above */
setSiblingItemFocus(widget, direction) {
// find all widgets that are directly above and have grab handles
const target = this._dashboard
.getSurroundingWidgets(widget, direction)
.map(_widget => this._handles.find(handle => handle.widget === _widget))
.filter(handle => !!handle)
.reduce((handle, current) => !handle || current.widget.getColumn() > handle.widget.getColumn() ? current : handle, null);
// ensure we have a target before focusing
if (!target) {
return;
}
// get the index of the target handle
const index = this.getHandleIndex(target);
// focus the item
this.setItemFocus(index);
}
/** Get handles in the order they appear rather than the order they are in the DOM */
getHandlesInOrder(handles = this._handles) {
const widgets = this._dashboard.getWidgetsByOrder();
// sort the handles according to the position of the widget it belongs to
return handles.sort((handleOne, handleTwo) => widgets.indexOf(handleOne.widget) - widgets.indexOf(handleTwo.widget));
}
getHandleIndex(handle) {
return this.getHandlesInOrder().findIndex(_handle => _handle === handle);
}
/** If the current focusable handle is removed we need to make another one focusable */
ensureFocusable() {
if (!this._handles.find(handle => handle.tabIndex === 0)) {
this.setFirstItemFocusable();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardGrabHandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardGrabHandleService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardGrabHandleService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class DashboardGrabHandleDirective {
constructor() {
this.widget = inject(DashboardWidgetComponent);
this._dashboard = inject(DashboardService);
this._handle = inject(DashboardGrabHandleService);
this._elementRef = inject(ElementRef);
this._announcer = inject(LiveAnnouncer);
/** Specify whether or not this handle can be used to perform moving */
this.uxGrabAllowMove = true;
/** Specify whether or not this handle can be used to perform resizing */
this.uxGrabAllowResize = true;
/** The aria label for the grab handle */
this.uxGrabAriaLabel = this.getDefaultAriaLabel.bind(this);
/** Customize the announcement that is made whenever an item has successfully been moved or resized */
this.uxGrabChangeSuccessAnnouncement = this.getChangeSuccessAnnouncement.bind(this);
/** Customize the announcement that is made whenever an item enters 'grab' mode */
this.uxGrabStartAnnouncement = this.getStartAnnouncement.bind(this);
/** Customize the announcement thqt is made whenever an item cannot be moved */
this.uxGrabMoveFailAnnouncement = this.getMoveFailAnnouncement.bind(this);
/** Customize the announcement thqt is made whenever an item cannot be resized */
this.uxGrabResizeFailAnnouncement = this.getResizeFailAnnouncement.bind(this);
/** Customize the announcement made whenever the moving/resizing is commited */
this.uxGrabConfirmAnnouncement = this.getConfirmAnnouncement.bind(this);
/** Customize the announcement made whenever the moving/resizing is cancelled */
this.uxGrabCancelAnnouncement = this.getCancellationAnnouncement.bind(this);
/** We must programmatically control the focus of the drag handles */
this.tabIndex = -1;
/** Store the current dragging state */
this.isGrabbing = false;
/** Emit when the directive is destroyed to unsubscribe from all observables */
this._onDestroy = new Subject();
if (!this.widget) {
throw new Error('uxDashboardGrabHandle must be used within a dashboard widget');
}
this._handle.addHandle(this);
// subscribe to changes to the current grab state
this._dashboard.isGrabbing$
.pipe(takeUntil(this._onDestroy), map(_widget => _widget === this.widget))
.subscribe(isGrabbing => (this.isGrabbing = isGrabbing));
}
/** Set the initial aria label and subscribe to layout changes */
ngOnInit() {
if (!this.widget.name) {
console.warn(`Dashboard widget ${this.widget.id} must have a valid 'name' to use uxDashboardGrabHandle`);
}
// set the initial aria label
this.ariaLabel = this.getAnnouncement(this.uxGrabAriaLabel);
// update the aria label when layout changes occur
this._dashboard.layout$
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this.ariaLabel = this.getAnnouncement(this.uxGrabAriaLabel)));
}
/** Unsubscribe from all observables */
ngOnDestroy() {
this._handle.removeHandle(this);
this._onDestroy.next();
this._onDestroy.complete();
}
/** Begin drag mode and cache initial state */
enableDragMode() {
if (!this.isGrabbing) {
// cache the widgets so we can restore when escape is pressed
this._cache = this._lastMovement = this._dashboard.cacheWidgets();
// store the current widget being grabbed
this._dashboard.isGrabbing$.next(this.widget);
this._dashboard.onShiftStart(this.widget);
// announce the grab start
this._announcer.announce(this.getAnnouncement(this.uxGrabStartAnnouncement));
}
}
/** Finish drag mode and commit the current state */
disableDragMode() {
if (this.isGrabbing) {
this._dashboard.isGrabbing$.next(null);
this._lastMovement = null;
this._dashboard.onShiftEnd();
// announce the confirmation
this._announcer.announce(this.getAnnouncement(this.uxGrabConfirmAnnouncement));
}
}
/** Finish the drag mode and restore the original state */
cancelDragMode() {
if (this.isGrabbing) {
this._dashboard.onShiftEnd();
this._dashboard.restoreWidgets(false, this._cache, true);
this._dashboard.setDashboardHeight();
this._dashboard.layout$.next(this._dashboard.getLayoutData());
this._dashboard.isGrabbing$.next(null);
// announce the cancellation
this._announcer.announce(this.getAnnouncement(this.uxGrabCancelAnnouncement));
}
}
/** Toggle the drag mode state */
toggleDragMode() {
this.isGrabbing ? this.disableDragMode() : this.enableDragMode();
}
/** Set the tab index and optionally focus the DOM element */
focus(focusElement = true) {
this.tabIndex = 0;
if (focusElement) {
this._elementRef.nativeElement.focus();
}
}
/** Make this item non-tabbable */
blur() {
this.tabIndex = -1;
}
/** When the grab handle loses focus then exit 'grab' mode */
onBlur() {
this.disableDragMode();
}
/** Handle key events */
onKeydown(event, key, ctrlKey) {
switch (key) {
case ESCAPE:
this.cancelDragMode();
break;
case SPACE:
case ENTER:
this.toggleDragMode();
event.preventDefault();
event.stopPropagation();
break;
case UP_ARROW:
case RIGHT_ARROW:
case DOWN_ARROW:
case LEFT_ARROW:
if (this.isGrabbing) {
ctrlKey ? this.resizeWidget(event, key) : this.moveWidget(event, key);
}
else {
this.moveFocus(event, key);
}
}
}
/** Get an announcement from the inputs - they may be a string or a function so handle both */
getAnnouncement(announcement, ...args) {
return typeof announcement === 'function' ? announcement(this.widget, ...args) : announcement;
}
/** Move the widget in a given direction based on arrow keys */
moveWidget(event, key) {
// check if moving is allowed
if (!this.widget.isDraggable || !this.uxGrabAllowMove) {
return;
}
// attempt to perform the move
this._dashboard.onShift(this.widget, this.getDirectionFromKey(key));
// get the announcable diff
const changes = this.getLayoutDiff();
// if there were changes then announce them
if (changes.length > 0) {
this._announcer.announce(this.getAnnouncement(this.uxGrabChangeSuccessAnnouncement, changes));
}
else {
this._announcer.announce(this.getAnnouncement(this.uxGrabMoveFailAnnouncement, this.getDirectionFromKey(key)));
}
this._lastMovement = this._dashboard.cacheWidgets();
event.preventDefault();
event.stopPropagation();
}
/** Resize the widgets accordingly based on the arrow keys */
resizeWidget(event, key) {
// check if resizing is allowed
if (!this.widget.resizable || !this.uxGrabAllowResize) {
return;
}
this._dashboard.onResize(this.widget, this.getDirectionFromKey(key));
// get the announcable diff
const changes = this.getLayoutDiff();
// if there were changes then announce them
if (changes.length > 0) {
this._announcer.announce(this.getAnnouncement(this.uxGrabChangeSuccessAnnouncement, changes));
}
else {
this._announcer.announce(this.getAnnouncement(this.uxGrabResizeFailAnnouncement, this.getDirectionFromKey(key)));
}
this._lastMovement = this._dashboard.cacheWidgets();
event.preventDefault();
event.stopPropagation();
}
/** Shift focus between the variour grab handles */
moveFocus(event, key) {
switch (key) {
case UP_ARROW:
this._handle.setSiblingItemFocus(this.widget, ActionDirection.Top);
break;
case RIGHT_ARROW:
this._handle.setNextItemFocus(this);
break;
case DOWN_ARROW:
this._handle.setSiblingItemFocus(this.widget, ActionDirection.Bottom);
break;
case LEFT_ARROW:
this._handle.setPreviousItemFocus(this);
break;
}
event.preventDefault();
event.stopPropagation();
}
/** Convert an arrow key code into an ActionDirection enum */
getDirectionFromKey(key) {
switch (key) {
case UP_ARROW:
return ActionDirection.Top;
case RIGHT_ARROW:
return ActionDirection.Right;
case DOWN_ARROW:
return ActionDirection.Bottom;
case LEFT_ARROW:
return ActionDirection.Left;
}
}
/** Supply the default grab handle aria label based on the provided constraints */
getDefaultAriaLabel(widget) {
if (widget.resizable && this.uxGrabAllowResize && widget.isDraggable && this.uxGrabAllowMove) {
return `Press space to move and resize the ${widget.name} panel.`;
}
else if (widget.resizable && this.uxGrabAllowResize) {
return `Press space to resize the ${widget.name} panel.`;
}
else if (widget.isDraggable && this.uxGrabAllowMove) {
return `Press space to move the ${widget.name} panel.`;
}
}
/** Get the default announcement whenever a movement or resize was successful */
getChangeSuccessAnnouncement() {
return `${this.getDiffAnnouncements().join(' ')} Use the cursor keys to continue moving and resizing, enter to commit, or escape to cancel.`;
}
getDiffAnnouncements() {
// map the differences to strings
return this.getLayoutDiff().map(diff => {
const changes = [];
// Handle movement strings
if (diff.isMovedHorizontally && diff.isMovedVertically) {
changes.push(`moved to row ${diff.currentRow}, column ${diff.currentColumn}`);
}
else if (diff.isMovedDown) {
changes.push(`moved down to row ${diff.currentRow}, column ${diff.currentColumn}`);
}
else if (diff.isMovedUp) {
changes.push(`moved up to row ${diff.currentRow}, column ${diff.currentColumn}`);
}
else if (diff.isMovedLeft) {
changes.push(`moved left to row ${diff.currentRow}, column ${diff.currentColumn}`);
}
else if (diff.isMovedRight) {
changes.push(`moved right to row ${diff.currentRow}, column ${diff.currentColumn}`);
}
// handle resize strings
if (diff.isResized) {
changes.push(`resized to ${diff.currentColumnSpan} columns wide and ${diff.currentRowSpan} rows high`);
}
return `${diff.widget.name} panel is ${changes.join(' and ')}.`;
});
}
/** Get the default announcement whenever a movement is not possible due to dashboard boundaries */
getMoveFailAnnouncement(widget, direction) {
switch (direction) {
case ActionDirection.Top:
return `Cannot move the ${widget.name} panel up, because it is at the top edge of the dashboard`;
case ActionDirection.Bottom:
return `Cannot move the ${widget.name} panel down, because it is at the bottom edge of the dashboard`;
case ActionDirection.Right:
return `Cannot move the ${widget.name} panel right, because it is at the right edge of the dashboard`;
case ActionDirection.Left:
return `Cannot move the ${widget.name} panel left, because it is at the left edge of the dashboard`;
}
}
/** Get the default announcement whenever a resize is not possible due to either widget constraints of dashboard bounds */
getResizeFailAnnouncement(widget, direction) {
switch (direction) {
case ActionDirection.Top:
return `Cannot make the ${widget.name} panel shorter, because it is currently at its minimum height.`;
case ActionDirection.Bottom:
return `Cannot make the ${widget.name} panel taller, because it is currently at its maximum height.`;
case ActionDirection.Right:
return `Cannot make the ${widget.name} panel wider, because it is at the right edge of the dashboard.`;
case ActionDirection.Left:
return `Cannot make the ${widget.name} panel narrower, because it is currently at its minimum width.`;
}
}
/** Get the default announcement whenever we enter 'grab' mode */
getStartAnnouncement(widget) {
if (widget.isDraggable && widget.resizable && this.uxGrabAllowMove && this.uxGrabAllowResize) {
return `${widget.name} panel is currently on row ${widget.getRow()}, column ${widget.getColumn()} and is ${widget.getColumnSpan()} columns wide and ${widget.getRowSpan()} rows high. Use the cursor keys to move the widget and the cursor keys with the control modifier to resize the widget. Press enter to commit changes and press escape to cancel changes.`;
}
else if (widget.isDraggable && this.uxGrabAllowMove) {
return `${widget.name} panel is currently on row ${widget.getRow()}, column ${widget.getColumn()}. Use the cursor keys to move the widget. Press enter to commit changes and press escape to cancel changes.`;
}
else if (widget.resizable && this.uxGrabAllowResize) {
return `${widget.name} panel is currently on row ${widget.getRow()}, column ${widget.getColumn()} and is ${widget.getColumnSpan()} columns wide and ${widget.getRowSpan()} rows high. Use the cursor keys with the control modifier to resize the widget. Press enter to commit changes and press escape to cancel changes.`;
}
}
/** Get the default announcement whenever grab mode is exited after a movement or resize */
getConfirmAnnouncement(widget) {
if (widget.isDraggable && widget.resizable && this.uxGrabAllowMove && this.uxGrabAllowResize) {
return `Moving and resizing complete. ${this.getDiffAnnouncements().join(' ')} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
else if (widget.isDraggable && this.uxGrabAllowMove) {
return `Moving complete. ${this.getDiffAnnouncements().join(' ')} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
else if (widget.resizable && this.uxGrabAllowResize) {
return `Resizing complete. ${this.getDiffAnnouncements().join(' ')} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
}
/** Get the default announcement whenever grab mode is exited after being cancelled */
getCancellationAnnouncement(widget) {
if (widget.isDraggable && widget.resizable && this.uxGrabAllowMove && this.uxGrabAllowResize) {
return `Moving and resizing cancelled. ${this.getDashboardAriaLabel()} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
else if (widget.isDraggable && this.uxGrabAllowMove) {
return `Moving cancelled. ${this.getDashboardAriaLabel()} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
else if (widget.resizable && this.uxGrabAllowResize) {
return `Resizing cancelled. ${this.getDashboardAriaLabel()} ${this.getAnnouncement(this.uxGrabAriaLabel)}`;
}
}
/** Get a description of all dashboard widgets, their positions and sizes */
getDashboardAriaLabel() {
return `Dashboard with ${this._dashboard.options.columns} columns, containing ${this._dashboard.widgets.length} panels. ${this._dashboard.widgets.map(this.getWidgetAriaLabel).join(' ')}`;
}
/** Get a description of a given widget */
getWidgetAriaLabel(widget) {
return `${widget.name} panel in row ${widget.getRow()}, column ${widget.getColumn()}, is ${widget.getColumnSpan()} columns wide and ${widget.getRowSpan()} rows high.`;
}
/** Get an object describing all the changes that have been made to all widgets since the last change */
getLayoutDiff() {
// find all changes
const diffs = this._dashboard.getLayoutData().map(layout => {
// get the most recent cache
const cache = this._lastMovement || this._cache;
// get the actual widget
const widget = this._dashboard.widgets.find(_widget => _widget.id === layout.id);
// get previous position
const previousLayout = cache.find(_widget => _widget.id === layout.id);
// ensure they are all numbers
layout.row = Number(layout.row);
layout.rowSpan = Number(layout.rowSpan);
layout.col = Number(layout.col);
layout.colSpan = Number(layout.colSpan);
previousLayout.row = Number(previousLayout.row);
previousLayout.rowSpan = Number(previousLayout.rowSpan);
previousLayout.column = Number(previousLayout.column);
previousLayout.columnSpan = Number(previousLayout.columnSpan);
return {
widget,
currentRow: layout.row,
currentColumn: layout.col,
currentRowSpan: layout.rowSpan,
currentColumnSpan: layout.colSpan,
previousColumn: previousLayout.column,
previousRow: previousLayout.row,
previousColumnSpan: previousLayout.columnSpan,
previousRowSpan: previousLayout.rowSpan,
isMovedLeft: layout.col < previousLayout.column,
isMovedRight: layout.col > previousLayout.column,
isMovedUp: layout.row < previousLayout.row,
isMovedDown: layout.row > previousLayout.row,
isMovedHorizontally: layout.col !== previousLayout.column,
isMovedVertically: layout.row !== previousLayout.row,
isMoved: layout.col !== previousLayout.column || layout.row !== previousLayout.row,
isResized: previousLayout.columnSpan !== layout.colSpan || previousLayout.rowSpan !== layout.rowSpan,
};
});
// get the order the widgets appear visually
const order = this._handle.getHandlesInOrder().map(handle => handle.widget);
// only return items that have been repositioned or resized
return diffs
.filter(diff => diff.isMoved || diff.isResized)
.sort((diffOne, diffTwo) => {
// sort this so that the item that the user moved is first in the list, and the remainder are in their new order as seen in the dashboard
if (diffOne.widget === this.widget) {
return -1;
}
if (diffTwo.widget === this.widget) {
return 1;
}
// otherwise sort based on their visual order
return order.indexOf(diffOne.widget) < order.indexOf(diffTwo.widget) ? -1 : 1;
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardGrabHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DashboardGrabHandleDirective, isStandalone: false, selector: "[uxDashboardGrabHandle]", inputs: { uxGrabAllowMove: "uxGrabAllowMove", uxGrabAllowResize: "uxGrabAllowResize", uxGrabAriaLabel: "uxGrabAriaLabel", uxGrabChangeSuccessAnnouncement: "uxGrabChangeSuccessAnnouncement", uxGrabStartAnnouncement: "uxGrabStartAnnouncement", uxGrabMoveFailAnnouncement: "uxGrabMoveFailAnnouncement", uxGrabResizeFailAnnouncement: "uxGrabResizeFailAnnouncement", uxGrabConfirmAnnouncement: "uxGrabConfirmAnnouncement", uxGrabCancelAnnouncement: "uxGrabCancelAnnouncement" }, host: { listeners: { "blur": "onBlur()", "keydown": "onKeydown($event,$event.which,$event.ctrlKey)" }, properties: { "attr.aria-label": "this.ariaLabel", "tabIndex": "this.tabIndex" } }, exportAs: ["ux-dashboard-grab-handle"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardGrabHandleDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxDashboardGrabHandle]',
exportAs: 'ux-dashboard-grab-handle',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxGrabAllowMove: [{
type: Input
}], uxGrabAllowResize: [{
type: Input
}], uxGrabAriaLabel: [{
type: Input
}], uxGrabChangeSuccessAnnouncement: [{
type: Input
}], uxGrabStartAnnouncement: [{
type: Input
}], uxGrabMoveFailAnnouncement: [{
type: Input
}], uxGrabResizeFailAnnouncement: [{
type: Input
}], uxGrabConfirmAnnouncement: [{
type: Input
}], uxGrabCancelAnnouncement: [{
type: Input
}], ariaLabel: [{
type: HostBinding,
args: ['attr.aria-label']
}], tabIndex: [{
type: HostBinding,
args: ['tabIndex']
}], onBlur: [{
type: HostListener,
args: ['blur']
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event', '$event.which', '$event.ctrlKey']]
}] } });
class DashboardComponent {
/** If defined or changed this will set the positions of the widgets within the dashboard. This is a two way binding that will be updated with the current layout when it changes. */
set layout(layout) {
if (layout) {
this.dashboardService.layout$.next(layout);
}
}
/** Configures the options for the dashboard, if an option is not specified the default value will be used. */
set options(options) {
this.dashboardService.options$.next({ ...defaultOptions, ...options });
}
constructor() {
this.dashboardService = inject(DashboardService);
this._changeDetector = inject(ChangeDetectorRef);
this.isGrabbing = false;
this.customAriaLabel = this.getDefaultAriaLabel;
/** Emits when layout has been changed. */
this.layoutChange = new EventEmitter();
this.role = 'region';
/** Ensure we unsubscribe from all observables */
this._onDestroy = new Subject();
this.dashboardService.layout$
.pipe(takeUntil(this._onDestroy), tap(() => (this.ariaLabel = this.getAriaLabel())))
.subscribe(() => this._changeDetector.markForCheck());
this.dashboardService.userLayoutChange$
.pipe(takeUntil(this._onDestroy))
.subscribe(data => this.layoutChange.emit(data));
// subscribe to changes to the grab mode
this.dashboardService.isGrabbing$
.pipe(takeUntil(this._onDestroy), map(widget => !!widget))
.subscribe(isGrabbing => (this.isGrabbing = isGrabbing));
}
/**
* Set the initial dimensions
*/
ngAfterViewInit() {
// set the initial dimensions
this.dashboardService.setDimensions(this.dashboardElement.nativeElement.offsetWidth, this.dashboardElement.nativeElement.offsetHeight);
}
ngAfterContentInit() {
this.dashboardService.initialized$.next(true);
}
ngOnChanges() {
this.dashboardService.renderDashboard();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onResize(event) {
this.dashboardService.setDimensions(event.width, event.height);
}
getAriaLabel() {
if (this.customAriaLabel && typeof this.customAriaLabel === 'string') {
return this.customAriaLabel;
}
else if (this.customAriaLabel && typeof this.customAriaLabel === 'function') {
return this.customAriaLabel(this.dashboardService.widgets, this.dashboardService.options);
}
return this.ariaLabel;
}
/** Shift widgets up where possible to fill any available space to optimize the dashboard layout */
refreshLayout() {
const didChangeLayout = this.dashboardService.shiftWidgetsUp();
if (didChangeLayout) {
// if widgets have shifted up the dashboard may no longer occupy the same
// height. We should remove any unneeded whitespace below widgets too.
this.dashboardService.setDashboardHeight();
// emit information about the layout
this.dashboardService.layout$.next(this.dashboardService.getLayoutData());
this.layoutChange.emit(this.dashboardService.layout$.value);
}
}
getDefaultAriaLabel(widgets, options) {
return `Dashboard with ${options.columns} columns, containing ${widgets.length} panels. ${widgets.map(this.getWidgetAriaLabel).join(' ')}`;
}
getWidgetAriaLabel(widget) {
return `${widget.name} panel in row ${widget.getRow()}, column ${widget.getColumn()}, is ${widget.getColumnSpan()} columns wide and ${widget.getRowSpan()} rows high.`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DashboardComponent, isStandalone: false, selector: "ux-dashboard", inputs: { customAriaLabel: ["aria-label", "customAriaLabel"], layout: "layout", options: "options", role: "role" }, outputs: { layoutChange: "layoutChange" }, host: { properties: { "attr.aria-label": "this.ariaLabel", "attr.role": "this.role" } }, providers: [DashboardService, DashboardGrabHandleService], queries: [{ propertyName: "handles", predicate: DashboardGrabHandleDirective, descendants: true }], viewQueries: [{ propertyName: "dashboardElement", first: true, predicate: ["dashboard"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div #dashboard class=\"dashboard-container\" [style.height.px]=\"dashboardService.height$ | async\">\n <div (uxResize)=\"onResize($event)\" [throttle]=\"16\" class=\"dashboard\">\n <ng-content></ng-content>\n </div>\n\n <!-- Wrap with ngIf so we only have one subscription rather than one for each property -->\n @if (dashboardService.placeholder$ | async; as placeholder) {\n @if (placeholder.visible) {\n <div\n class=\"position-indicator\"\n [style.left.px]=\"placeholder.x\"\n [style.top.px]=\"placeholder.y\"\n [style.width.px]=\"placeholder.width\"\n [style.height.px]=\"placeholder.height\"\n ></div>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: ResizeDirective, selector: "[uxResize]", inputs: ["throttle"], outputs: ["uxResize"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-dashboard', changeDetection: ChangeDetectionStrategy.OnPush, providers: [DashboardService, DashboardGrabHandleService], standalone: false, template: "<div #dashboard class=\"dashboard-container\" [style.height.px]=\"dashboardService.height$ | async\">\n <div (uxResize)=\"onResize($event)\" [throttle]=\"16\" class=\"dashboard\">\n <ng-content></ng-content>\n </div>\n\n <!-- Wrap with ngIf so we only have one subscription rather than one for each property -->\n @if (dashboardService.placeholder$ | async; as placeholder) {\n @if (placeholder.visible) {\n <div\n class=\"position-indicator\"\n [style.left.px]=\"placeholder.x\"\n [style.top.px]=\"placeholder.y\"\n [style.width.px]=\"placeholder.width\"\n [style.height.px]=\"placeholder.height\"\n ></div>\n }\n }\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { customAriaLabel: [{
type: Input,
args: ['aria-label']
}], layout: [{
type: Input
}], options: [{
type: Input
}], layoutChange: [{
type: Output
}], ariaLabel: [{
type: HostBinding,
args: ['attr.aria-label']
}], role: [{
type: HostBinding,
args: ['attr.role']
}, {
type: Input
}], dashboardElement: [{
type: ViewChild,
args: ['dashboard', { static: true }]
}], handles: [{
type: ContentChildren,
args: [DashboardGrabHandleDirective, { descendants: true }]
}] } });
class DashboardDragHandleDirective extends DragDirective {
constructor() {
super();
this.widget = inject(DashboardWidgetComponent);
this.dashboardService = inject(DashboardService);
// inform the widget that it can be dragged
this.widget.isDraggable = true;
this.onDragStart
.pipe(takeUntil(this._onDestroy), tap(() => this.dashboardService.isGrabbing$.next(null)))
.subscribe((event) => this.dashboardService.onDragStart({
widget: this.widget,
direction: ActionDirection.Move,
event,
}));
this.onDrag.pipe(takeUntil(this._onDestroy)).subscribe((event) => this.dashboardService.onDrag({
widget: this.widget,
direction: ActionDirection.Move,
event,
}));
this.onDragScroll
.pipe(takeUntil(this._onDestroy))
.subscribe((event) => this.dashboardService.onDragScroll(this.widget, event));
this.onDragEnd
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.dashboardService.onDragEnd());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDragHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DashboardDragHandleDirective, isStandalone: false, selector: "[uxDashboardWidgetDragHandle], [ux-dashboard-widget-drag-handle]", usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardDragHandleDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxDashboardWidgetDragHandle], [ux-dashboard-widget-drag-handle]',
standalone: false,
}]
}], ctorParameters: () => [] });
const DECLARATIONS$9 = [
DashboardComponent,
DashboardWidgetComponent,
DashboardDragHandleDirective,
DashboardGrabHandleDirective,
];
class DashboardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DashboardModule, declarations: [DashboardComponent,
DashboardWidgetComponent,
DashboardDragHandleDirective,
DashboardGrabHandleDirective], imports: [A11yModule, CommonModule, ResizeModule, DragModule], exports: [DashboardComponent,
DashboardWidgetComponent,
DashboardDragHandleDirective,
DashboardGrabHandleDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardModule, providers: [DashboardService], imports: [A11yModule, CommonModule, ResizeModule, DragModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DashboardModule, decorators: [{
type: NgModule,
args: [{
imports: [A11yModule, CommonModule, ResizeModule, DragModule],
exports: DECLARATIONS$9,
declarations: DECLARATIONS$9,
providers: [DashboardService],
}]
}] });
/**
* Convert a single dimension array to a double dimension array
* @param items the single dimension array to convert
* @param columns the number of items each array should have
*/
function gridify(items, columns) {
// create a copy of array so not to effect the original
items = items.slice(0);
const grid = [];
while (items.length) {
grid.push(items.splice(0, columns));
}
return grid;
}
/**
* Create an array of numbers between two limits
* @param start the lower limit
* @param end the upper limit
*/
function range(start, end) {
const list = [];
for (let idx = start; idx <= end; idx++) {
list.push(idx);
}
return list;
}
/**
* Create an array of dates between two points
* @param start the date to start the array
* @param end the date to end the array
*/
function dateRange(start, end) {
// don't alter the start date object
start = new Date(start);
const dates = [];
// loop through all the days between the date range
while (start <= end) {
// add the date to the array
dates.push(new Date(start));
// move to the next day
start.setDate(start.getDate() + 1);
}
return dates;
}
/**
* Compare two dates to see if they are on the same day
* @param day1 the first date to compare
* @param day2 the second date to compare
*/
function compareDays(day1, day2) {
return (day1.getDate() === day2.getDate() &&
day1.getMonth() === day2.getMonth() &&
day1.getFullYear() === day2.getFullYear());
}
/**
* Date comparison for use primarily with distinctUntilChanged
*/
function dateComparator(dateOne, dateTwo) {
if ((!dateOne && dateTwo) || (dateOne && !dateTwo)) {
return false;
}
if (!dateOne && !dateTwo) {
return true;
}
return dateOne.getTime() === dateTwo.getTime();
}
/**
* Calculate the number of days between two dates
* @param start The start date
* @param end The end date
* @param fullDay Whether or not we should take from 00:00 on the start date and 23:59 on the end date
*/
function differenceBetweenDates(start, end, fullDay = true) {
if (!start || !end) {
return null;
}
const millisecondsInDay = 86400000;
const startDay = new Date(start.getTime() < end.getTime() ? start : end);
const endDay = new Date(start.getTime() > end.getTime() ? start : end);
// get the start of day
if (fullDay) {
startDay.setHours(0, 0, 0, 0);
endDay.setHours(23, 59, 59, 0);
}
return Math.round((endDay.getTime() - startDay.getTime()) / millisecondsInDay);
}
/**
* Timezone comparison for use primarily with distinctUntilChanged
*/
function timezoneComparator(zoneOne, zoneTwo) {
return zoneOne.name === zoneTwo.name && zoneOne.offset === zoneTwo.offset;
}
/**
* Get a date object with the time of the start of the given day
* @param date The date to get the start of day
*/
function getStartOfDay(date) {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
return startOfDay;
}
function isDateAfter(date, after, isEqual = false) {
return isEqual
? getStartOfDay(date).getTime() >= getStartOfDay(after).getTime()
: getStartOfDay(date).getTime() > getStartOfDay(after).getTime();
}
function isDateBefore(date, before, isEqual = false) {
return isEqual
? getStartOfDay(date).getTime() <= getStartOfDay(before).getTime()
: getStartOfDay(date).getTime() < getStartOfDay(before).getTime();
}
/**
* Export an array of all the available months
*/
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const monthsShort = getShortMonthNames();
function getShortMonthNames() {
return months.map(month => month.substring(0, 3));
}
/**
* Export an array of all the available days of the week
*/
const weekdays = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
const weekdaysShort = getShortWeekdayNames();
const meridians = ['AM', 'PM'];
function getShortWeekdayNames() {
return weekdays.map(weekday => weekday.substring(0, 3));
}
/** Export the default set of time zone */
const timezones = [
{ name: 'GMT-11', offset: 660 },
{ name: 'GMT-10', offset: 600 },
{ name: 'GMT-9', offset: 540 },
{ name: 'GMT-8', offset: 480 },
{ name: 'GMT-7', offset: 420 },
{ name: 'GMT-6', offset: 360 },
{ name: 'GMT-5', offset: 300 },
{ name: 'GMT-4', offset: 240 },
{ name: 'GMT-3', offset: 180 },
{ name: 'GMT-2', offset: 120 },
{ name: 'GMT-1', offset: 60 },
{ name: 'GMT', offset: 0 },
{ name: 'GMT+1', offset: -60 },
{ name: 'GMT+2', offset: -120 },
{ name: 'GMT+3', offset: -180 },
{ name: 'GMT+4', offset: -240 },
{ name: 'GMT+5', offset: -300 },
{ name: 'GMT+6', offset: -360 },
{ name: 'GMT+7', offset: -420 },
{ name: 'GMT+8', offset: -480 },
{ name: 'GMT+9', offset: -540 },
{ name: 'GMT+10', offset: -600 },
{ name: 'GMT+11', offset: -660 },
{ name: 'GMT+12', offset: -720 },
];
class DateRangeService {
constructor() {
/** Indicate whether we want to show a date range */
this.isRange = false;
/** Specify the direction of the selection */
this.direction = DateRangePicker.Start;
/** Emit whenever the start date changes */
this.onStartChange = new Subject();
/** Emit whenever the end date changes */
this.onEndChange = new Subject();
/** Emit whenever the range has changed */
this.onRangeChange = new Subject();
/** Emit whenever the hover date changes */
this.onHoverChange = new Subject();
/** Emit whenever the range is cleared */
this.onClear = new Subject();
/** Indicate if we should show time */
this.showTime = false;
/** Defines the aria label for the range start picker */
this.startPickerAriaLabel = 'Selecting the start date';
/** Defines the aria label for the range end picker */
this.endPickerAriaLabel = 'Selecting the end date';
/** Indicate if we are currently changing the time */
this.isChangingTime = false;
/** Store the current start time */
this.startTime = { hours: 0, minutes: 0, seconds: 0 };
/** Store the current end time */
this.endTime = { hours: 23, minutes: 59, seconds: 59 };
}
setStartDate(date) {
// if the start date is after the end date the clear the end date
if (date && this.end && isDateAfter(date, this.end)) {
this.clear();
}
this.start = date;
this.onStartChange.next(this.start);
this.onRangeChange.next();
}
setEndDate(date) {
// if the end date is before the start date the clear the start date
if (date && this.start && isDateBefore(date, this.start)) {
this.clear();
}
this.end = date;
this.onEndChange.next(this.end);
this.onRangeChange.next();
}
clear() {
this.setStartDate(null);
this.setEndDate(null);
this.onClear.next();
}
setDateMouseEnter(date) {
this.hover = date;
this.onHoverChange.next();
}
setDateMouseLeave(date) {
if (date && this.hover && compareDays(date, this.hover)) {
this.setDateMouseEnter(null);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangeService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangeService, decorators: [{
type: Injectable
}] });
var DateRangePicker;
(function (DateRangePicker) {
DateRangePicker["Start"] = "start";
DateRangePicker["End"] = "end";
})(DateRangePicker || (DateRangePicker = {}));
/**
* This directive allows us to pass information down to a specific ux-date-time-picker
* without having to expose additional inputs to the consuming application.
*
* For example, the day picker needs to know if it is the start or end picker
* as the behavior will be different for each. However we don't want to expose an
* input on the DateTimePickerComponent as this will appear in Editor Autocomplete Suggestions
* options if the Angular Language Service is installed, and we don't want these to be public
* options.
*/
class DateRangeOptions {
constructor() {
this.picker = DateRangePicker.Start;
}
}
class DateRangePickerDirective {
constructor() {
this._options = inject(DateRangeOptions, { self: true });
}
/** Specify whether this is the start or end picker */
set picker(picker) {
this._options.picker = picker;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: DateRangePickerDirective, isStandalone: false, selector: "[uxDateRangePicker]", inputs: { picker: "picker" }, providers: [DateRangeOptions], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxDateRangePicker]',
providers: [DateRangeOptions],
standalone: false,
}]
}], propDecorators: { picker: [{
type: Input
}] } });
/**
* Add a config service to allow an application
* to customize the date time picker default settings
* across the entire application
*/
class DateTimePickerConfig {
constructor() {
this.showDate = true;
this.showTime = true;
this.showTimezone = true;
this.showSeconds = false;
this.showMeridian = true;
this.showSpinners = true;
this.showNowBtn = true;
this.weekdays = weekdaysShort;
this.nowBtnText = 'Today';
this.timezones = timezones;
this.months = months;
this.monthsShort = monthsShort;
this.meridians = meridians;
this.min = null;
this.max = null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerConfig }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerConfig, decorators: [{
type: Injectable
}] });
class DateTimePickerService {
constructor() {
this._config = inject(DateTimePickerConfig, { optional: true });
this.rangeService = inject(DateRangeService, { optional: true });
this.rangeOptions = inject(DateRangeOptions, { optional: true });
this.mode$ = new BehaviorSubject(DatePickerMode.Day);
this.date$ = new BehaviorSubject(new Date());
this.timezone$ = new BehaviorSubject(null);
this.selected$ = new BehaviorSubject(new Date());
// the month and year to display in the viewport
this.month$ = new BehaviorSubject(new Date().getMonth());
this.year$ = new BehaviorSubject(new Date().getFullYear());
this.showDate$ = new BehaviorSubject(this._config ? this._config.showDate : true);
this.showTime$ = new BehaviorSubject(this._config ? this._config.showTime : true);
this.showTimezone$ = new BehaviorSubject(this._config ? this._config.showTimezone : true);
this.showSeconds$ = new BehaviorSubject(this._config ? this._config.showSeconds : false);
this.showMeridian$ = new BehaviorSubject(this._config ? this._config.showMeridian : true);
this.showSpinners$ = new BehaviorSubject(this._config ? this._config.showSpinners : true);
this.showNowBtn$ = new BehaviorSubject(this._config ? this._config.showNowBtn : true);
this.weekdays$ = new BehaviorSubject(this._config ? this._config.weekdays : weekdaysShort);
this.nowBtnText$ = new BehaviorSubject(this._config ? this._config.nowBtnText : 'Today');
this.timezones$ = new BehaviorSubject(this._config ? this._config.timezones : timezones);
this.min$ = new BehaviorSubject(this._config ? this._config.min : null);
this.max$ = new BehaviorSubject(this._config ? this._config.max : null);
this.header$ = new BehaviorSubject(null);
this.headerEvent$ = new Subject();
this.modeDirection = ModeDirection.None;
this.startOfWeek$ = new BehaviorSubject(WeekDay.Sunday);
this.months = this._config ? this._config.months : months;
this.monthsShort = this._config ? this._config.monthsShort : monthsShort;
this.meridians = this._config ? this._config.meridians : meridians;
/**
* Store whether or not the component has fully initialised or not. We use this to prevent initial
* focus on the end date range picker when the popover is first opened
*/
this.initialised = false;
// when the active date changes set the currently selected date
this._subscription = this.selected$.subscribe(date => {
// the month and year displayed in the viewport should reflect the newly selected items
if (date instanceof Date) {
this.setViewportMonth(date.getMonth());
this.setViewportYear(date.getFullYear());
}
// emit the new date to the component host but only if they are different
if (!dateComparator(date, this.date$.value)) {
if (this.rangeService) {
if (this.rangeOptions.picker === 'start') {
this.rangeService.setStartDate(date);
}
else {
this.rangeService.setEndDate(date);
}
}
else {
this.date$.next(date);
}
}
});
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
setViewportMonth(month) {
if (month < 0) {
this.month$.next(11);
this.year$.next(this.year$.value - 1);
}
else if (month > 11) {
this.month$.next(0);
this.year$.next(this.year$.value + 1);
}
else {
this.month$.next(month);
}
}
setViewportYear(year) {
this.year$.next(year);
}
setDate(day, month, year, hours, minutes, seconds) {
const date = new Date(this.selected$.value);
date.setFullYear(year);
date.setMonth(month, day);
if (hours !== undefined) {
date.setHours(hours);
}
if (minutes !== undefined) {
date.setMinutes(minutes);
}
if (seconds !== undefined) {
date.setSeconds(seconds);
}
if (this.isInRange(date)) {
this.selected$.next(date);
}
}
setDateToNow() {
const now = new Date();
if (this.isInRange(now)) {
this.selected$.next(now);
}
}
setViewportMode(mode) {
this.mode$.next(mode);
}
goToChildMode() {
this.modeDirection = ModeDirection.Descend;
switch (this.mode$.value) {
case DatePickerMode.Year:
return this.setViewportMode(DatePickerMode.Month);
case DatePickerMode.Month:
return this.setViewportMode(DatePickerMode.Day);
}
}
goToParentMode() {
this.modeDirection = ModeDirection.Ascend;
switch (this.mode$.value) {
case DatePickerMode.Day:
return this.setViewportMode(DatePickerMode.Month);
case DatePickerMode.Month:
return this.setViewportMode(DatePickerMode.Year);
}
}
goToNext() {
this.headerEvent$.next(DatePickerHeaderEvent.Next);
}
goToPrevious() {
this.headerEvent$.next(DatePickerHeaderEvent.Previous);
}
setHeader(header) {
this.header$.next(header);
}
isTimezoneAvailable(timezone) {
if (!timezone || !this.timezones$.value) {
return false;
}
return (this.timezones$.value.findIndex(_timezone => _timezone.offset === timezone.offset && _timezone.name === timezone.name) !== -1);
}
getDefaultTimezone() {
const offset = new Date().getTimezoneOffset();
const matchingZone = this.timezones$.value.find(_timezone => _timezone.offset === offset);
return (matchingZone ||
this.timezones$.value.find(_timezone => _timezone.offset === 0) || { name: 'GMT', offset: 0 });
}
isInRange(date) {
return ((!this.min$.value || date >= this.min$.value) && (!this.max$.value || date <= this.max$.value));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
var DatePickerMode;
(function (DatePickerMode) {
DatePickerMode[DatePickerMode["Day"] = 0] = "Day";
DatePickerMode[DatePickerMode["Month"] = 1] = "Month";
DatePickerMode[DatePickerMode["Year"] = 2] = "Year";
})(DatePickerMode || (DatePickerMode = {}));
var ModeDirection;
(function (ModeDirection) {
ModeDirection[ModeDirection["None"] = 0] = "None";
ModeDirection[ModeDirection["Ascend"] = 1] = "Ascend";
ModeDirection[ModeDirection["Descend"] = 2] = "Descend";
})(ModeDirection || (ModeDirection = {}));
var DatePickerHeaderEvent;
(function (DatePickerHeaderEvent) {
DatePickerHeaderEvent[DatePickerHeaderEvent["Previous"] = 0] = "Previous";
DatePickerHeaderEvent[DatePickerHeaderEvent["Next"] = 1] = "Next";
})(DatePickerHeaderEvent || (DatePickerHeaderEvent = {}));
class HeaderComponent {
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
get _rangeStart() {
return this._isRangeMode && this._rangeService ? this._rangeService.start : null;
}
get _rangeEnd() {
return this._isRangeMode && this._rangeService ? this._rangeService.end : null;
}
constructor() {
this.datepicker = inject(DateTimePickerService);
this._changeDetector = inject(ChangeDetectorRef);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
this.canAscend$ = this.datepicker.mode$.pipe(map(mode => mode !== DatePickerMode.Year));
this.mode$ = this.datepicker.mode$.pipe(map(mode => {
switch (mode) {
case DatePickerMode.Day:
return 'Day';
case DatePickerMode.Month:
return 'Month';
case DatePickerMode.Year:
return 'Year';
}
}));
this.headerAria$ = this.datepicker.mode$.pipe(map(mode => {
switch (mode) {
case DatePickerMode.Day:
return 'Switch to show months in the year';
case DatePickerMode.Month:
return 'Switch to show years in the decade';
case DatePickerMode.Year:
return '';
}
}));
this.previousAria$ = this.datepicker.mode$.pipe(map(mode => {
switch (mode) {
case DatePickerMode.Day:
return 'Previous month';
case DatePickerMode.Month:
return 'Previous year';
case DatePickerMode.Year:
return 'Previous decade';
}
}));
this.nextAria$ = this.datepicker.mode$.pipe(map(mode => {
switch (mode) {
case DatePickerMode.Day:
return 'Next month';
case DatePickerMode.Month:
return 'Next year';
case DatePickerMode.Year:
return 'Next decade';
}
}));
/** Unsubscribe from all observables */
this._onDestroy = new Subject();
if (this._rangeService) {
// delay required to allow all ui to update elsewhere
this._rangeService.onRangeChange
.pipe(delay(100), takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
}
ngAfterViewInit() {
// update on min/max changes
merge(this.datepicker.min$, this.datepicker.max$)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Navigate to the previous day, month or year */
previous() {
this.datepicker.goToPrevious();
}
/** Navigate to the larger scale, eg. Days -> Months, Months -> Years */
ascend() {
this.datepicker.goToParentMode();
}
/** Navigate to the previous day, month or year */
next() {
this.datepicker.goToNext();
}
/** Determine if the previous button is enabled */
isPreviousDisabled() {
const min = this.datepicker.min$.value;
if (min && this._isBeforeView(min)) {
return true;
}
// if we are not in range mode or there are no disabled items then we can navigate back
if (!this._isRangeMode ||
(this._rangeStart && this._rangeEnd) ||
(!this._rangeStart && !this._rangeEnd) ||
this._isRangeStart ||
(this._isRangeEnd && this._rangeEnd)) {
return false;
}
if (this._isBeforeView(this._rangeStart)) {
return true;
}
return false;
}
/** Determine if the previous button is enabled */
isNextDisabled() {
const max = this.datepicker.max$.value;
if (max && this._isAfterView(max)) {
return true;
}
// if we are not in range mode or there are no disabled items then we can navigate back
if (!this._isRangeMode ||
(this._rangeStart && this._rangeEnd) ||
(!this._rangeStart && !this._rangeEnd) ||
(this._isRangeStart && this._rangeStart) ||
this._isRangeEnd) {
return false;
}
if (this._isAfterView(this._rangeEnd)) {
return true;
}
return false;
}
_isBeforeView(date) {
const month = this.datepicker.month$.value;
const year = this.datepicker.year$.value;
const mode = this.datepicker.mode$.value;
const yearRange = this.datepicker.yearRange;
if (mode === DatePickerMode.Day) {
return year <= date.getFullYear() && month <= date.getMonth();
}
if (mode === DatePickerMode.Month) {
return year <= date.getFullYear();
}
if (mode === DatePickerMode.Year) {
return yearRange.start <= date.getFullYear();
}
}
_isAfterView(date) {
const month = this.datepicker.month$.value;
const year = this.datepicker.year$.value;
const mode = this.datepicker.mode$.value;
const yearRange = this.datepicker.yearRange;
if (mode === DatePickerMode.Day) {
return year >= date.getFullYear() && month >= date.getMonth();
}
if (mode === DatePickerMode.Month) {
return year >= date.getFullYear();
}
if (mode === DatePickerMode.Year) {
return yearRange.end >= date.getFullYear();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: HeaderComponent, isStandalone: false, selector: "ux-date-time-picker-header", ngImport: i0, template: "<header class=\"header\">\n <button\n type=\"button\"\n uxFocusIndicator\n [disabled]=\"isPreviousDisabled()\"\n class=\"header-navigation\"\n (click)=\"previous(); $event.stopPropagation()\"\n [attr.aria-label]=\"previousAria$ | async\"\n tabindex=\"0\"\n >\n <ux-icon name=\"previous\" class=\"header-navigation-previous-icon\"></ux-icon>\n </button>\n\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"header-title\"\n [attr.aria-label]=\"headerAria$ | async\"\n [class.active]=\"canAscend$ | async\"\n (click)=\"ascend(); $event.stopPropagation()\"\n [tabindex]=\"(canAscend$ | async) ? 0 : -1\"\n >\n {{ datepicker.header$ | async }}\n </button>\n\n <button\n type=\"button\"\n uxFocusIndicator\n [disabled]=\"isNextDisabled()\"\n class=\"header-navigation\"\n (click)=\"next(); $event.stopPropagation()\"\n [attr.aria-label]=\"nextAria$ | async\"\n tabindex=\"0\"\n >\n <ux-icon name=\"next\" class=\"header-navigation-next-icon\"></ux-icon>\n </button>\n</header>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HeaderComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker-header', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<header class=\"header\">\n <button\n type=\"button\"\n uxFocusIndicator\n [disabled]=\"isPreviousDisabled()\"\n class=\"header-navigation\"\n (click)=\"previous(); $event.stopPropagation()\"\n [attr.aria-label]=\"previousAria$ | async\"\n tabindex=\"0\"\n >\n <ux-icon name=\"previous\" class=\"header-navigation-previous-icon\"></ux-icon>\n </button>\n\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"header-title\"\n [attr.aria-label]=\"headerAria$ | async\"\n [class.active]=\"canAscend$ | async\"\n (click)=\"ascend(); $event.stopPropagation()\"\n [tabindex]=\"(canAscend$ | async) ? 0 : -1\"\n >\n {{ datepicker.header$ | async }}\n </button>\n\n <button\n type=\"button\"\n uxFocusIndicator\n [disabled]=\"isNextDisabled()\"\n class=\"header-navigation\"\n (click)=\"next(); $event.stopPropagation()\"\n [attr.aria-label]=\"nextAria$ | async\"\n tabindex=\"0\"\n >\n <ux-icon name=\"next\" class=\"header-navigation-next-icon\"></ux-icon>\n </button>\n</header>\n" }]
}], ctorParameters: () => [] });
class DayViewService {
constructor() {
this._datepicker = inject(DateTimePickerService);
this.grid$ = new BehaviorSubject([[]]);
this.focused$ = new BehaviorSubject(null);
this._subscription = combineLatest(this._datepicker.month$, this._datepicker.year$, this._datepicker.startOfWeek$).subscribe(([month, year]) => this.createDayGrid(month, year));
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
setFocus(day, month, year) {
this.focused$.next({ day, month, year });
// update the date picker to show the required month and year
this._datepicker.setViewportMonth(month);
this._datepicker.setViewportYear(year);
}
createDayGrid(month, year) {
// update the header
this._datepicker.setHeader(this._datepicker.months[month] + ' ' + year);
// find the lower and upper boundaries
const start = new Date(year, month, 1);
const end = new Date(year, month + 1, 0);
// ensure the startOfWeek value is between 0-6 to prevent any infinite loop
const startOfWeek = Math.min(WeekDay.Saturday, Math.max(WeekDay.Sunday, this._datepicker.startOfWeek$.value));
// we always want to show from the specified start of week - this may include showing some dates from the previous month
while (start.getDay() !== startOfWeek) {
start.setDate(start.getDate() - 1);
}
// we also want to make sure that the range ends on a saturday
end.setDate(end.getDate() + (6 - end.getDay()));
// create an array of all the days to display
const dates = dateRange(start, end).map(date => ({
day: date.getDate(),
month: date.getMonth(),
year: date.getFullYear(),
date,
isToday: this.isToday(date),
isActive: this.isActive(date),
isCurrentMonth: date.getMonth() === month,
}));
// turn the dates into a grid
const items = gridify(dates, 7);
this.grid$.next(items);
// if no item has yet been focused then focus the first day of the month
if ((this._datepicker.modeDirection === ModeDirection.None ||
this._datepicker.modeDirection === ModeDirection.Descend) &&
this.focused$.value === null) {
// check if the selected item is visible
const selectedDay = dates.find(day => day.isCurrentMonth && day.isActive);
if (selectedDay) {
this.setFocus(selectedDay.day, selectedDay.month, selectedDay.year);
}
else {
// find the first day of the month
const first = dates.find(date => date.day === 1);
// focus the date
this.setFocus(first.day, first.month, first.year);
}
}
}
/**
* Determine whether or not a specific date is today
* @param date The date to check
*/
isToday(date) {
return compareDays(new Date(), date);
}
/**
* Determines whether or not a specific date is the selected one
* @param date the date to check
*/
isActive(date) {
return this._datepicker.selected$.value
? compareDays(this._datepicker.selected$.value, date)
: false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DayViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DayViewService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DayViewService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class FocusIfDirective {
constructor() {
this._elementRef = inject(ElementRef);
/** The delay that should ellapse before focussing the element */
this.focusIfDelay = 0;
/** Determine if we should scroll the element into view when focused */
this.focusIfScroll = true;
this._timeout = null;
}
/** Focus when the boolean value is true */
set focusIf(focus) {
// if a timeout is pending then cancel it
if (!focus && this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (focus && this._timeout === null) {
this._timeout = window.setTimeout(() => {
this._elementRef.nativeElement.focus({ preventScroll: !this.focusIfScroll });
this._timeout = null;
}, this.focusIfDelay);
}
}
ngOnDestroy() {
if (this._timeout !== null) {
clearTimeout(this._timeout);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FocusIfDirective, isStandalone: false, selector: "[focusIf]", inputs: { focusIfDelay: "focusIfDelay", focusIfScroll: "focusIfScroll", focusIf: "focusIf" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIfDirective, decorators: [{
type: Directive,
args: [{
selector: '[focusIf]',
standalone: false,
}]
}], propDecorators: { focusIfDelay: [{
type: Input
}], focusIfScroll: [{
type: Input
}], focusIf: [{
type: Input
}] } });
class WeekDaySortPipe {
transform(value, startOfWeek) {
// ensure start of week is in range
startOfWeek = Math.max(WeekDay.Sunday, Math.min(WeekDay.Saturday, startOfWeek));
// create a new array to avoid altering the original
const weekdays = [...value];
for (let idx = 0; idx < startOfWeek; idx++) {
weekdays.push(weekdays.shift());
}
return weekdays;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WeekDaySortPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: WeekDaySortPipe, isStandalone: false, name: "weekDaySort" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WeekDaySortPipe, decorators: [{
type: Pipe,
args: [{
name: 'weekDaySort',
standalone: false,
}]
}] });
class DayViewComponent {
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
get _rangeStart() {
return this._isRangeMode && this._rangeService ? this._rangeService.start : null;
}
get _rangeEnd() {
return this._isRangeMode && this._rangeService ? this._rangeService.end : null;
}
constructor() {
this.datePicker = inject(DateTimePickerService);
this.dayService = inject(DayViewService);
this._changeDetector = inject(ChangeDetectorRef);
this._focusOrigin = inject(FocusIndicatorOriginService);
this._liveAnnouncer = inject(LiveAnnouncer);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
this._onDestroy = new Subject();
this.datePicker.headerEvent$
.pipe(takeUntil(this._onDestroy))
.subscribe(event => (event === DatePickerHeaderEvent.Next ? this.next() : this.previous()));
// if we are a range picker then we also want to subscribe to range changes
if (this._rangeService) {
merge(this._rangeService.onRangeChange, this._rangeService.onHoverChange)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
// subscribe to changes to the start date
this._rangeService.onStartChange
.pipe(takeUntil(this._onDestroy), filter(date => !!date && this._isRangeEnd && this.datePicker.initialised), delay(0))
.subscribe(date => this.onRangeChange(date));
// subscribe to changes to the end date
this._rangeService.onEndChange
.pipe(takeUntil(this._onDestroy), filter(date => !!date && this._isRangeStart && this.datePicker.initialised), delay(0))
.subscribe(date => this.onRangeChange(date));
// when the range is cleared reset the selected date so we can click on the same date again if we want to
this._rangeService.onClear
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.datePicker.selected$.next(null));
}
}
ngAfterViewInit() {
// update when there are changes to the min/max values
merge(this.datePicker.min$, this.datePicker.max$)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
// if we open and the range start is already selected, ensure that we move the end picker to a month with options
if (!this.datePicker.initialised && this._rangeStart && !this._rangeEnd && this._isRangeEnd) {
this.onRangeChange(this._rangeStart);
}
// if we open and the range end is already selected, ensure that we move the start picker to a month with options
if (!this.datePicker.initialised && this._rangeEnd && !this._rangeStart && this._isRangeStart) {
this.onRangeChange(this._rangeEnd);
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Navigate to the previous page of dates
*/
previous() {
this.datePicker.setViewportMonth(this.datePicker.month$.value - 1);
}
/**
* Navigate to the next page of dates
*/
next() {
this.datePicker.setViewportMonth(this.datePicker.month$.value + 1);
}
/**
* Select a particular date
* @param date the date to select
*/
select(date) {
// if we are the start range picker and we click the already selected day deselect it
if (this._isRangeMode &&
this._isRangeStart &&
this._rangeStart &&
compareDays(this._rangeStart, date)) {
this._rangeService.setStartDate(null);
this.datePicker.selected$.next(null);
return;
}
// if we are the end range picker and we click the already selected day deselect it
if (this._isRangeMode &&
this._isRangeEnd &&
this._rangeEnd &&
compareDays(this._rangeEnd, date)) {
this._rangeService.setEndDate(null);
this.datePicker.selected$.next(null);
return;
}
// if we are in range mode ensure we include the time from the time picker
if (this._isRangeMode) {
// update the current date object
if (this._isRangeStart && !this._rangeService.showTime) {
this.datePicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), 0, 0, 0);
}
else if (this._isRangeEnd && !this._rangeService.showTime) {
this.datePicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), 23, 59, 59);
}
else {
this.datePicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), this.datePicker.hours, this.datePicker.minutes, this.datePicker.seconds);
}
}
else {
// update the current date object
this.datePicker.setDate(date.getDate(), date.getMonth(), date.getFullYear());
}
// focus the newly selected date
this.dayService.setFocus(date.getDate(), date.getMonth(), date.getFullYear());
// if we select a start date that is after the end date then clear the end date
if (this._isRangeMode && this._isRangeStart && this._rangeStart && this._rangeEnd) {
if (this._rangeStart.getTime() > this._rangeEnd.getTime()) {
this._rangeService.setEndDate(null);
}
}
// if we select a end date that is before the start date then clear the start date
if (this._isRangeMode && this._isRangeEnd && this._rangeStart && this._rangeEnd) {
if (this._rangeEnd.getTime() < this._rangeStart.getTime()) {
this._rangeService.setStartDate(null);
}
}
}
trackWeekByFn(index) {
return index;
}
trackDayByFn(_index, item) {
return `${item.day} ${item.month} ${item.year}`;
}
focusDate(item, dayOffset) {
// determine the date of the day
const target = new Date(item.date.setDate(item.date.getDate() + dayOffset));
// we should force the origin to be keyboard
this._focusOrigin.setOrigin('keyboard');
// identify which date should be focused
this.dayService.setFocus(target.getDate(), target.getMonth(), target.getFullYear());
}
getTabbable(item) {
const focused = this.dayService.focused$.value;
const grid = this.dayService.grid$.value;
const month = this.datePicker.month$.value;
// if there is a focused month check if this is it
if (focused) {
// check if the focused day is visible
const isFocusedDayVisible = !!grid.find(row => !!row.find(_item => _item.day === focused.day &&
_item.month === focused.month &&
_item.year === focused.year &&
_item.month === month));
if (isFocusedDayVisible) {
return (focused.day === item.day && focused.month === item.month && focused.year === item.year);
}
}
// if there is no focusable day then check if there is a selected day
const isSelectedDayVisible = !!grid.find(row => !!row.find(day => day.isActive));
if (isSelectedDayVisible) {
return item.isActive;
}
// find the first non disabled day that is part of the current month
for (const row of grid) {
for (const column of row) {
if (column === item && column.month === month && !this.getDisabled(column.date)) {
return true;
}
}
}
return false;
}
getDisabled(date) {
// if we are not in range mode then it will always be enabled
if (this._isRangeMode) {
// if we are range start and dates are after the range end then they should also be disabled
if (this._isRangeStart &&
!this._rangeStart &&
this._rangeEnd &&
isDateAfter(date, this._rangeEnd)) {
return true;
}
// if we are range end and dates are before the range start then they should also be disabled
if (this._isRangeEnd &&
!this._rangeEnd &&
this._rangeStart &&
isDateBefore(date, this._rangeStart)) {
return true;
}
}
if (this.datePicker.min$.value && isDateBefore(date, this.datePicker.min$.value)) {
return true;
}
if (this.datePicker.max$.value && isDateAfter(date, this.datePicker.max$.value)) {
return true;
}
return false;
}
isRangeStartDate(date) {
return this._isRangeMode && this._rangeStart && compareDays(date, this._rangeStart);
}
isRangeEndDate(date) {
return this._isRangeMode && this._rangeEnd && compareDays(date, this._rangeEnd);
}
isWithinRange(date) {
return (this._isRangeMode &&
this._rangeStart &&
isDateAfter(date, this._rangeStart) &&
isDateBefore(date, this._rangeEnd));
}
isDateHovered(date) {
// if we are not in range mode or both start and end dates are selected then dont show range hover
if (!this._isRangeMode || !this._rangeService.hover || (this._rangeStart && this._rangeEnd)) {
return;
}
return ((this._rangeStart &&
isDateAfter(date, this._rangeStart) &&
isDateBefore(date, this._rangeService.hover, true)) ||
(this._rangeEnd &&
isDateBefore(date, this._rangeEnd) &&
isDateAfter(date, this._rangeService.hover, true)));
}
isItemActive(date, isActive) {
if (!this._isRangeMode) {
return isActive;
}
return ((this._isRangeStart && this._rangeStart && compareDays(this._rangeStart, date)) ||
(this._isRangeEnd && this._rangeEnd && compareDays(this._rangeEnd, date)));
}
onRangeMouseEnter(date) {
if (this._isRangeMode) {
this._rangeService.setDateMouseEnter(date);
}
}
onRangeMouseLeave(date) {
if (this._isRangeMode) {
this._rangeService.setDateMouseLeave(date);
}
}
/** Announce the date when we focus on a date */
announceRangeMode() {
if (this._isRangeMode) {
this._liveAnnouncer.announce(this._isRangeStart
? this._rangeService.startPickerAriaLabel
: this._rangeService.endPickerAriaLabel);
}
}
/** Determine if we should focus a date */
shouldFocus(item) {
// if we are opening the popover initially we never want to focus a date in the range end picker
if ((!this.datePicker.initialised && this._isRangeEnd) ||
(this._rangeService && this._rangeService.isChangingTime)) {
return false;
}
// extract the current focused dates
const { day, month, year } = this.dayService.focused$.value;
// check if the current date is the focused date and it is in the viewport date
return day === item.day && month === item.month && year === item.year && item.isCurrentMonth;
}
/** Update the viewport when the range changes to ensure focus is present on a valid item */
onRangeChange(date) {
if (!((this._isRangeStart && !this._rangeStart) || (this._isRangeEnd && !this._rangeEnd))) {
return;
}
// get the month showing on the other date range picker
const currentDate = new Date(this.datePicker.year$.value, this.datePicker.month$.value);
// if we are the start date and we are after the end date - ONLY then should be change the visible month the match the end date
const startShouldUpdate = this._isRangeStart &&
!this._rangeStart &&
(currentDate.getFullYear() > date.getFullYear() ||
(currentDate.getFullYear() === date.getFullYear() &&
currentDate.getMonth() > date.getMonth()));
// if we are the end date and we are before the start date - ONLY then should be change the visible month the match the start date
const endShouldUpdate = this._isRangeEnd &&
!this._rangeEnd &&
(currentDate.getFullYear() < date.getFullYear() ||
(currentDate.getFullYear() === date.getFullYear() &&
currentDate.getMonth() < date.getMonth()));
if (startShouldUpdate || endShouldUpdate) {
this.datePicker.setViewportMonth(date.getMonth());
this.datePicker.setViewportYear(date.getFullYear());
this.dayService.setFocus(date.getDate(), date.getMonth(), date.getFullYear());
this._changeDetector.detectChanges();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DayViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DayViewComponent, isStandalone: false, selector: "ux-date-time-picker-day-view", providers: [DayViewService], ngImport: i0, template: "<table class=\"calendar\">\n <thead>\n <tr>\n @for (\n day of datePicker.weekdays$ | async | weekDaySort: (datePicker.startOfWeek$ | async);\n let index = $index;\n track index\n ) {\n <th class=\"weekday\" [attr.aria-label]=\"day\">{{ day }}</th>\n }\n </tr>\n </thead>\n\n <tbody role=\"grid\">\n @for (row of dayService.grid$ | async; track trackWeekByFn($index)) {\n <tr role=\"row\">\n @for (item of row; track trackDayByFn($index, item)) {\n <td class=\"date-cell\">\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"date-button\"\n role=\"gridcell\"\n [class.range-start]=\"isRangeStartDate(item.date)\"\n [class.range-between]=\"isWithinRange(item.date) || isDateHovered(item.date)\"\n [class.range-end]=\"isRangeEndDate(item.date)\"\n [focusIf]=\"shouldFocus(item)\"\n [attr.aria-label]=\"item.date | date\"\n [attr.aria-selected]=\"isItemActive(item.date, item.isActive)\"\n [attr.aria-hidden]=\"!item.isCurrentMonth\"\n [class.current]=\"item.isToday\"\n [class.active]=\"isItemActive(item.date, item.isActive) && !getDisabled(item.date)\"\n [class.preview]=\"!item.isCurrentMonth\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n [disabled]=\"getDisabled(item.date)\"\n (click)=\"select(item.date); $event.stopPropagation()\"\n (mouseenter)=\"onRangeMouseEnter(item.date)\"\n (mouseleave)=\"onRangeMouseLeave(item.date)\"\n (keydown.ArrowLeft)=\"focusDate(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusDate(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusDate(item, -7); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusDate(item, 7); $event.preventDefault()\"\n (focus)=\"announceRangeMode()\"\n >\n {{ item.date.getDate() }}\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "pipe", type: WeekDaySortPipe, name: "weekDaySort" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DayViewComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker-day-view', providers: [DayViewService], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<table class=\"calendar\">\n <thead>\n <tr>\n @for (\n day of datePicker.weekdays$ | async | weekDaySort: (datePicker.startOfWeek$ | async);\n let index = $index;\n track index\n ) {\n <th class=\"weekday\" [attr.aria-label]=\"day\">{{ day }}</th>\n }\n </tr>\n </thead>\n\n <tbody role=\"grid\">\n @for (row of dayService.grid$ | async; track trackWeekByFn($index)) {\n <tr role=\"row\">\n @for (item of row; track trackDayByFn($index, item)) {\n <td class=\"date-cell\">\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"date-button\"\n role=\"gridcell\"\n [class.range-start]=\"isRangeStartDate(item.date)\"\n [class.range-between]=\"isWithinRange(item.date) || isDateHovered(item.date)\"\n [class.range-end]=\"isRangeEndDate(item.date)\"\n [focusIf]=\"shouldFocus(item)\"\n [attr.aria-label]=\"item.date | date\"\n [attr.aria-selected]=\"isItemActive(item.date, item.isActive)\"\n [attr.aria-hidden]=\"!item.isCurrentMonth\"\n [class.current]=\"item.isToday\"\n [class.active]=\"isItemActive(item.date, item.isActive) && !getDisabled(item.date)\"\n [class.preview]=\"!item.isCurrentMonth\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n [disabled]=\"getDisabled(item.date)\"\n (click)=\"select(item.date); $event.stopPropagation()\"\n (mouseenter)=\"onRangeMouseEnter(item.date)\"\n (mouseleave)=\"onRangeMouseLeave(item.date)\"\n (keydown.ArrowLeft)=\"focusDate(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusDate(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusDate(item, -7); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusDate(item, 7); $event.preventDefault()\"\n (focus)=\"announceRangeMode()\"\n >\n {{ item.date.getDate() }}\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n" }]
}], ctorParameters: () => [] });
class MonthViewService {
constructor() {
this._datepicker = inject(DateTimePickerService);
this.grid$ = new BehaviorSubject([[]]);
this.focused$ = new BehaviorSubject(null);
this._subscription = this._datepicker.year$.subscribe(year => this.createMonthGrid(year));
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
setFocus(month, year) {
this.focused$.next({ month, year });
// update the viewport to ensure focused month is visible
this._datepicker.setViewportYear(year);
}
createMonthGrid(year) {
// update the header
this._datepicker.setHeader(year.toString());
// get the current year and month
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
// get the currently selected month
const activeMonth = this._datepicker.selected$.value
? this._datepicker.selected$.value.getMonth()
: null;
const activeYear = this._datepicker.selected$.value
? this._datepicker.selected$.value.getFullYear()
: null;
// create a 4x3 grid of month numbers
const months = range(0, 11).map(month => {
return {
name: this._datepicker.monthsShort[month],
month,
year,
isCurrentMonth: year === currentYear && month === currentMonth,
isActiveMonth: year === activeYear && month === activeMonth,
};
});
// map these to the appropriate format
const items = gridify(months, 4);
// update the grid
this.grid$.next(items);
// if there is no focused month select the first one
if (this._datepicker.modeDirection === ModeDirection.Descend && this.focused$.value === null) {
// check if the selected month is in view
const selectedMonth = months.find(month => month.isActiveMonth);
this.setFocus(selectedMonth ? selectedMonth.month : 0, year);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MonthViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MonthViewService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MonthViewService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class MonthViewComponent {
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
get _rangeStart() {
return this._isRangeMode && this._rangeService ? this._rangeService.start : null;
}
get _rangeEnd() {
return this._isRangeMode && this._rangeService ? this._rangeService.end : null;
}
get _minMonth() {
return this._datePicker.min$.value
? new Date(this._datePicker.min$.value.getFullYear(), this._datePicker.min$.value.getMonth())
: null;
}
get _maxMonth() {
return this._datePicker.max$.value
? new Date(this._datePicker.max$.value.getFullYear(), this._datePicker.max$.value.getMonth())
: null;
}
constructor() {
this.monthService = inject(MonthViewService);
this._datePicker = inject(DateTimePickerService);
this._liveAnnouncer = inject(LiveAnnouncer);
this._changeDetector = inject(ChangeDetectorRef);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
this._onDestroy = new Subject();
this._datePicker.headerEvent$
.pipe(takeUntil(this._onDestroy))
.subscribe(event => (event === DatePickerHeaderEvent.Next ? this.next() : this.previous()));
if (this._rangeService) {
this._rangeService.onRangeChange
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
// if the currently focused item is disabled then choose a month that isn't disabled
if (this.monthService.focused$.value) {
if (this.getDisabled(this.monthService.focused$.value)) {
for (const row of this.monthService.grid$.value) {
for (const column of row) {
if (!this.getDisabled(column)) {
this.monthService.setFocus(column.month, column.year);
return;
}
}
}
}
}
}
ngAfterViewInit() {
// update on min/max changes
merge(this._datePicker.min$, this._datePicker.max$)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Get the disabled state of a month */
getDisabled(item) {
const date = new Date(item.year, item.month);
// if we are not in range mode then it will always be enabled
if (this._isRangeMode) {
// if we are range start and dates are after the range end then they should also be disabled
if (this._isRangeStart &&
!this._rangeStart &&
this._rangeEnd &&
isDateAfter(date, new Date(this._rangeEnd.getFullYear(), this._rangeEnd.getMonth()))) {
return true;
}
// if we are range end and dates are before the range start then they should also be disabled
if (this._isRangeEnd &&
!this._rangeEnd &&
this._rangeStart &&
isDateBefore(date, new Date(this._rangeStart.getFullYear(), this._rangeStart.getMonth()))) {
return true;
}
}
if (this._minMonth && isDateBefore(date, this._minMonth)) {
return true;
}
if (this._maxMonth && isDateAfter(date, this._maxMonth)) {
return true;
}
return false;
}
/**
* Go to the previous year
*/
previous() {
this._datePicker.setViewportYear(this._datePicker.year$.value - 1);
}
/**
* Go to the next year
*/
next() {
this._datePicker.setViewportYear(this._datePicker.year$.value + 1);
}
/**
* Select a month in the calendar
* @param month the index of the month to select
*/
select(month) {
this._datePicker.setViewportMonth(month);
// show the day picker
this._datePicker.goToChildMode();
}
focusMonth(item, monthOffset) {
let targetMonth = item.month + monthOffset;
let targetYear = item.year;
if (targetMonth < 0) {
targetMonth += 12;
targetYear -= 1;
}
if (targetMonth >= 12) {
targetMonth -= 12;
targetYear += 1;
}
this.monthService.setFocus(targetMonth, targetYear);
}
trackRowByFn(index) {
return index;
}
trackMonthByFn(_index, item) {
return `${item.month} ${item.year}`;
}
getTabbable(item) {
const focused = this.monthService.focused$.value;
const grid = this.monthService.grid$.value;
// if there is a focused month check if this is it
if (focused) {
// check if the focused month is visible
const isFocusedMonthVisible = !!grid.find(row => !!row.find(_item => _item.month === focused.month && _item.year === focused.year));
if (isFocusedMonthVisible) {
return focused.month === item.month && focused.year === item.year;
}
}
// if there is no focusable month then check if there is a selected month
const isSelectedMonthVisible = !!grid.find(row => !!row.find(month => month.isActiveMonth));
if (isSelectedMonthVisible) {
return item.isActiveMonth;
}
// otherwise find the first non-disabled month
for (const row of grid) {
for (const column of row) {
if (!this.getDisabled(column)) {
return item === column;
}
}
}
return false;
}
/** Announce the date when we focus on a date */
announceRangeMode() {
if (this._isRangeMode) {
this._liveAnnouncer.announce(this._isRangeStart
? this._rangeService.startPickerAriaLabel
: this._rangeService.endPickerAriaLabel);
}
}
shouldFocus(item) {
const focused = this.monthService.focused$.value;
if (focused) {
return focused.month === item.month && focused.year === item.year;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MonthViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MonthViewComponent, isStandalone: false, selector: "ux-date-time-picker-month-view", providers: [MonthViewService], ngImport: i0, template: "<div class=\"calendar\" role=\"grid\">\n @for (row of monthService.grid$ | async; track trackRowByFn($index)) {\n <div class=\"calendar-row\" role=\"row\">\n @for (item of row; track trackMonthByFn($index, item)) {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [programmaticFocusIndicator]=\"true\"\n role=\"gridcell\"\n class=\"calendar-item\"\n [focusIf]=\"shouldFocus(item)\"\n [disabled]=\"getDisabled(item)\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n [attr.aria-label]=\"item.name + ' ' + item.year\"\n [attr.aria-selected]=\"item.isActiveMonth\"\n [class.active]=\"item.isActiveMonth && !getDisabled(item)\"\n [class.current]=\"item.isCurrentMonth\"\n (click)=\"select(item.month); $event.stopPropagation()\"\n (focus)=\"announceRangeMode()\"\n (keydown.ArrowLeft)=\"focusMonth(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusMonth(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusMonth(item, -4); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusMonth(item, 4); $event.preventDefault()\"\n >\n {{ item.name }}\n </button>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MonthViewComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker-month-view', providers: [MonthViewService], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"calendar\" role=\"grid\">\n @for (row of monthService.grid$ | async; track trackRowByFn($index)) {\n <div class=\"calendar-row\" role=\"row\">\n @for (item of row; track trackMonthByFn($index, item)) {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [programmaticFocusIndicator]=\"true\"\n role=\"gridcell\"\n class=\"calendar-item\"\n [focusIf]=\"shouldFocus(item)\"\n [disabled]=\"getDisabled(item)\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n [attr.aria-label]=\"item.name + ' ' + item.year\"\n [attr.aria-selected]=\"item.isActiveMonth\"\n [class.active]=\"item.isActiveMonth && !getDisabled(item)\"\n [class.current]=\"item.isCurrentMonth\"\n (click)=\"select(item.month); $event.stopPropagation()\"\n (focus)=\"announceRangeMode()\"\n (keydown.ArrowLeft)=\"focusMonth(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusMonth(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusMonth(item, -4); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusMonth(item, 4); $event.preventDefault()\"\n >\n {{ item.name }}\n </button>\n }\n </div>\n }\n</div>\n" }]
}], ctorParameters: () => [] });
class YearViewService {
constructor() {
this._datepicker = inject(DateTimePickerService);
this.grid$ = new BehaviorSubject([[]]);
this.focused$ = new BehaviorSubject(null);
this._year = new Date().getFullYear();
this._subscription = new Subscription();
const year = this._datepicker.year$.subscribe(_year => this.createYearGrid(_year));
const event = this._datepicker.headerEvent$.subscribe(_event => _event === DatePickerHeaderEvent.Next ? this.goToNextDecade() : this.goToPreviousDecade());
this._subscription.add(year);
this._subscription.add(event);
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
setFocus(year) {
this.focused$.next(year);
this.createYearGrid(year);
}
goToPreviousDecade() {
this.createYearGrid(this._year - 10);
}
goToNextDecade() {
this.createYearGrid(this._year + 10);
}
createYearGrid(year = this._year) {
this._year = year;
// get the years to display
const decade = this.getDecade(year);
const currentYear = new Date().getFullYear();
// produce items in the correct format
const items = decade.range.map(_year => {
return {
year: _year,
isCurrentYear: _year === currentYear,
isActiveYear: _year === this._datepicker.year$.value,
};
});
// update the header text
this._datepicker.setHeader(decade.start + ' - ' + decade.end);
// create the grid
this.grid$.next(gridify(items, 4));
}
/**
* Get the years in the current decade to display
*/
getDecade(year) {
// figure the start and end points
const start = year - (year % 10);
const end = start + 9;
this._datepicker.yearRange = { start, end, range: range(start, end) };
// create an array containing all the numbers between the start and end points
return this._datepicker.yearRange;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: YearViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: YearViewService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: YearViewService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class YearViewComponent {
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
get _rangeStart() {
return this._isRangeMode && this._rangeService ? this._rangeService.start : null;
}
get _rangeEnd() {
return this._isRangeMode && this._rangeService ? this._rangeService.end : null;
}
get _minYear() {
return this._datePicker.min$.value
? new Date(this._datePicker.min$.value.getFullYear(), 0)
: null;
}
get _maxYear() {
return this._datePicker.max$.value
? new Date(this._datePicker.max$.value.getFullYear(), 0)
: null;
}
constructor() {
this.yearService = inject(YearViewService);
this._datePicker = inject(DateTimePickerService);
this._liveAnnouncer = inject(LiveAnnouncer);
this._changeDetector = inject(ChangeDetectorRef);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
this._onDestroy = new Subject();
if (this._rangeService) {
this._rangeService.onRangeChange
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
}
ngAfterViewInit() {
// update on min/max changes
merge(this._datePicker.min$, this._datePicker.max$)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.detectChanges());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
select(year) {
this._datePicker.setViewportYear(year);
// show the month picker
this._datePicker.goToChildMode();
}
/** Get the disabled state of a month */
getDisabled(item) {
const date = new Date(item.year, 0);
// if we are not in range mode then it will always be enabled
if (this._isRangeMode) {
// if we are range start and dates are after the range end then they should also be disabled
if (this._isRangeStart &&
!this._rangeStart &&
this._rangeEnd &&
isDateAfter(date, new Date(this._rangeEnd.getFullYear(), 0))) {
return true;
}
// if we are range end and dates are before the range start then they should also be disabled
if (this._isRangeEnd &&
!this._rangeEnd &&
this._rangeStart &&
isDateBefore(date, new Date(this._rangeStart.getFullYear(), 0))) {
return true;
}
}
if (this._minYear && isDateBefore(date, this._minYear)) {
return true;
}
if (this._maxYear && isDateAfter(date, this._maxYear)) {
return true;
}
return false;
}
focusYear(item, yearOffset) {
this.yearService.setFocus(item.year + yearOffset);
}
trackRowByFn(index) {
return index;
}
trackYearByFn(_index, item) {
return item.year;
}
getTabbable(item) {
const focused = this.yearService.focused$.value;
const grid = this.yearService.grid$.value;
// if there is a focused year check if this is it
if (focused) {
// check if the focused year is visible
const isFocusedYearVisible = !!grid.find(row => !!row.find(_item => _item.year === focused));
if (isFocusedYearVisible) {
return focused === item.year;
}
}
// if there is no focusable year then check if there is a selected year
const isSelectedYearVisible = !!grid.find(row => !!row.find(year => year.isActiveYear));
if (isSelectedYearVisible) {
return item.isActiveYear;
}
// otherwise find the first non-disabled month
for (const row of this.yearService.grid$.value) {
for (const column of row) {
if (!this.getDisabled(column)) {
return item === column;
}
}
}
// otherwise make the first month tabbable
return false;
}
/** Announce the date when we focus on a date */
announceRangeMode() {
if (this._isRangeMode) {
this._liveAnnouncer.announce(this._isRangeStart
? this._rangeService.startPickerAriaLabel
: this._rangeService.endPickerAriaLabel);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: YearViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: YearViewComponent, isStandalone: false, selector: "ux-date-time-picker-year-view", providers: [YearViewService], ngImport: i0, template: "<div class=\"calendar\" role=\"grid\">\n @for (row of yearService.grid$ | async; track trackRowByFn($index)) {\n <div class=\"calendar-row\" role=\"row\">\n @for (item of row; track trackYearByFn($index, item)) {\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [programmaticFocusIndicator]=\"true\"\n type=\"button\"\n role=\"gridcell\"\n class=\"calendar-item\"\n [focusIf]=\"(yearService.focused$ | async) === item.year\"\n [attr.aria-label]=\"item.year\"\n [attr.aria-selected]=\"item.isActiveYear\"\n [class.current]=\"item.isCurrentYear\"\n [class.active]=\"item.isActiveYear && !getDisabled(item)\"\n [disabled]=\"getDisabled(item)\"\n (click)=\"select(item.year); $event.stopPropagation()\"\n (focus)=\"announceRangeMode()\"\n (keydown.ArrowLeft)=\"focusYear(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusYear(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusYear(item, -4); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusYear(item, 4); $event.preventDefault()\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n >\n {{ item.year }}\n </button>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: YearViewComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker-year-view', providers: [YearViewService], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"calendar\" role=\"grid\">\n @for (row of yearService.grid$ | async; track trackRowByFn($index)) {\n <div class=\"calendar-row\" role=\"row\">\n @for (item of row; track trackYearByFn($index, item)) {\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [programmaticFocusIndicator]=\"true\"\n type=\"button\"\n role=\"gridcell\"\n class=\"calendar-item\"\n [focusIf]=\"(yearService.focused$ | async) === item.year\"\n [attr.aria-label]=\"item.year\"\n [attr.aria-selected]=\"item.isActiveYear\"\n [class.current]=\"item.isCurrentYear\"\n [class.active]=\"item.isActiveYear && !getDisabled(item)\"\n [disabled]=\"getDisabled(item)\"\n (click)=\"select(item.year); $event.stopPropagation()\"\n (focus)=\"announceRangeMode()\"\n (keydown.ArrowLeft)=\"focusYear(item, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"focusYear(item, 1); $event.preventDefault()\"\n (keydown.ArrowUp)=\"focusYear(item, -4); $event.preventDefault()\"\n (keydown.ArrowDown)=\"focusYear(item, 4); $event.preventDefault()\"\n [tabindex]=\"getTabbable(item) ? 0 : -1\"\n >\n {{ item.year }}\n </button>\n }\n </div>\n }\n</div>\n" }]
}], ctorParameters: () => [] });
const SPIN_BUTTON_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SpinButtonComponent),
multi: true,
};
class SpinButtonComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
this.type = 'text';
this.placeholder = '';
this.disabled = false;
this.spinners = true;
this.readOnly = true;
this.scrolling = true;
this.arrowkeys = true;
this.maxLength = Infinity;
this.valueChange = new EventEmitter();
this.increment = new EventEmitter();
this.decrement = new EventEmitter();
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
this._regexKeypress = RegExp(/^[0-9.,-]+$/);
// eslint-disable-next-line no-useless-escape
this._regexPaste = RegExp(/^\-?\d+(\.\d+)?$/);
}
set value(value) {
this._value = value;
this.onChangeCallback(value);
this.onTouchedCallback();
}
get value() {
return this._value;
}
scroll(event) {
if (!this.scrolling) {
return;
}
if (event.deltaY > 0) {
this.triggerDecrement();
}
else {
this.triggerIncrement();
}
event.preventDefault();
}
triggerIncrement() {
if (!this.disabled) {
this.increment.emit();
}
}
triggerDecrement() {
if (!this.disabled) {
this.decrement.emit();
}
}
writeValue(value) {
this.value = value;
this._changeDetector.markForCheck();
}
registerOnChange(fn) {
this.onChangeCallback = fn;
}
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
onKeypress(event) {
// we only need to perform checks if the type is number
if (this.type !== 'number') {
return;
}
if (!this._regexKeypress.test(event.key)) {
return false;
}
return true;
}
onPaste(event) {
// we only need to perform checks if the type is number
if (this.type !== 'number') {
return;
}
// get the value being pasted
const value = event.clipboardData.getData('text');
// check if it contains the character
if (!this._regexPaste.test(value)) {
// inset the numeric value only if there is one
const numericValue = parseFloat(value);
if (!isNaN(numericValue)) {
this.value = numericValue;
}
event.stopPropagation();
event.preventDefault();
}
}
onValueChange(input, value) {
// ensure the value is not longer than the maxLength (verify value is a string in case it is
// null or undefined, before trying to check the length.
if (typeof value === 'string' && value.length > this.maxLength) {
// if the type specified is a number then it may begin with a 0
// e.g. "02", in which case if we add a second digit we should drop
// the leading "0" and allow the non-zero number to be added
if (this.type === 'number') {
value = parseFloat(value).toString();
}
// remove any characters over the max length
value = value.substring(0, this.maxLength);
// We must manually update the input value in this case rather than relying
// on Angular, as if value was previously "11" and we add an additional digit
// e.g. "112", after performing the substring, the outputted value would again
// be "11" which Angular would not recognize as having changed so it will not
// update the value displayed in the input.
input.value = value;
}
// emit the value after all length checks
this.valueChange.emit(value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SpinButtonComponent, isStandalone: false, selector: "ux-spin-button", inputs: { value: "value", type: "type", min: "min", max: "max", placeholder: "placeholder", disabled: "disabled", spinners: "spinners", readOnly: "readOnly", scrolling: "scrolling", arrowkeys: "arrowkeys", maxLength: "maxLength", incrementAriaLabel: "incrementAriaLabel", inputAriaLabel: "inputAriaLabel", decrementAriaLabel: "decrementAriaLabel" }, outputs: { valueChange: "valueChange", increment: "increment", decrement: "decrement" }, providers: [SPIN_BUTTON_VALUE_ACCESSOR], ngImport: i0, template: "@if (spinners) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"spin-button\"\n tabindex=\"-1\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"incrementAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"triggerIncrement()\"\n >\n <ux-icon name=\"up\" class=\"spin-button-up-icon\"></ux-icon>\n </button>\n}\n\n<input\n #input\n type=\"text\"\n role=\"spinbutton\"\n [min]=\"min\"\n [max]=\"max\"\n [tabindex]=\"0\"\n class=\"form-control\"\n [placeholder]=\"placeholder\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"inputAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-valuemin]=\"min\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-valuemax]=\"max\"\n [attr.aria-readonly]=\"readOnly\"\n [ngModel]=\"value\"\n (ngModelChange)=\"onValueChange(input, $event)\"\n (wheel)=\"scroll($event)\"\n (keypress)=\"onKeypress($event)\"\n (paste)=\"onPaste($event)\"\n (keydown.arrowup)=\"arrowkeys ? triggerIncrement() : null; $event.preventDefault()\"\n (keydown.arrowdown)=\"arrowkeys ? triggerDecrement() : null; $event.preventDefault()\"\n/>\n\n@if (spinners) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"spin-button\"\n tabindex=\"-1\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"decrementAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"triggerDecrement()\"\n >\n <ux-icon name=\"down\" class=\"spin-button-down-icon\"></ux-icon>\n </button>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-spin-button', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [SPIN_BUTTON_VALUE_ACCESSOR], standalone: false, template: "@if (spinners) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"spin-button\"\n tabindex=\"-1\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"incrementAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"triggerIncrement()\"\n >\n <ux-icon name=\"up\" class=\"spin-button-up-icon\"></ux-icon>\n </button>\n}\n\n<input\n #input\n type=\"text\"\n role=\"spinbutton\"\n [min]=\"min\"\n [max]=\"max\"\n [tabindex]=\"0\"\n class=\"form-control\"\n [placeholder]=\"placeholder\"\n [readOnly]=\"readOnly\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"inputAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-valuemin]=\"min\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-valuemax]=\"max\"\n [attr.aria-readonly]=\"readOnly\"\n [ngModel]=\"value\"\n (ngModelChange)=\"onValueChange(input, $event)\"\n (wheel)=\"scroll($event)\"\n (keypress)=\"onKeypress($event)\"\n (paste)=\"onPaste($event)\"\n (keydown.arrowup)=\"arrowkeys ? triggerIncrement() : null; $event.preventDefault()\"\n (keydown.arrowdown)=\"arrowkeys ? triggerDecrement() : null; $event.preventDefault()\"\n/>\n\n@if (spinners) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"spin-button\"\n tabindex=\"-1\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"decrementAriaLabel\"\n [attr.aria-disabled]=\"disabled\"\n (click)=\"triggerDecrement()\"\n >\n <ux-icon name=\"down\" class=\"spin-button-down-icon\"></ux-icon>\n </button>\n}\n" }]
}], propDecorators: { value: [{
type: Input
}], type: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], placeholder: [{
type: Input
}], disabled: [{
type: Input
}], spinners: [{
type: Input
}], readOnly: [{
type: Input
}], scrolling: [{
type: Input
}], arrowkeys: [{
type: Input
}], maxLength: [{
type: Input
}], incrementAriaLabel: [{
type: Input
}], inputAriaLabel: [{
type: Input
}], decrementAriaLabel: [{
type: Input
}], valueChange: [{
type: Output
}], increment: [{
type: Output
}], decrement: [{
type: Output
}] } });
const TIME_PICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimePickerComponent),
multi: true,
};
class TimePickerComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Whether the arrow keys can be used to increment or decrement the selected time component. */
this.arrowkeys = true;
/** Whether the mouse scroll wheel can be used to increment or decrement the selected time component. */
this.mousewheel = true;
/** Whether the control is disabled. */
this.disabled = false;
/** Whether the control is readonly. */
this.readOnly = false;
/** Whether to show the meridian (AM/PM) selector. If this is false, the 24-hour clock will be used. */
this.showMeridian = false;
/** Whether to show the hour selector. */
this.showHours = true;
/** Whether to show the minute selector. */
this.showMinutes = true;
/** Whether to show the second selector. */
this.showSeconds = false;
/** Whether to show increment and decrement buttons in the time picker. */
this.showSpinners = true;
/** The number of hours to increment or decrement by when using the spinner buttons, arrow keys, or mouse scroll wheel. */
this.hourStep = 1;
/** The number of minutes to increment or decrement by when using the spinner buttons, arrow keys, or mouse scroll wheel. */
this.minuteStep = 1;
/** The number of seconds to increment or decrement by when using the spinner buttons, arrow keys, or mouse scroll wheel. */
this.secondStep = 1;
/** An array containing the labels to show in the meridian selector. */
this.meridians = ['AM', 'PM'];
/** Emitted when the `value` changes. */
this.valueChange = new EventEmitter();
/** Emitted when the validity of the control changes. */
this.isValid = new EventEmitter();
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
this._value = new Date();
this._isValid = true;
}
/** The value to display. */
set value(value) {
this._value = new Date(value);
this.valueChange.emit(this._value);
this.onChangeCallback(this._value);
this.onTouchedCallback();
}
get value() {
return new Date(this._value);
}
get _meridian() {
return this._value.getHours() < 12 ? this.meridians[0] : this.meridians[1];
}
get _valid() {
return this.checkValidity(this._value);
}
get isValidDate() {
return this.value instanceof Date && !isNaN(this.value.getTime());
}
writeValue(value) {
this.value = value;
this._changeDetector.markForCheck();
}
registerOnChange(fn) {
this.onChangeCallback = fn;
}
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
getMeridianTime(hour) {
return hour > 12 ? hour - 12 : hour;
}
setHour(hour) {
const date = this.value;
date.setHours(hour ? hour : 0);
this.value = date;
}
setMinute(minute) {
const date = this.value;
date.setMinutes(minute ? minute : 0);
this.value = date;
}
setSeconds(seconds) {
const date = this.value;
date.setSeconds(seconds ? seconds : 0);
this.value = date;
}
incrementHour(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setHour(this.value.getHours() + this.hourStep);
}
decrementHour(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setHour(this.value.getHours() - this.hourStep);
}
incrementMinute(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setMinute(this.value.getMinutes() + this.minuteStep);
}
decrementMinute(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setMinute(this.value.getMinutes() - this.minuteStep);
}
incrementSecond(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setSeconds(this.value.getSeconds() + this.secondStep);
}
decrementSecond(arrowkey = false) {
if (this.disabled || (arrowkey && !this.arrowkeys)) {
return;
}
this.setSeconds(this.value.getSeconds() - this.secondStep);
}
selectMeridian(meridian) {
// get the current time
const hour = this.value.getHours();
// if we have selected AM
if (meridian === this.meridians[0]) {
if (hour >= 12) {
this.setHour(hour - 12);
}
}
// if we have selected PM
if (meridian === this.meridians[1]) {
if (hour < 12) {
this.setHour(hour + 12);
}
}
}
checkValidity(date) {
let valid = true;
// Fix min and max date components in order to compare time only
const min = this.normalizeDate(this.min, date);
const max = this.normalizeDate(this.max, date);
if ((min && date.getTime() < min.getTime()) || (max && date.getTime() > max.getTime())) {
valid = false;
}
// if the valid state has changed then emit the isValid output
if (valid !== this._isValid) {
this._isValid = valid;
this.isValid.emit(valid);
}
return valid;
}
hourChange(value) {
// if the value is empty then emit nothing
if (value && typeof value === 'string' && value.trim() === '') {
return;
}
// convert the string to a number
let hour = typeof value === 'number' ? value : parseInt(value);
// ensure the hours is valid
if (!isNaN(hour)) {
if (hour < 0) {
hour = 0;
}
if (hour > (this.showMeridian ? 12 : 23)) {
hour = this.showMeridian ? 12 : 23;
}
}
const currentHour = this.value.getHours();
// if the value hasn't changed, do nothing
if (hour === currentHour) {
return;
}
hour = isNaN(hour) ? currentHour : hour;
if (this.showMeridian) {
// if the number is invalid then restore it to the previous value
if (this._meridian === this.meridians[0]) {
if (hour >= 12) {
hour -= 12;
}
}
// if we have selected PM
if (this._meridian === this.meridians[1]) {
if (hour < 12) {
hour += 12;
}
}
}
this.setHour(hour);
}
minuteChange(value) {
// convert the string to a number
let minute = typeof value === 'number' ? value : parseInt(value);
const currentMinute = this.value.getMinutes();
// if the value hasn't changed, do nothing
if (minute === currentMinute) {
return;
}
// ensure the hours is valid
if (!isNaN(minute)) {
if (minute < 0) {
minute = 59;
}
if (minute > 59) {
minute = 0;
}
}
// if the number is invalid then restore it to the previous value
this.setMinute(isNaN(minute) ? currentMinute : minute);
}
secondChange(value) {
// convert the string to a number
let second = typeof value === 'number' ? value : parseInt(value);
const currentSecond = this.value.getSeconds();
// if the value hasn't changed, do nothing
if (second === currentSecond) {
return;
}
// ensure the hours is valid
if (!isNaN(second)) {
if (second < 0) {
second = 0;
}
if (second > 59) {
second = 59;
}
}
// if the number is invalid then restore it to the previous value
this.setSeconds(isNaN(second) ? currentSecond : second);
}
/** Normalise a date's year/month/date components. */
normalizeDate(date, reference) {
if (!date) {
return null;
}
const normalized = new Date(date);
normalized.setFullYear(reference.getFullYear());
normalized.setMonth(reference.getMonth());
normalized.setDate(reference.getDate());
return normalized;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TimePickerComponent, isStandalone: false, selector: "ux-time-picker", inputs: { arrowkeys: "arrowkeys", mousewheel: "mousewheel", disabled: "disabled", readOnly: "readOnly", showMeridian: "showMeridian", showHours: "showHours", showMinutes: "showMinutes", showSeconds: "showSeconds", showSpinners: "showSpinners", hourStep: "hourStep", minuteStep: "minuteStep", secondStep: "secondStep", min: "min", max: "max", meridians: "meridians", value: "value" }, outputs: { valueChange: "valueChange", isValid: "isValid" }, providers: [TIME_PICKER_VALUE_ACCESSOR], ngImport: i0, template: "<div class=\"time-picker\">\n @if (showHours) {\n <div class=\"time-picker-column time-hours-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"HH\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"showMeridian ? 12 : 23\"\n [value]=\"isValidDate ? (value | date: (showMeridian ? 'h' : 'HH')) : ''\"\n (valueChange)=\"hourChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"hour\"\n incrementAriaLabel=\"Increment the hour\"\n decrementAriaLabel=\"Decrement the hour\"\n (increment)=\"incrementHour()\"\n (decrement)=\"decrementHour()\"\n >\n </ux-spin-button>\n </div>\n }\n\n @if (showMinutes) {\n <div class=\"time-picker-separator\">:</div>\n }\n\n @if (showMinutes) {\n <div class=\"time-picker-column time-minutes-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"MM\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"59\"\n [value]=\"isValidDate ? (value | date: 'mm') : ''\"\n (valueChange)=\"minuteChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"minute\"\n incrementAriaLabel=\"Increment the minute\"\n decrementAriaLabel=\"Decrement the minute\"\n (increment)=\"incrementMinute()\"\n (decrement)=\"decrementMinute()\"\n >\n </ux-spin-button>\n </div>\n }\n\n @if (showSeconds) {\n <div class=\"time-picker-separator\">:</div>\n }\n\n @if (showSeconds) {\n <div class=\"time-picker-column time-seconds-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"SS\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"59\"\n [value]=\"isValidDate ? (value | date: 'ss') : ''\"\n (valueChange)=\"secondChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"seconds\"\n incrementAriaLabel=\"Increment the second\"\n decrementAriaLabel=\"Decrement the second\"\n (increment)=\"incrementSecond()\"\n (decrement)=\"decrementSecond()\"\n >\n </ux-spin-button>\n </div>\n }\n</div>\n\n@if (showMeridian) {\n <div class=\"time-picker-meridian\">\n <div class=\"btn-group\" role=\"radiogroup\">\n @for (meridian of meridians; track meridian) {\n <button\n type=\"button\"\n class=\"btn button-toggle-accent\"\n role=\"radio\"\n tabindex=\"0\"\n [disabled]=\"disabled\"\n (click)=\"selectMeridian(meridian)\"\n [class.active]=\"meridian === _meridian\"\n [attr.aria-label]=\"meridian\"\n [attr.aria-checked]=\"meridian === _meridian\"\n [attr.aria-disabled]=\"disabled\"\n >\n {{ meridian }}\n </button>\n }\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "component", type: SpinButtonComponent, selector: "ux-spin-button", inputs: ["value", "type", "min", "max", "placeholder", "disabled", "spinners", "readOnly", "scrolling", "arrowkeys", "maxLength", "incrementAriaLabel", "inputAriaLabel", "decrementAriaLabel"], outputs: ["valueChange", "increment", "decrement"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimePickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-time-picker', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [TIME_PICKER_VALUE_ACCESSOR], standalone: false, template: "<div class=\"time-picker\">\n @if (showHours) {\n <div class=\"time-picker-column time-hours-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"HH\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"showMeridian ? 12 : 23\"\n [value]=\"isValidDate ? (value | date: (showMeridian ? 'h' : 'HH')) : ''\"\n (valueChange)=\"hourChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"hour\"\n incrementAriaLabel=\"Increment the hour\"\n decrementAriaLabel=\"Decrement the hour\"\n (increment)=\"incrementHour()\"\n (decrement)=\"decrementHour()\"\n >\n </ux-spin-button>\n </div>\n }\n\n @if (showMinutes) {\n <div class=\"time-picker-separator\">:</div>\n }\n\n @if (showMinutes) {\n <div class=\"time-picker-column time-minutes-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"MM\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"59\"\n [value]=\"isValidDate ? (value | date: 'mm') : ''\"\n (valueChange)=\"minuteChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"minute\"\n incrementAriaLabel=\"Increment the minute\"\n decrementAriaLabel=\"Decrement the minute\"\n (increment)=\"incrementMinute()\"\n (decrement)=\"decrementMinute()\"\n >\n </ux-spin-button>\n </div>\n }\n\n @if (showSeconds) {\n <div class=\"time-picker-separator\">:</div>\n }\n\n @if (showSeconds) {\n <div class=\"time-picker-column time-seconds-picker\" [class.has-error]=\"!_valid\">\n <ux-spin-button\n type=\"number\"\n class=\"time-spinner\"\n placeholder=\"SS\"\n [maxLength]=\"2\"\n [min]=\"0\"\n [max]=\"59\"\n [value]=\"isValidDate ? (value | date: 'ss') : ''\"\n (valueChange)=\"secondChange($event)\"\n [spinners]=\"showSpinners\"\n [disabled]=\"disabled\"\n [readOnly]=\"readOnly\"\n inputAriaLabel=\"seconds\"\n incrementAriaLabel=\"Increment the second\"\n decrementAriaLabel=\"Decrement the second\"\n (increment)=\"incrementSecond()\"\n (decrement)=\"decrementSecond()\"\n >\n </ux-spin-button>\n </div>\n }\n</div>\n\n@if (showMeridian) {\n <div class=\"time-picker-meridian\">\n <div class=\"btn-group\" role=\"radiogroup\">\n @for (meridian of meridians; track meridian) {\n <button\n type=\"button\"\n class=\"btn button-toggle-accent\"\n role=\"radio\"\n tabindex=\"0\"\n [disabled]=\"disabled\"\n (click)=\"selectMeridian(meridian)\"\n [class.active]=\"meridian === _meridian\"\n [attr.aria-label]=\"meridian\"\n [attr.aria-checked]=\"meridian === _meridian\"\n [attr.aria-disabled]=\"disabled\"\n >\n {{ meridian }}\n </button>\n }\n </div>\n </div>\n}\n" }]
}], propDecorators: { arrowkeys: [{
type: Input
}], mousewheel: [{
type: Input
}], disabled: [{
type: Input
}], readOnly: [{
type: Input
}], showMeridian: [{
type: Input
}], showHours: [{
type: Input
}], showMinutes: [{
type: Input
}], showSeconds: [{
type: Input
}], showSpinners: [{
type: Input
}], hourStep: [{
type: Input
}], minuteStep: [{
type: Input
}], secondStep: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], meridians: [{
type: Input
}], value: [{
type: Input
}], valueChange: [{
type: Output
}], isValid: [{
type: Output
}] } });
class TimeViewComponent {
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
get _rangeStart() {
return this._isRangeMode && this._rangeService ? this._rangeService.start : null;
}
get _rangeEnd() {
return this._isRangeMode && this._rangeService ? this._rangeService.end : null;
}
constructor() {
this.datepicker = inject(DateTimePickerService);
this._changeDetector = inject(ChangeDetectorRef);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
/** Earliest time permitted on the time picker. */
this.min = null;
/** Latest time permitted on the time picker. */
this.max = null;
/** Emit when the timezone changes. */
this.timezoneChange = new EventEmitter();
this._onDestroy = new Subject();
// when the date changes we should update the value
this.datepicker.date$
.pipe(filter(date => date && this.value instanceof Date), takeUntil(this._onDestroy))
.subscribe(date => {
this.value = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
this._changeDetector.detectChanges();
});
if (!this._isRangeMode) {
this.datepicker.selected$
.pipe(filter(date => !!date), takeUntil(this._onDestroy))
.subscribe(date => (this.value = new Date(date)));
}
if (this._isRangeMode && this._isRangeStart) {
this.value = new Date();
if (!this._rangeStart) {
this.value.setHours(0, 0, 0, 0);
}
else {
this.value.setHours(this._rangeStart.getHours(), this._rangeStart.getMinutes(), this._rangeStart.getSeconds());
}
}
if (this._isRangeMode && this._isRangeEnd) {
this.value = new Date();
if (!this._rangeEnd) {
this.value.setHours(23, 59, 59, 0);
}
else {
this.value.setHours(this._rangeEnd.getHours(), this._rangeEnd.getMinutes(), this._rangeEnd.getSeconds());
}
}
}
ngOnInit() {
// min should only apply if it's on the same day as the selected date
combineLatest(this.datepicker.min$, this.datepicker.date$)
.pipe(takeUntil(this._onDestroy))
.subscribe(([min, date]) => {
this.min = min && date && compareDays(date, min) ? min : null;
this._changeDetector.detectChanges();
});
// max should only apply if it's on the same day as the selected date
combineLatest(this.datepicker.max$, this.datepicker.date$)
.pipe(takeUntil(this._onDestroy))
.subscribe(([max, date]) => {
this.max = max && date && compareDays(date, max) ? max : null;
this._changeDetector.detectChanges();
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onTimeChange(time) {
if (this._isRangeMode) {
this.datepicker.hours = time.getHours();
this.datepicker.minutes = time.getMinutes();
this.datepicker.seconds = time.getSeconds();
// update the time in the range picker service
if (this._isRangeStart) {
this._rangeService.startTime = {
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds(),
};
}
else {
this._rangeService.endTime = {
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds(),
};
}
// if a date is currently selected we should update it
if (this._isRangeStart && this._rangeStart) {
const start = new Date(this._rangeStart);
start.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
this._rangeService.setStartDate(start);
}
if (this._isRangeEnd && this._rangeEnd) {
const end = new Date(this._rangeEnd);
end.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
this._rangeService.setEndDate(end);
}
return;
}
// if the selected time is null then do nothing
if (!this.datepicker.selected$.value) {
return;
}
// otherwise set the time
const date = new Date(this.datepicker.selected$.value);
// update the time
date.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
// emit the time
this.datepicker.selected$.next(date);
}
selectTimezone(name) {
const timezones = this.datepicker.timezones$.value;
// find matching timezone
const timezone = timezones.find(_timezone => _timezone.name === name);
if (timezone) {
this.timezoneChange.emit(timezone);
}
}
incrementTimezone() {
const timezone = this.datepicker.timezone$.value;
const timezones = this.datepicker.timezones$.value;
const currentZone = timezones.findIndex(zone => zone.name === timezone.name && zone.offset === timezone.offset);
// try to get the previous zone
this.timezoneChange.emit(timezones[currentZone + 1] ? timezones[currentZone + 1] : timezones[currentZone]);
}
decrementTimezone() {
const timezone = this.datepicker.timezone$.value;
const timezones = this.datepicker.timezones$.value;
const currentZone = timezones.findIndex(zone => zone.name === timezone.name && zone.offset === timezone.offset);
// try to get the previous zone
this.timezoneChange.emit(timezones[currentZone - 1] ? timezones[currentZone - 1] : timezones[currentZone]);
}
onFocusWithin() {
if (this._isRangeMode) {
this._rangeService.isChangingTime = true;
}
}
onFocusOut() {
if (this._isRangeMode) {
this._rangeService.isChangingTime = false;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TimeViewComponent, isStandalone: false, selector: "ux-date-time-picker-time-view", outputs: { timezoneChange: "timezoneChange" }, host: { listeners: { "focusin": "onFocusWithin()", "focusout": "onFocusOut()" } }, ngImport: i0, template: "@if (datepicker.showTime$ | async) {\n <ux-time-picker\n [value]=\"value\"\n (valueChange)=\"onTimeChange($event)\"\n [showSeconds]=\"datepicker.showSeconds$ | async\"\n [showMeridian]=\"datepicker.showMeridian$ | async\"\n [showSpinners]=\"datepicker.showSpinners$ | async\"\n [meridians]=\"datepicker.meridians\"\n [min]=\"min\"\n [max]=\"max\"\n >\n </ux-time-picker>\n}\n\n@if (datepicker.showTimezone$ | async) {\n @if (datepicker.showSpinners$ | async) {\n <div class=\"time-zone-picker\">\n <ux-spin-button\n class=\"time-zone-spinner\"\n [value]=\"(datepicker.timezone$ | async)?.name\"\n [readOnly]=\"true\"\n (increment)=\"incrementTimezone()\"\n (decrement)=\"decrementTimezone()\"\n inputAriaLabel=\"Time Zone\"\n incrementAriaLabel=\"Switch to the next time zone\"\n decrementAriaLabel=\"Switch to the previous time zone\"\n >\n </ux-spin-button>\n </div>\n }\n @if ((datepicker.showSpinners$ | async) === false) {\n <div class=\"time-zone-picker\">\n <select\n class=\"form-control time-zone-select\"\n tabindex=\"0\"\n [ngModel]=\"(datepicker.timezone$ | async)?.name\"\n (ngModelChange)=\"selectTimezone($event)\"\n aria-label=\"Timezone\"\n [attr.aria-valuetext]=\"(datepicker.timezone$ | async)?.name\"\n >\n @for (zone of datepicker.timezones$ | async; track zone) {\n <option\n [selected]=\"zone.name === (datepicker.timezone$ | async)?.name\"\n [value]=\"zone.name\"\n >\n {{ zone?.name }}\n </option>\n }\n </select>\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: SpinButtonComponent, selector: "ux-spin-button", inputs: ["value", "type", "min", "max", "placeholder", "disabled", "spinners", "readOnly", "scrolling", "arrowkeys", "maxLength", "incrementAriaLabel", "inputAriaLabel", "decrementAriaLabel"], outputs: ["valueChange", "increment", "decrement"] }, { kind: "component", type: TimePickerComponent, selector: "ux-time-picker", inputs: ["arrowkeys", "mousewheel", "disabled", "readOnly", "showMeridian", "showHours", "showMinutes", "showSeconds", "showSpinners", "hourStep", "minuteStep", "secondStep", "min", "max", "meridians", "value"], outputs: ["valueChange", "isValid"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimeViewComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker-time-view', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "@if (datepicker.showTime$ | async) {\n <ux-time-picker\n [value]=\"value\"\n (valueChange)=\"onTimeChange($event)\"\n [showSeconds]=\"datepicker.showSeconds$ | async\"\n [showMeridian]=\"datepicker.showMeridian$ | async\"\n [showSpinners]=\"datepicker.showSpinners$ | async\"\n [meridians]=\"datepicker.meridians\"\n [min]=\"min\"\n [max]=\"max\"\n >\n </ux-time-picker>\n}\n\n@if (datepicker.showTimezone$ | async) {\n @if (datepicker.showSpinners$ | async) {\n <div class=\"time-zone-picker\">\n <ux-spin-button\n class=\"time-zone-spinner\"\n [value]=\"(datepicker.timezone$ | async)?.name\"\n [readOnly]=\"true\"\n (increment)=\"incrementTimezone()\"\n (decrement)=\"decrementTimezone()\"\n inputAriaLabel=\"Time Zone\"\n incrementAriaLabel=\"Switch to the next time zone\"\n decrementAriaLabel=\"Switch to the previous time zone\"\n >\n </ux-spin-button>\n </div>\n }\n @if ((datepicker.showSpinners$ | async) === false) {\n <div class=\"time-zone-picker\">\n <select\n class=\"form-control time-zone-select\"\n tabindex=\"0\"\n [ngModel]=\"(datepicker.timezone$ | async)?.name\"\n (ngModelChange)=\"selectTimezone($event)\"\n aria-label=\"Timezone\"\n [attr.aria-valuetext]=\"(datepicker.timezone$ | async)?.name\"\n >\n @for (zone of datepicker.timezones$ | async; track zone) {\n <option\n [selected]=\"zone.name === (datepicker.timezone$ | async)?.name\"\n [value]=\"zone.name\"\n >\n {{ zone?.name }}\n </option>\n }\n </select>\n </div>\n }\n}\n" }]
}], ctorParameters: () => [], propDecorators: { timezoneChange: [{
type: Output
}], onFocusWithin: [{
type: HostListener,
args: ['focusin']
}], onFocusOut: [{
type: HostListener,
args: ['focusout']
}] } });
class DateTimePickerComponent {
/** Defines whether or not the date picker should be visible. */
set showDate(value) {
if (value !== undefined) {
this.datepicker.showDate$.next(value);
}
}
/** Defines whether or not the time picker should be visible. */
set showTime(value) {
if (value !== undefined) {
this.datepicker.showTime$.next(value);
}
}
/** Defines whether or not the time picker should allow the user to choose a timezone. */
set showTimezone(value) {
if (value !== undefined) {
this.datepicker.showTimezone$.next(value);
}
}
/** Defines whether or not the time picker should allow the user to specify seconds. */
set showSeconds(value) {
if (value !== undefined) {
this.datepicker.showSeconds$.next(value);
}
}
/** Defines whether or not the time picker should show an AM/PM button, or time should be represented in 24hr format instead. */
set showMeridian(value) {
if (value !== undefined) {
this.datepicker.showMeridian$.next(value);
}
}
/** Defines whether or not the time picker should allow the user to select the time using spinners. */
set showSpinners(value) {
if (value !== undefined) {
this.datepicker.showSpinners$.next(value);
}
}
/** If defined will override the weekday names displayed. */
set weekdays(value) {
if (value !== undefined) {
this.datepicker.weekdays$.next(value);
}
}
/** Defines the names of the months. */
set months(months) {
if (months !== undefined) {
this.datepicker.months = months;
}
}
/** Defines the short names of each month. */
set monthsShort(months) {
if (months !== undefined) {
this.datepicker.monthsShort = months;
}
}
/** Defines the labels to show in the meridian (AM/PM) selector. */
set meridians(meridians) {
if (meridians !== undefined) {
this.datepicker.meridians = meridians;
}
}
/** Defines the text to be displayed in the button used to set the selected time to the current time. */
set nowBtnText(value) {
if (value !== undefined) {
this.datepicker.nowBtnText$.next(value);
}
}
/** Specify whether or not to show the show now button */
set showNowBtn(value) {
if (value !== undefined) {
this.datepicker.showNowBtn$.next(value);
}
}
/**
* Defines the list of available timezones. The `DateTimePickerTimezone` interface specifies that each timezone should
* be an object with a `name` property that represents the timezone, eg. `GMT+2`, and an `offset` property that represents
* the number of minutes relative to GMT the timezone is.
*/
set timezones(value) {
if (value !== undefined) {
this.datepicker.timezones$.next(value);
}
}
/** Defines the day of the week that should appear in the first column. `WeekDay` is an enumeration available in `@angular/common`. */
set startOfWeek(startOfWeek) {
if (startOfWeek !== undefined) {
this.datepicker.startOfWeek$.next(startOfWeek);
}
}
/** The selected date to be displayed in the component. */
set date(value) {
if (value && !dateComparator(value, this.datepicker.date$.value)) {
if (this._isRangeMode) {
this.datepicker.date$.next(new Date(value));
this.datepicker.selected$.next(new Date(value));
}
else {
this.datepicker.selected$.next(new Date(value));
}
}
if (!value) {
this.datepicker.date$.next(value);
this.datepicker.selected$.next(value);
}
}
/** Will set the selected timezone. */
set timezone(value) {
this.setTimezone(value);
}
/** The earliest selectable date. */
set min(value) {
this.datepicker.min$.next(value);
}
/** The latest selectable date. */
set max(value) {
this.datepicker.max$.next(value);
}
/** Determine if we are in range selection mode */
get _isRangeMode() {
return !!this._rangeOptions;
}
/** Determine if this picker is the start picker */
get _isRangeStart() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.Start;
}
/** Determine if this picker is the end picker */
get _isRangeEnd() {
return this._isRangeMode && this._rangeOptions.picker === DateRangePicker.End;
}
/** Determine if the today button is disabled */
get _isTodayDisabled() {
const min = this.datepicker.min$.value;
const max = this.datepicker.max$.value;
if (!min && !max) {
return false;
}
if (min && !max) {
return isDateBefore(new Date(), min);
}
if (!min && max) {
return isDateAfter(new Date(), max);
}
return isDateBefore(new Date(), min) || isDateAfter(new Date(), max);
}
constructor() {
this.datepicker = inject(DateTimePickerService);
this._rangeService = inject(DateRangeService, { optional: true });
this._rangeOptions = inject(DateRangeOptions, { optional: true });
/** Define the aria label for the now button */
this.nowBtnAriaLabel = 'Set date to now';
/** Emits an event when the date is changed using the component. */
this.dateChange = new EventEmitter();
/** If not defined the picker will try to use the user's timezone. If that is not available, it will revert to GMT. */
this.timezoneChange = new EventEmitter();
// expose enum to view
this.DatePickerMode = DatePickerMode;
this._onDestroy = new Subject();
this.datepicker.selected$
.pipe(distinctUntilChanged(dateComparator), takeUntil(this._onDestroy))
.subscribe(date => this.dateChange.emit(date));
}
ngOnInit() {
this.setTimezone(this.datepicker.timezone$.value);
}
ngAfterViewInit() {
setTimeout(() => (this.datepicker.initialised = true));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Change the date to the current date and time
*/
setToNow() {
if (this._isRangeMode) {
const date = new Date();
if (this._isRangeStart && !this._rangeService.showTime) {
this.datepicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), 0, 0, 0);
}
else if (this._isRangeEnd && !this._rangeService.showTime) {
this.datepicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), 23, 59, 59);
}
else {
this.datepicker.setDate(date.getDate(), date.getMonth(), date.getFullYear(), this.datepicker.hours, this.datepicker.minutes, this.datepicker.seconds);
}
}
else {
// set the date to the current moment
this.datepicker.setDateToNow();
}
}
_onTimezoneChange(timezone) {
if (!timezoneComparator(this.datepicker.timezone$.value, timezone)) {
this.timezoneChange.emit(timezone);
}
}
/**
* Update the service with the new timezone value, falling back on the default if it is undefined or
* not present in `timezones`.
*/
setTimezone(timezone) {
// if the user does not provide a timezone, set it to the current timezone and emit the change
if (!timezone) {
this.datepicker.timezone$.next(this.datepicker.getDefaultTimezone());
this.timezoneChange.emit(this.datepicker.timezone$.value);
return;
}
// Check if the timezone is available in the timezones list; if not, get the default timezone
if (this.datepicker.isTimezoneAvailable(timezone)) {
this.datepicker.timezone$.next(timezone);
}
else {
// This is probably an unintended state so emit a warning
console.warn(`ux-date-time-picker: specified timezone ${JSON.stringify(timezone)} is not present in the timezones array.`);
// Fall back on the default timezone
const defaultTimezone = this.datepicker.getDefaultTimezone();
if (!this.datepicker.timezone$.value ||
!timezoneComparator(defaultTimezone, this.datepicker.timezone$.value)) {
this.datepicker.timezone$.next(defaultTimezone);
this.timezoneChange.emit(defaultTimezone);
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DateTimePickerComponent, isStandalone: false, selector: "ux-date-time-picker", inputs: { showDate: "showDate", showTime: "showTime", showTimezone: "showTimezone", showSeconds: "showSeconds", showMeridian: "showMeridian", showSpinners: "showSpinners", weekdays: "weekdays", months: "months", monthsShort: "monthsShort", meridians: "meridians", nowBtnText: "nowBtnText", showNowBtn: "showNowBtn", timezones: "timezones", startOfWeek: "startOfWeek", nowBtnAriaLabel: "nowBtnAriaLabel", date: "date", timezone: "timezone", min: "min", max: "max" }, outputs: { dateChange: "dateChange", timezoneChange: "timezoneChange" }, providers: [DateTimePickerService], ngImport: i0, template: "<div class=\"calendar-container\">\n <ux-date-time-picker-header></ux-date-time-picker-header>\n\n @if (datepicker.showDate$ | async) {\n @switch (datepicker.mode$ | async) {\n <!-- Display days in the current month -->\n @case (DatePickerMode.Day) {\n <ux-date-time-picker-day-view></ux-date-time-picker-day-view>\n }\n <!-- Display the months in the current year -->\n @case (DatePickerMode.Month) {\n <ux-date-time-picker-month-view></ux-date-time-picker-month-view>\n }\n <!-- Display a decade -->\n @case (DatePickerMode.Year) {\n <ux-date-time-picker-year-view></ux-date-time-picker-year-view>\n }\n }\n }\n\n <!-- Display a Time Picker -->\n @if (datepicker.showTime$ | async) {\n <ux-date-time-picker-time-view\n (timezoneChange)=\"_onTimezoneChange($event)\"\n ></ux-date-time-picker-time-view>\n }\n</div>\n\n@if (datepicker.showNowBtn$ | async) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"now-button\"\n [attr.aria-label]=\"nowBtnAriaLabel\"\n [disabled]=\"_isTodayDisabled\"\n (click)=\"setToNow()\"\n >\n {{ datepicker.nowBtnText$ | async }}\n </button>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: HeaderComponent, selector: "ux-date-time-picker-header" }, { kind: "component", type: DayViewComponent, selector: "ux-date-time-picker-day-view" }, { kind: "component", type: MonthViewComponent, selector: "ux-date-time-picker-month-view" }, { kind: "component", type: YearViewComponent, selector: "ux-date-time-picker-year-view" }, { kind: "component", type: TimeViewComponent, selector: "ux-date-time-picker-time-view", outputs: ["timezoneChange"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-time-picker', providers: [DateTimePickerService], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"calendar-container\">\n <ux-date-time-picker-header></ux-date-time-picker-header>\n\n @if (datepicker.showDate$ | async) {\n @switch (datepicker.mode$ | async) {\n <!-- Display days in the current month -->\n @case (DatePickerMode.Day) {\n <ux-date-time-picker-day-view></ux-date-time-picker-day-view>\n }\n <!-- Display the months in the current year -->\n @case (DatePickerMode.Month) {\n <ux-date-time-picker-month-view></ux-date-time-picker-month-view>\n }\n <!-- Display a decade -->\n @case (DatePickerMode.Year) {\n <ux-date-time-picker-year-view></ux-date-time-picker-year-view>\n }\n }\n }\n\n <!-- Display a Time Picker -->\n @if (datepicker.showTime$ | async) {\n <ux-date-time-picker-time-view\n (timezoneChange)=\"_onTimezoneChange($event)\"\n ></ux-date-time-picker-time-view>\n }\n</div>\n\n@if (datepicker.showNowBtn$ | async) {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"now-button\"\n [attr.aria-label]=\"nowBtnAriaLabel\"\n [disabled]=\"_isTodayDisabled\"\n (click)=\"setToNow()\"\n >\n {{ datepicker.nowBtnText$ | async }}\n </button>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { showDate: [{
type: Input
}], showTime: [{
type: Input
}], showTimezone: [{
type: Input
}], showSeconds: [{
type: Input
}], showMeridian: [{
type: Input
}], showSpinners: [{
type: Input
}], weekdays: [{
type: Input
}], months: [{
type: Input
}], monthsShort: [{
type: Input
}], meridians: [{
type: Input
}], nowBtnText: [{
type: Input
}], showNowBtn: [{
type: Input
}], timezones: [{
type: Input
}], startOfWeek: [{
type: Input
}], nowBtnAriaLabel: [{
type: Input
}], dateChange: [{
type: Output
}], timezoneChange: [{
type: Output
}], date: [{
type: Input
}], timezone: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}] } });
class DateFormatterPipe {
constructor() {
this._locale = inject(LOCALE_ID);
}
transform(value, formatter) {
// we may not initially have a value
if (!value) {
return '';
}
return typeof formatter === 'function'
? formatter(value)
: formatDate(value, formatter, this._locale);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipe, isStandalone: false, name: "formatDate" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipe, decorators: [{
type: Pipe,
args: [{
name: 'formatDate',
standalone: false,
}]
}] });
class DateRangePickerComponent {
/** The selected start date to be displayed in the component. */
set start(start) {
this.rangeService.start = start;
}
/** The selected end date to be displayed in the component. */
set end(end) {
this.rangeService.end = end;
}
/** Defines the aria label for the range start picker */
set startPickerAriaLabel(label) {
this.rangeService.startPickerAriaLabel = label;
}
/** Defines the aria label for the range end picker */
set endPickerAriaLabel(label) {
this.rangeService.endPickerAriaLabel = label;
}
/** Defines whether or not the time picker should be visible. */
set showTime(showTime) {
this.rangeService.showTime = showTime;
}
get showTime() {
return this.rangeService.showTime;
}
/** Calculate the number of days between the start and end date */
get _duration() {
if (this.rangeService.start && this.rangeService.end) {
return differenceBetweenDates(this.rangeService.start, this.rangeService.end, false);
}
if (this.rangeService.start && !this.rangeService.end && this.rangeService.hover) {
// apply the time from the time picker
const hoverDate = new Date(this.rangeService.hover);
hoverDate.setHours(this.rangeService.endTime.hours, this.rangeService.endTime.minutes, this.rangeService.endTime.seconds);
return this.rangeService.start.getTime() <= hoverDate.getTime()
? differenceBetweenDates(this.rangeService.start, hoverDate, false)
: null;
}
// if we only have one selected date and have a hover date
if (this.rangeService.end && !this.rangeService.start && this.rangeService.hover) {
// apply the time from the time picker
const hoverDate = new Date(this.rangeService.hover);
hoverDate.setHours(this.rangeService.startTime.hours, this.rangeService.startTime.minutes, this.rangeService.startTime.seconds);
return this.rangeService.end.getTime() >= hoverDate.getTime()
? differenceBetweenDates(this.rangeService.end, hoverDate, false)
: null;
}
}
constructor() {
this.rangeService = inject(DateRangeService);
this._destroyRef = inject(DestroyRef);
/** Expose enum to the view */
this.DateRangePicker = DateRangePicker;
/** Defines whether or not the time picker should allow the user to specify seconds. */
this.showSeconds = false;
/** Defines whether or not the time picker should show an AM/PM button, or time should be represented in 24hr format instead. */
this.showMeridian = true;
/** Defines whether or not the time picker should allow the user to select the time using spinners. */
this.showSpinners = true;
/** If defined will override the weekday names displayed. */
this.weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
/** Specify whether or not the show now button should be visible */
this.showNowBtn = false;
/** Defines the title to display above the start picker. */
this.selectStartTitle = 'Select Start Date';
/** Defines the title to display above the end picker. */
this.selectEndTitle = 'Select End Date';
/** Define the aria label for the now button */
this.nowBtnAriaLabel = 'Set date to now';
/**
* Defines the list of available timezones. The `DateTimePickerTimezone` interface specifies that each timezone should
* be an object with a `name` property that represents the timezone, eg. `GMT+2`, and an `offset` property that represents
* the number of minutes relative to GMT the timezone is.
*/
this.timezones = timezones;
/** Will set the selected start timezone. */
this.startTimezone = this.getCurrentTimezone();
/** Will set the selected end timezone. */
this.endTimezone = this.getCurrentTimezone();
/** Defines the day of the week that should appear in the first column. `WeekDay` is an enumeration available in `@angular/common`. */
this.startOfWeek = WeekDay.Sunday;
/** Define a function to return the number of days within the selected range */
this.durationTitle = this.getDurationTitle;
/** Emit when the start date changes */
this.startChange = new EventEmitter();
/** Emit when the end date changes */
this.endChange = new EventEmitter();
/** Emit when the start timezone changes. */
this.startTimezoneChange = new EventEmitter();
/** Emit when the end timezone changes. */
this.endTimezoneChange = new EventEmitter();
/** Use an observable to debounce rapid start changes */
this.startChange$ = new Subject();
/** Use an observable to debounce rapid end changes */
this.endChange$ = new Subject();
/** Unsubscribe from all observables private */
this._onDestroy = new Subject();
this.startChange$
.pipe(debounceTime(0), filter(date => date !== undefined), takeUntilDestroyed(this._destroyRef))
.subscribe(date => this.onStartChange(date));
this.endChange$
.pipe(debounceTime(0), filter(date => date !== undefined), takeUntilDestroyed(this._destroyRef))
.subscribe(date => this.onEndChange(date));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Clear the selected date range */
clear() {
this.rangeService.clear();
}
/** Get the timezone based on the machine timezone */
getCurrentTimezone() {
return this.timezones.find(timezone => timezone.offset === new Date().getTimezoneOffset());
}
onStartChange(date) {
this.rangeService.setStartDate(date);
this.startChange.emit(date);
}
onEndChange(date) {
this.rangeService.setEndDate(date);
this.endChange.emit(date);
}
/** Get the text to display to indicate the duration */
getDurationTitle(days) {
return days + ' ' + (days > 1 ? 'days' : 'day');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DateRangePickerComponent, isStandalone: false, selector: "ux-date-range-picker", inputs: { start: "start", end: "end", dateFormat: "dateFormat", timeFormat: "timeFormat", min: "min", max: "max", showTimezone: "showTimezone", showSeconds: "showSeconds", showMeridian: "showMeridian", showSpinners: "showSpinners", weekdays: "weekdays", months: "months", monthsShort: "monthsShort", meridians: "meridians", nowBtnText: "nowBtnText", showNowBtn: "showNowBtn", selectStartTitle: "selectStartTitle", selectEndTitle: "selectEndTitle", nowBtnAriaLabel: "nowBtnAriaLabel", startPickerAriaLabel: "startPickerAriaLabel", endPickerAriaLabel: "endPickerAriaLabel", showTime: "showTime", timezones: "timezones", startTimezone: "startTimezone", endTimezone: "endTimezone", startOfWeek: "startOfWeek", durationTitle: "durationTitle" }, outputs: { startChange: "startChange", endChange: "endChange", startTimezoneChange: "startTimezoneChange", endTimezoneChange: "endTimezoneChange" }, providers: [DateRangeService], ngImport: i0, template: "<div class=\"range-header\">\n <div class=\"header-section\">\n @if (!rangeService.start) {\n <div class=\"select-header\">{{ selectStartTitle }}</div>\n }\n @if (rangeService.start) {\n <div class=\"date-header\">\n {{ rangeService.start | formatDate: dateFormat || 'd MMMM y' }}\n </div>\n }\n <div\n [style.visibility]=\"rangeService.start && showTime ? 'visible' : 'hidden'\"\n class=\"time-header\"\n >\n {{ rangeService.start | formatDate: timeFormat || (showMeridian ? 'shortTime' : 'HH:mm') }}\n </div>\n </div>\n\n <div class=\"header-separator\">\n <ux-icon name=\"link-next\"></ux-icon>\n <p\n class=\"duration\"\n [style.visibility]=\"_duration !== null && _duration !== undefined ? 'visible' : 'hidden'\"\n >\n {{ durationTitle(_duration || 0) }}\n </p>\n </div>\n\n <div class=\"header-section\">\n @if (!rangeService.end) {\n <div class=\"select-header\">{{ selectEndTitle }}</div>\n }\n @if (rangeService.end) {\n <div class=\"date-header\">\n {{ rangeService.end | formatDate: dateFormat || 'd MMMM y' }}\n </div>\n }\n <div\n [style.visibility]=\"rangeService.end && showTime ? 'visible' : 'hidden'\"\n class=\"time-header\"\n >\n {{ rangeService.end | formatDate: timeFormat || (showMeridian ? 'shortTime' : 'HH:mm') }}\n </div>\n </div>\n</div>\n\n<div class=\"content\">\n <ux-date-time-picker\n uxDateRangePicker\n [picker]=\"DateRangePicker.Start\"\n class=\"start-date-picker\"\n [date]=\"rangeService.start\"\n (dateChange)=\"startChange$.next($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [weekdays]=\"weekdays\"\n [startOfWeek]=\"startOfWeek\"\n [showTime]=\"showTime\"\n [showTimezone]=\"showTimezone\"\n [showSeconds]=\"showSeconds\"\n [showMeridian]=\"showMeridian\"\n [showSpinners]=\"showSpinners\"\n [months]=\"months\"\n [monthsShort]=\"monthsShort\"\n [meridians]=\"meridians\"\n [nowBtnText]=\"nowBtnText\"\n [showNowBtn]=\"showNowBtn\"\n [nowBtnAriaLabel]=\"nowBtnAriaLabel\"\n [timezones]=\"timezones\"\n [(timezone)]=\"startTimezone\"\n (timezoneChange)=\"startTimezoneChange.emit($event)\"\n >\n </ux-date-time-picker>\n\n <ux-date-time-picker\n uxDateRangePicker\n [picker]=\"DateRangePicker.End\"\n class=\"end-date-picker\"\n [date]=\"rangeService.end\"\n (dateChange)=\"endChange$.next($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [weekdays]=\"weekdays\"\n [startOfWeek]=\"startOfWeek\"\n [showTime]=\"showTime\"\n [showTimezone]=\"showTimezone\"\n [showSeconds]=\"showSeconds\"\n [showMeridian]=\"showMeridian\"\n [showSpinners]=\"showSpinners\"\n [months]=\"months\"\n [monthsShort]=\"monthsShort\"\n [meridians]=\"meridians\"\n [nowBtnText]=\"nowBtnText\"\n [showNowBtn]=\"showNowBtn\"\n [nowBtnAriaLabel]=\"nowBtnAriaLabel\"\n [timezones]=\"timezones\"\n [(timezone)]=\"endTimezone\"\n (timezoneChange)=\"endTimezoneChange.emit($event)\"\n >\n </ux-date-time-picker>\n</div>\n", dependencies: [{ kind: "component", type: DateTimePickerComponent, selector: "ux-date-time-picker", inputs: ["showDate", "showTime", "showTimezone", "showSeconds", "showMeridian", "showSpinners", "weekdays", "months", "monthsShort", "meridians", "nowBtnText", "showNowBtn", "timezones", "startOfWeek", "nowBtnAriaLabel", "date", "timezone", "min", "max"], outputs: ["dateChange", "timezoneChange"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: DateRangePickerDirective, selector: "[uxDateRangePicker]", inputs: ["picker"] }, { kind: "pipe", type: DateFormatterPipe, name: "formatDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-date-range-picker', changeDetection: ChangeDetectionStrategy.OnPush, providers: [DateRangeService], standalone: false, template: "<div class=\"range-header\">\n <div class=\"header-section\">\n @if (!rangeService.start) {\n <div class=\"select-header\">{{ selectStartTitle }}</div>\n }\n @if (rangeService.start) {\n <div class=\"date-header\">\n {{ rangeService.start | formatDate: dateFormat || 'd MMMM y' }}\n </div>\n }\n <div\n [style.visibility]=\"rangeService.start && showTime ? 'visible' : 'hidden'\"\n class=\"time-header\"\n >\n {{ rangeService.start | formatDate: timeFormat || (showMeridian ? 'shortTime' : 'HH:mm') }}\n </div>\n </div>\n\n <div class=\"header-separator\">\n <ux-icon name=\"link-next\"></ux-icon>\n <p\n class=\"duration\"\n [style.visibility]=\"_duration !== null && _duration !== undefined ? 'visible' : 'hidden'\"\n >\n {{ durationTitle(_duration || 0) }}\n </p>\n </div>\n\n <div class=\"header-section\">\n @if (!rangeService.end) {\n <div class=\"select-header\">{{ selectEndTitle }}</div>\n }\n @if (rangeService.end) {\n <div class=\"date-header\">\n {{ rangeService.end | formatDate: dateFormat || 'd MMMM y' }}\n </div>\n }\n <div\n [style.visibility]=\"rangeService.end && showTime ? 'visible' : 'hidden'\"\n class=\"time-header\"\n >\n {{ rangeService.end | formatDate: timeFormat || (showMeridian ? 'shortTime' : 'HH:mm') }}\n </div>\n </div>\n</div>\n\n<div class=\"content\">\n <ux-date-time-picker\n uxDateRangePicker\n [picker]=\"DateRangePicker.Start\"\n class=\"start-date-picker\"\n [date]=\"rangeService.start\"\n (dateChange)=\"startChange$.next($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [weekdays]=\"weekdays\"\n [startOfWeek]=\"startOfWeek\"\n [showTime]=\"showTime\"\n [showTimezone]=\"showTimezone\"\n [showSeconds]=\"showSeconds\"\n [showMeridian]=\"showMeridian\"\n [showSpinners]=\"showSpinners\"\n [months]=\"months\"\n [monthsShort]=\"monthsShort\"\n [meridians]=\"meridians\"\n [nowBtnText]=\"nowBtnText\"\n [showNowBtn]=\"showNowBtn\"\n [nowBtnAriaLabel]=\"nowBtnAriaLabel\"\n [timezones]=\"timezones\"\n [(timezone)]=\"startTimezone\"\n (timezoneChange)=\"startTimezoneChange.emit($event)\"\n >\n </ux-date-time-picker>\n\n <ux-date-time-picker\n uxDateRangePicker\n [picker]=\"DateRangePicker.End\"\n class=\"end-date-picker\"\n [date]=\"rangeService.end\"\n (dateChange)=\"endChange$.next($event)\"\n [min]=\"min\"\n [max]=\"max\"\n [weekdays]=\"weekdays\"\n [startOfWeek]=\"startOfWeek\"\n [showTime]=\"showTime\"\n [showTimezone]=\"showTimezone\"\n [showSeconds]=\"showSeconds\"\n [showMeridian]=\"showMeridian\"\n [showSpinners]=\"showSpinners\"\n [months]=\"months\"\n [monthsShort]=\"monthsShort\"\n [meridians]=\"meridians\"\n [nowBtnText]=\"nowBtnText\"\n [showNowBtn]=\"showNowBtn\"\n [nowBtnAriaLabel]=\"nowBtnAriaLabel\"\n [timezones]=\"timezones\"\n [(timezone)]=\"endTimezone\"\n (timezoneChange)=\"endTimezoneChange.emit($event)\"\n >\n </ux-date-time-picker>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { start: [{
type: Input
}], end: [{
type: Input
}], dateFormat: [{
type: Input
}], timeFormat: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], showTimezone: [{
type: Input
}], showSeconds: [{
type: Input
}], showMeridian: [{
type: Input
}], showSpinners: [{
type: Input
}], weekdays: [{
type: Input
}], months: [{
type: Input
}], monthsShort: [{
type: Input
}], meridians: [{
type: Input
}], nowBtnText: [{
type: Input
}], showNowBtn: [{
type: Input
}], selectStartTitle: [{
type: Input
}], selectEndTitle: [{
type: Input
}], nowBtnAriaLabel: [{
type: Input
}], startPickerAriaLabel: [{
type: Input
}], endPickerAriaLabel: [{
type: Input
}], showTime: [{
type: Input
}], timezones: [{
type: Input
}], startTimezone: [{
type: Input
}], endTimezone: [{
type: Input
}], startOfWeek: [{
type: Input
}], durationTitle: [{
type: Input
}], startChange: [{
type: Output
}], endChange: [{
type: Output
}], startTimezoneChange: [{
type: Output
}], endTimezoneChange: [{
type: Output
}] } });
class DateFormatterPipeModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipeModule, declarations: [DateFormatterPipe], exports: [DateFormatterPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipeModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateFormatterPipeModule, decorators: [{
type: NgModule,
args: [{
exports: [DateFormatterPipe],
declarations: [DateFormatterPipe],
}]
}] });
class FocusIfModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIfModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FocusIfModule, declarations: [FocusIfDirective], exports: [FocusIfDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIfModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FocusIfModule, decorators: [{
type: NgModule,
args: [{
exports: [FocusIfDirective],
declarations: [FocusIfDirective],
}]
}] });
class SpinButtonModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonModule, declarations: [SpinButtonComponent], imports: [AccessibilityModule, CommonModule, FormsModule, IconModule], exports: [SpinButtonComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonModule, imports: [AccessibilityModule, CommonModule, FormsModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SpinButtonModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, FormsModule, IconModule],
exports: [SpinButtonComponent],
declarations: [SpinButtonComponent],
}]
}] });
class TimePickerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimePickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TimePickerModule, declarations: [TimePickerComponent], imports: [AccessibilityModule, CommonModule, FormsModule, SpinButtonModule], exports: [TimePickerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimePickerModule, imports: [AccessibilityModule, CommonModule, FormsModule, SpinButtonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimePickerModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, FormsModule, SpinButtonModule],
exports: [TimePickerComponent],
declarations: [TimePickerComponent],
}]
}] });
class DateTimePickerModule {
static forRoot() {
return {
ngModule: DateTimePickerModule,
providers: [DateTimePickerConfig],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerModule, declarations: [DateTimePickerComponent,
HeaderComponent,
DayViewComponent,
MonthViewComponent,
YearViewComponent,
TimeViewComponent,
WeekDaySortPipe], imports: [A11yModule,
AccessibilityModule,
CommonModule,
FocusIfModule,
FormsModule,
IconModule,
SpinButtonModule,
TimePickerModule], exports: [DateTimePickerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerModule, imports: [A11yModule,
AccessibilityModule,
CommonModule,
FocusIfModule,
FormsModule,
IconModule,
SpinButtonModule,
TimePickerModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateTimePickerModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
CommonModule,
FocusIfModule,
FormsModule,
IconModule,
SpinButtonModule,
TimePickerModule,
],
exports: [DateTimePickerComponent],
declarations: [
DateTimePickerComponent,
HeaderComponent,
DayViewComponent,
MonthViewComponent,
YearViewComponent,
TimeViewComponent,
WeekDaySortPipe,
],
}]
}] });
class DateRangePickerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerModule, declarations: [DateRangePickerComponent, DateRangePickerDirective], imports: [CommonModule, DateTimePickerModule, IconModule, DateFormatterPipeModule], exports: [DateRangePickerComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerModule, imports: [CommonModule, DateTimePickerModule, IconModule, DateFormatterPipeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DateRangePickerModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, DateTimePickerModule, IconModule, DateFormatterPipeModule],
declarations: [DateRangePickerComponent, DateRangePickerDirective],
exports: [DateRangePickerComponent],
}]
}] });
class EboxComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: EboxComponent, isStandalone: false, selector: "ux-ebox", ngImport: i0, template: "<div class=\"ux-ebox-header\">\n <ng-content select=\"ux-ebox-header\"></ng-content>\n</div>\n\n<div class=\"ux-ebox-content\">\n <ng-content select=\"ux-ebox-content\"></ng-content>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-ebox', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"ux-ebox-header\">\n <ng-content select=\"ux-ebox-header\"></ng-content>\n</div>\n\n<div class=\"ux-ebox-content\">\n <ng-content select=\"ux-ebox-content\"></ng-content>\n</div>\n" }]
}] });
class EboxHeaderDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: EboxHeaderDirective, isStandalone: false, selector: "ux-ebox-header", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-ebox-header',
standalone: false,
}]
}] });
class EboxContentDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: EboxContentDirective, isStandalone: false, selector: "ux-ebox-content", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxContentDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-ebox-content',
standalone: false,
}]
}] });
class EboxModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: EboxModule, declarations: [EboxComponent, EboxContentDirective, EboxHeaderDirective], exports: [EboxComponent, EboxContentDirective, EboxHeaderDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: EboxModule, decorators: [{
type: NgModule,
args: [{
exports: [EboxComponent, EboxContentDirective, EboxHeaderDirective],
declarations: [EboxComponent, EboxContentDirective, EboxHeaderDirective],
}]
}] });
class FacetHeaderComponent {
constructor() {
this.focusIndicatorService = inject(FocusIndicatorService);
this.elementRef = inject(ElementRef);
/** Defines whether or not clicking on the header will toggle the expanded state. */
this.canExpand = true;
/** Can be used to set the initial expanded state. */
this.expanded = true;
/** If two-way binding is used it will be updated when the expanded state changes. */
this.expandedChange = new EventEmitter();
this._focusIndicator = this.focusIndicatorService.monitor(this.elementRef.nativeElement);
}
ngOnDestroy() {
this._focusIndicator.destroy();
}
toggleExpand() {
// if not expandable then do nothing
if (this.canExpand) {
this.expanded = !this.expanded;
this.expandedChange.emit(this.expanded);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetHeaderComponent, isStandalone: false, selector: "ux-facet-header", inputs: { header: "header", canExpand: "canExpand", expanded: "expanded" }, outputs: { expandedChange: "expandedChange" }, host: { attributes: { "role": "button", "tabindex": "0" }, listeners: { "click": "toggleExpand()", "keyup.enter": "toggleExpand()" }, properties: { "attr.aria-expanded": "expanded", "attr.aria-label": "header + ' Facet: Activate to ' + (expanded ? 'collapse' : 'expand')", "class.expanded": "this.expanded" } }, ngImport: i0, template: "<span class=\"facet-header-title\">{{ header }}</span>\n@if (canExpand) {\n <ux-icon [name]=\"expanded ? 'down' : 'previous'\" class=\"facet-header-icon\"></ux-icon>\n}\n", dependencies: [{ kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetHeaderComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-header', host: {
role: 'button',
tabindex: '0',
'(click)': 'toggleExpand()',
'(keyup.enter)': 'toggleExpand()',
'[attr.aria-expanded]': 'expanded',
'[attr.aria-label]': "header + ' Facet: Activate to ' + (expanded ? 'collapse' : 'expand')",
}, standalone: false, template: "<span class=\"facet-header-title\">{{ header }}</span>\n@if (canExpand) {\n <ux-icon [name]=\"expanded ? 'down' : 'previous'\" class=\"facet-header-icon\"></ux-icon>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { header: [{
type: Input
}], canExpand: [{
type: Input
}], expanded: [{
type: Input
}, {
type: HostBinding,
args: ['class.expanded']
}], expandedChange: [{
type: Output
}] } });
class FacetSelect {
constructor(facet) {
this.facet = facet;
}
}
class FacetDeselect {
constructor(facet) {
this.facet = facet;
}
}
class FacetDeselectAll {
}
class FacetService {
constructor() {
/** The list of active facets */
this.facets$ = new BehaviorSubject([]);
/** Emit all the events when they occur */
this.events$ = new Subject();
}
select(facet) {
// if the facet is already selected or disabled then do nothing
if (this.isSelected(facet) || facet.disabled) {
return;
}
// update the list of active facets
this.facets$.next([...this.facets$.value, facet]);
// emit the event
this.events$.next(new FacetSelect(facet));
}
deselect(facet) {
// if the facet is not selected then do nothing
if (!this.isSelected(facet)) {
return;
}
// update the list of active facets
this.facets$.next(this.facets$.value.filter(_selectedFacet => !this.isFacetMatch(_selectedFacet, facet)));
// emit the event
this.events$.next(new FacetDeselect(facet));
}
deselectAll() {
// empty the list of active facets
this.facets$.next([]);
// emit the event
this.events$.next(new FacetDeselectAll());
}
toggle(facet) {
this.isSelected(facet) ? this.deselect(facet) : this.select(facet);
}
isSelected(facet) {
return !!this.facets$.value.find(_selectedFacet => this.isFacetMatch(_selectedFacet, facet));
}
isFacetMatch(facet1, facet2) {
if (facet1.id === undefined || facet2.id === undefined) {
return facet1 === facet2;
}
return facet1.id === facet2.id;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetService, decorators: [{
type: Injectable
}] });
class FacetCheckListItemComponent {
constructor() {
this.facet = null;
this.selected = false;
this.tabbable = false;
this.simplified = false;
this.selectedChange = new EventEmitter();
this.itemFocus = new EventEmitter();
this.itemBlur = new EventEmitter();
}
get disabled() {
return this.facet && this.facet.disabled;
}
getLabel() {
return this.facet ? this.facet.title : '';
}
focus() {
this.option.nativeElement.focus();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetCheckListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetCheckListItemComponent, isStandalone: false, selector: "ux-facet-check-list-item", inputs: { id: "id", facet: "facet", selected: "selected", tabbable: "tabbable", simplified: "simplified" }, outputs: { selectedChange: "selectedChange", itemFocus: "itemFocus", itemBlur: "itemBlur" }, host: { properties: { "id": "this.id" } }, viewQueries: [{ propertyName: "option", first: true, predicate: ["option"], descendants: true, static: true }], ngImport: i0, template: "<div\n #option\n uxFocusIndicator\n class=\"facet-check-list-item\"\n [class.facet-active]=\"selected\"\n [attr.aria-checked]=\"selected\"\n role=\"option\"\n [tabindex]=\"tabbable ? 0 : -1\"\n (focus)=\"itemFocus.emit()\"\n (blur)=\"itemBlur.emit()\"\n (click)=\"selectedChange.emit(facet)\"\n (keydown.enter)=\"selectedChange.emit(facet)\"\n (keydown.space)=\"selectedChange.emit(facet); $event.preventDefault()\"\n (keydown.spacebar)=\"selectedChange.emit(facet); $event.preventDefault()\"\n [class.disabled]=\"facet?.disabled\"\n>\n <!-- Show check icon to indicate the state -->\n <ux-checkbox\n [clickable]=\"false\"\n [value]=\"selected\"\n [simplified]=\"simplified\"\n [tabindex]=\"-1\"\n [disabled]=\"disabled\"\n [id]=\"id + '-checkbox'\"\n >\n <span class=\"facet-check-list-item-title\">{{ facet?.title }}</span>\n @if (facet?.count !== undefined) {\n <span class=\"facet-check-list-item-count\">({{ facet?.count }})</span>\n }\n </ux-checkbox>\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: CheckboxComponent, selector: "ux-checkbox", inputs: ["id", "name", "value", "required", "tabindex", "clickable", "simplified", "indeterminateValue", "disabled", "aria-label", "aria-labelledby"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetCheckListItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-check-list-item', changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, standalone: false, template: "<div\n #option\n uxFocusIndicator\n class=\"facet-check-list-item\"\n [class.facet-active]=\"selected\"\n [attr.aria-checked]=\"selected\"\n role=\"option\"\n [tabindex]=\"tabbable ? 0 : -1\"\n (focus)=\"itemFocus.emit()\"\n (blur)=\"itemBlur.emit()\"\n (click)=\"selectedChange.emit(facet)\"\n (keydown.enter)=\"selectedChange.emit(facet)\"\n (keydown.space)=\"selectedChange.emit(facet); $event.preventDefault()\"\n (keydown.spacebar)=\"selectedChange.emit(facet); $event.preventDefault()\"\n [class.disabled]=\"facet?.disabled\"\n>\n <!-- Show check icon to indicate the state -->\n <ux-checkbox\n [clickable]=\"false\"\n [value]=\"selected\"\n [simplified]=\"simplified\"\n [tabindex]=\"-1\"\n [disabled]=\"disabled\"\n [id]=\"id + '-checkbox'\"\n >\n <span class=\"facet-check-list-item-title\">{{ facet?.title }}</span>\n @if (facet?.count !== undefined) {\n <span class=\"facet-check-list-item-count\">({{ facet?.count }})</span>\n }\n </ux-checkbox>\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}, {
type: HostBinding
}], facet: [{
type: Input
}], selected: [{
type: Input
}], tabbable: [{
type: Input
}], simplified: [{
type: Input
}], selectedChange: [{
type: Output
}], itemFocus: [{
type: Output
}], itemBlur: [{
type: Output
}], option: [{
type: ViewChild,
args: ['option', { static: true }]
}] } });
let uniqueId$a = 0;
class FacetCheckListComponent {
/** This will allow you to define an initial set of selected facets. */
set selected(selection) {
if (Array.isArray(selection)) {
selection.forEach(facet => this.facetService.select(facet));
}
}
constructor() {
this.facetService = inject(FacetService);
this.id = `ux-facet-check-list-${uniqueId$a++}`;
/** Defines the complete list of facets that can be selected. */
this.facets = [];
/** If `false` the list will grow to display all possible facets. If `true` a scrollbar will appear to prevent the list from growing too large. */
this.scrollbar = true;
/** Defines whether or not the checkboxes will appear in simplified form. */
this.simplified = false;
/** Defines whether or not the Facet Check List should be initially expanded or not. */
this.expanded = true;
/**
* This will be triggered when a facet is selected, deselected or all facets are deselected.
* The event will be an instance of either `FacetSelect`, `FacetDeselect` or `FacetDeselectAll` and
* will contain the facet being selected or deselected in a `facet` property (deselect all will not contain affected facets).
*/
this.events = new Subject();
/** If two-way binding is used this array will get updated any time the selected facets change. */
this.selectedChange = new EventEmitter();
this.isFocused = false;
this.activeIndex = 0;
this._onDestroy = new Subject();
this.facetService.events$.pipe(takeUntil(this._onDestroy)).subscribe(event => {
// deselect all events should always be emitted
if (event instanceof FacetDeselectAll) {
this.events.next(event);
this.selectedChange.next([]);
}
// selection and deselection events should only be emitted when the facet belongs to this component
if ((event instanceof FacetSelect || event instanceof FacetDeselect) &&
this.isOwnFacet(event.facet)) {
this.events.next(event);
this.selectedChange.next(this.getSelectedFacets());
}
});
}
ngAfterViewInit() {
this._focusKeyManager = new FocusKeyManager(this.options).withVerticalOrientation();
this._focusKeyManager.change
.pipe(takeUntil(this._onDestroy))
.subscribe(index => (this.activeIndex = index));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onFocus(index) {
if (this._focusKeyManager.activeItemIndex === -1) {
this._focusKeyManager.setActiveItem(index);
}
}
onKeydown(event) {
this._focusKeyManager.onKeydown(event);
}
toggleFacet(index, facet) {
this.facetService.toggle(facet);
this._focusKeyManager.setActiveItem(index);
}
getSelectedFacets() {
return this.facetService.facets$.value.filter(facet => this.isOwnFacet(facet));
}
isOwnFacet(facet) {
return this.facets.indexOf(facet) !== -1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetCheckListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetCheckListComponent, isStandalone: false, selector: "ux-facet-check-list", inputs: { id: "id", selected: "selected", facets: "facets", header: "header", scrollbar: "scrollbar", simplified: "simplified", expanded: "expanded" }, outputs: { events: "events", selectedChange: "selectedChange" }, host: { properties: { "id": "this.id" } }, viewQueries: [{ propertyName: "options", predicate: FacetCheckListItemComponent, descendants: true }], ngImport: i0, template: "<ux-facet-header [header]=\"header\" [(expanded)]=\"expanded\"></ux-facet-header>\n\n<!-- Create a container which will show when section is expanded -->\n@if (expanded) {\n <div\n class=\"facet-check-list-container\"\n tabindex=\"-1\"\n role=\"listbox\"\n [class.facet-check-list-scrollbar]=\"scrollbar\"\n [class.facet-check-list-scrollbar-focused]=\"isFocused\"\n >\n <!-- Iterate through each possible facet -->\n @for (facet of facets; track facet; let index = $index) {\n <ux-facet-check-list-item\n [id]=\"id + '-check-list-item-' + index\"\n [facet]=\"facet\"\n [simplified]=\"simplified\"\n [tabbable]=\"activeIndex === index\"\n [selected]=\"facetService.isSelected(facet)\"\n (selectedChange)=\"toggleFacet(index, facet)\"\n (keydown)=\"onKeydown($event)\"\n (itemFocus)=\"isFocused = true; onFocus(index)\"\n (itemBlur)=\"isFocused = false\"\n >\n </ux-facet-check-list-item>\n }\n </div>\n}\n", dependencies: [{ kind: "component", type: FacetHeaderComponent, selector: "ux-facet-header", inputs: ["header", "canExpand", "expanded"], outputs: ["expandedChange"] }, { kind: "component", type: FacetCheckListItemComponent, selector: "ux-facet-check-list-item", inputs: ["id", "facet", "selected", "tabbable", "simplified"], outputs: ["selectedChange", "itemFocus", "itemBlur"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetCheckListComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-check-list', standalone: false, template: "<ux-facet-header [header]=\"header\" [(expanded)]=\"expanded\"></ux-facet-header>\n\n<!-- Create a container which will show when section is expanded -->\n@if (expanded) {\n <div\n class=\"facet-check-list-container\"\n tabindex=\"-1\"\n role=\"listbox\"\n [class.facet-check-list-scrollbar]=\"scrollbar\"\n [class.facet-check-list-scrollbar-focused]=\"isFocused\"\n >\n <!-- Iterate through each possible facet -->\n @for (facet of facets; track facet; let index = $index) {\n <ux-facet-check-list-item\n [id]=\"id + '-check-list-item-' + index\"\n [facet]=\"facet\"\n [simplified]=\"simplified\"\n [tabbable]=\"activeIndex === index\"\n [selected]=\"facetService.isSelected(facet)\"\n (selectedChange)=\"toggleFacet(index, facet)\"\n (keydown)=\"onKeydown($event)\"\n (itemFocus)=\"isFocused = true; onFocus(index)\"\n (itemBlur)=\"isFocused = false\"\n >\n </ux-facet-check-list-item>\n }\n </div>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { id: [{
type: Input
}, {
type: HostBinding
}], selected: [{
type: Input
}], facets: [{
type: Input
}], header: [{
type: Input
}], scrollbar: [{
type: Input
}], simplified: [{
type: Input
}], expanded: [{
type: Input
}], events: [{
type: Output
}], selectedChange: [{
type: Output
}], options: [{
type: ViewChildren,
args: [FacetCheckListItemComponent]
}] } });
class FacetClearButtonDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetClearButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FacetClearButtonDirective, isStandalone: false, selector: "[uxFacetClearButton]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetClearButtonDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFacetClearButton]',
standalone: false,
}]
}] });
class FacetContainerComponent {
/** Allows a predefined set of Facets to be displayed. */
set facets(facets) {
this.facetService.facets$.next(facets);
}
get facets() {
return this.facetService.facets$.value;
}
constructor() {
this.facetService = inject(FacetService);
this._announcer = inject(LiveAnnouncer);
/** Defines the text displayed at the top of the Facet Container. */
this.header = 'Selected';
/** Defines the text to display in the tooltip when hovering over the clear all button. */
this.clearTooltip = 'Clear All';
/** Defines the text to display when there are no selected facets. */
this.emptyText = 'No Items';
/** Determines if the facets can be reordered. */
this.facetsReorderable = false;
/** Defines the aria-label for the clear all button. */
this.clearAriaLabel = 'Clear All';
/** Defines the aria-label for the deselect facet button.. */
this.deselectFacetAriaLabel = 'Deselect Facet';
/** If using two-way binding this array will update when the selected facets change. */
this.facetsChange = new EventEmitter();
/**
* This will be triggered when a facet is selected, deselected or all facets are deselected.
* The event will be an instance of either `FacetSelect`, `FacetDeselect` or `FacetDeselectAll` and
* will contain the facet being selected or deselected in a `facet` property
* (deselect all will not contain affected facets). */
this.events = new EventEmitter();
this.target = null;
this.source = null;
this.dragRef = null;
this._onDestroy = new Subject();
this.facetService.facets$.subscribe(facets => this.facetsChange.next(facets));
this.facetService.events$.subscribe(event => this.triggerEvent(event));
// announce deselection
this.facetService.events$
.pipe(filter(event => event instanceof FacetDeselect))
.subscribe(event => this._announcer.announce(`Option ${event.facet.title} deselected.`, 'assertive'));
}
ngAfterViewInit() {
const placeholderElement = this.placeholder.element.nativeElement;
placeholderElement.style.display = 'none';
placeholderElement.parentNode.removeChild(placeholderElement);
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onDropListDropped() {
if (!this.target) {
return;
}
const placeholderElement = this.placeholder.element.nativeElement;
const placeholderParentElement = placeholderElement.parentElement;
placeholderElement.style.display = 'none';
placeholderParentElement.removeChild(placeholderElement);
placeholderParentElement.appendChild(placeholderElement);
placeholderParentElement.insertBefore(this.source.element.nativeElement, placeholderParentElement.children[this.sourceIndex]);
if (this.placeholder._dropListRef.isDragging()) {
this.placeholder._dropListRef.exit(this.dragRef);
}
this.target = null;
this.source = null;
this.dragRef = null;
if (this.sourceIndex !== this.targetIndex) {
moveItemInArray(this.facets, this.sourceIndex, this.targetIndex);
}
}
onDropListEntered({ item, container }) {
if (container == this.placeholder) {
return;
}
const placeholderElement = this.placeholder.element.nativeElement;
const sourceElement = item.dropContainer.element.nativeElement;
const dropElement = container.element.nativeElement;
const dragIndex = Array.prototype.indexOf.call(dropElement.parentElement.children, this.source ? placeholderElement : sourceElement);
const dropIndex = Array.prototype.indexOf.call(dropElement.parentElement.children, dropElement);
if (!this.source) {
this.sourceIndex = dragIndex;
this.source = item.dropContainer;
sourceElement.parentElement.removeChild(sourceElement);
}
this.targetIndex = dropIndex;
this.target = container;
this.dragRef = item._dragRef;
placeholderElement.style.display = '';
dropElement.parentElement.insertBefore(placeholderElement, dropIndex > dragIndex ? dropElement.nextSibling : dropElement);
this.placeholder._dropListRef.enter(item._dragRef, item.element.nativeElement.offsetLeft, item.element.nativeElement.offsetTop);
}
selectFacet(facet) {
this.facetService.select(facet);
}
deselectFacet(facet, tag) {
// find the index of the item in the selected array
const idx = this.facets.findIndex(selectedFacet => facet === selectedFacet);
// if match there was no match then finish
if (idx === -1) {
return;
}
// remove the last item
this.facetService.deselect(facet);
// announce the facet removal
this._announcer.announce(`Option ${facet.title} deselected.`, 'assertive');
// focus another tag if there is one
if (tag) {
const sibling = tag.previousElementSibling || tag.nextElementSibling;
// if there is a sibling then focus it
if (sibling) {
sibling.focus();
}
}
}
deselectAllFacets() {
// empty the selected array
this.facetService.deselectAll();
// announce the facet removal
this._announcer.announce('All options deselected.', 'assertive');
}
trackBy(_index, facet) {
return facet.id || facet.title;
}
shiftRight(facet, element) {
// only move the item if reordering is allowed
if (this.facetsReorderable === false) {
return;
}
// perform the movement
this.shiftFacet(facet, 1);
// the item may become unfocused during the reorder so we should refocus it
requestAnimationFrame(() => element.focus());
// announce the move
this._announcer.announce(`Option ${facet.title} moved down.`);
}
shiftLeft(facet, element) {
// only move the item if reordering is allowed
if (this.facetsReorderable === false) {
return;
}
// perform the movement
this.shiftFacet(facet, -1);
// the item may become unfocused during the reorder so we should refocus it
requestAnimationFrame(() => element.focus());
// announce the move
this._announcer.announce(`Option ${facet.title} moved up.`);
}
shiftFacet(facet, distance) {
const index = this.facets.indexOf(facet);
const target = index + distance;
// Ensure the move is valid
if (target < 0 || target === this.facets.length) {
return;
}
// Perform the move
this.facets.splice(index, 1);
this.facets.splice(target, 0, facet);
}
triggerEvent(event) {
this.events.next(event);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetContainerComponent, isStandalone: false, selector: "ux-facet-container", inputs: { header: "header", clearTooltip: "clearTooltip", emptyText: "emptyText", facetsReorderable: "facetsReorderable", facets: "facets", clearAriaLabel: "clearAriaLabel", deselectFacetAriaLabel: "deselectFacetAriaLabel" }, outputs: { facetsChange: "facetsChange", events: "events" }, providers: [FacetService], queries: [{ propertyName: "clearButton", first: true, predicate: FacetClearButtonDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "placeholder", first: true, predicate: CdkDropList, descendants: true }], ngImport: i0, template: "<!-- Display Any Selected Facets -->\n<div class=\"facets-selected-container\">\n <!-- Display Title an Clear Button -->\n <div class=\"facets-selected-header-container\">\n <!-- Show The Selected Text -->\n <span class=\"facets-selected-header-label\">{{ header }}</span>\n\n <!-- Add a Clear Button -->\n @if ((facetService.facets$ | async).length > 0) {\n <ng-container [ngTemplateOutlet]=\"clearButton || clearButtonDefault\"></ng-container>\n }\n </div>\n\n <!-- Display Tags For Selected Items -->\n <div class=\"facets-selected-list\" cdkDropListGroup role=\"list\">\n <div\n cdkDropList\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListDropped)=\"onDropListDropped()\"\n ></div>\n\n <!-- Show Selected Tags -->\n @for (facet of facetService.facets$ | async; track trackBy($index, facet)) {\n <div\n #tag\n class=\"facet-selected-tag\"\n role=\"listitem\"\n tabindex=\"0\"\n cdkDropList\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListDropped)=\"onDropListDropped()\"\n [attr.aria-label]=\"facet.title\"\n (mousedown)=\"tag.focus()\"\n (touchmove)=\"$event.preventDefault()\"\n (keydown.ArrowRight)=\"shiftRight(facet, tag)\"\n (keydown.ArrowLeft)=\"shiftLeft(facet, tag)\"\n >\n @if (facetsReorderable) {\n <div class=\"facet-drag-item\" cdkDrag>\n <!-- Display Label -->\n <span class=\"facet-selected-tag-label\">{{ facet.title }}</span>\n <!-- Display Remove Icon -->\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"facet-selected-remove-btn\"\n [attr.aria-label]=\"deselectFacetAriaLabel\"\n (click)=\"deselectFacet(facet, tag)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n </div>\n } @else {\n <span class=\"facet-selected-tag-label\">{{ facet.title }}</span>\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"facet-selected-remove-btn\"\n [attr.aria-label]=\"deselectFacetAriaLabel\"\n (click)=\"deselectFacet(facet, tag)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Show Message Here if No Facets Selected -->\n @if (emptyText && (facetService.facets$ | async).length === 0) {\n <p class=\"facets-selected-none-label\">{{ emptyText }}</p>\n }\n</div>\n\n<!-- Any Facet Elements Should be Added Here By User -->\n<div class=\"facets-region\">\n <ng-content></ng-content>\n</div>\n\n<ng-template #clearButtonDefault>\n <button\n type=\"button\"\n class=\"btn btn-link btn-icon button-secondary\"\n tabindex=\"0\"\n [attr.aria-label]=\"clearAriaLabel\"\n [uxTooltip]=\"clearTooltip\"\n placement=\"left\"\n (click)=\"deselectAllFacets()\"\n >\n <svg\n class=\"facets-selected-clear-graphic\"\n focusable=\"false\"\n viewBox=\"0 0 19 12\"\n shape-rendering=\"geometricPrecision\"\n >\n <rect class=\"light-grey\" x=\"0\" y=\"2\" width=\"7\" height=\"2\"></rect>\n <rect class=\"dark-grey\" x=\"0\" y=\"5\" width=\"9\" height=\"2\"></rect>\n <rect class=\"light-grey\" x=\"0\" y=\"8\" width=\"7\" height=\"2\"></rect>\n <path class=\"dark-grey\" d=\"M9,1 h1 l9,9 v1 h-1 l-9,-9 v-1 Z\"></path>\n <path class=\"dark-grey\" d=\"M9,11 v-1 l9,-9 h1 v1 l-9,9 h-1 Z\"></path>\n </svg>\n </button>\n</ng-template>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "directive", type: i6.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i6.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i6.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetContainerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-container', providers: [FacetService], preserveWhitespaces: false, standalone: false, template: "<!-- Display Any Selected Facets -->\n<div class=\"facets-selected-container\">\n <!-- Display Title an Clear Button -->\n <div class=\"facets-selected-header-container\">\n <!-- Show The Selected Text -->\n <span class=\"facets-selected-header-label\">{{ header }}</span>\n\n <!-- Add a Clear Button -->\n @if ((facetService.facets$ | async).length > 0) {\n <ng-container [ngTemplateOutlet]=\"clearButton || clearButtonDefault\"></ng-container>\n }\n </div>\n\n <!-- Display Tags For Selected Items -->\n <div class=\"facets-selected-list\" cdkDropListGroup role=\"list\">\n <div\n cdkDropList\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListDropped)=\"onDropListDropped()\"\n ></div>\n\n <!-- Show Selected Tags -->\n @for (facet of facetService.facets$ | async; track trackBy($index, facet)) {\n <div\n #tag\n class=\"facet-selected-tag\"\n role=\"listitem\"\n tabindex=\"0\"\n cdkDropList\n (cdkDropListEntered)=\"onDropListEntered($event)\"\n (cdkDropListDropped)=\"onDropListDropped()\"\n [attr.aria-label]=\"facet.title\"\n (mousedown)=\"tag.focus()\"\n (touchmove)=\"$event.preventDefault()\"\n (keydown.ArrowRight)=\"shiftRight(facet, tag)\"\n (keydown.ArrowLeft)=\"shiftLeft(facet, tag)\"\n >\n @if (facetsReorderable) {\n <div class=\"facet-drag-item\" cdkDrag>\n <!-- Display Label -->\n <span class=\"facet-selected-tag-label\">{{ facet.title }}</span>\n <!-- Display Remove Icon -->\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"facet-selected-remove-btn\"\n [attr.aria-label]=\"deselectFacetAriaLabel\"\n (click)=\"deselectFacet(facet, tag)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n </div>\n } @else {\n <span class=\"facet-selected-tag-label\">{{ facet.title }}</span>\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"facet-selected-remove-btn\"\n [attr.aria-label]=\"deselectFacetAriaLabel\"\n (click)=\"deselectFacet(facet, tag)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Show Message Here if No Facets Selected -->\n @if (emptyText && (facetService.facets$ | async).length === 0) {\n <p class=\"facets-selected-none-label\">{{ emptyText }}</p>\n }\n</div>\n\n<!-- Any Facet Elements Should be Added Here By User -->\n<div class=\"facets-region\">\n <ng-content></ng-content>\n</div>\n\n<ng-template #clearButtonDefault>\n <button\n type=\"button\"\n class=\"btn btn-link btn-icon button-secondary\"\n tabindex=\"0\"\n [attr.aria-label]=\"clearAriaLabel\"\n [uxTooltip]=\"clearTooltip\"\n placement=\"left\"\n (click)=\"deselectAllFacets()\"\n >\n <svg\n class=\"facets-selected-clear-graphic\"\n focusable=\"false\"\n viewBox=\"0 0 19 12\"\n shape-rendering=\"geometricPrecision\"\n >\n <rect class=\"light-grey\" x=\"0\" y=\"2\" width=\"7\" height=\"2\"></rect>\n <rect class=\"dark-grey\" x=\"0\" y=\"5\" width=\"9\" height=\"2\"></rect>\n <rect class=\"light-grey\" x=\"0\" y=\"8\" width=\"7\" height=\"2\"></rect>\n <path class=\"dark-grey\" d=\"M9,1 h1 l9,9 v1 h-1 l-9,-9 v-1 Z\"></path>\n <path class=\"dark-grey\" d=\"M9,11 v-1 l9,-9 h1 v1 l-9,9 h-1 Z\"></path>\n </svg>\n </button>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { header: [{
type: Input
}], clearTooltip: [{
type: Input
}], emptyText: [{
type: Input
}], facetsReorderable: [{
type: Input
}], facets: [{
type: Input
}], clearAriaLabel: [{
type: Input
}], deselectFacetAriaLabel: [{
type: Input
}], facetsChange: [{
type: Output
}], events: [{
type: Output
}], clearButton: [{
type: ContentChild,
args: [FacetClearButtonDirective, { read: TemplateRef, static: false }]
}], placeholder: [{
type: ViewChild,
args: [CdkDropList]
}] } });
class TypeaheadOptionEvent {
constructor(option, origin) {
this.option = option;
this.origin = origin;
}
}
class TypeaheadKeyService {
handleKey(event, typeahead) {
if (!typeahead) {
return;
}
switch (event.keyCode) {
case UP_ARROW:
if (!typeahead.open) {
typeahead.open = true;
}
else {
typeahead.moveHighlight(-1);
}
event.preventDefault();
break;
case DOWN_ARROW:
if (!typeahead.open) {
typeahead.open = true;
}
else {
typeahead.moveHighlight(1);
}
event.preventDefault();
break;
case ESCAPE:
typeahead.open = false;
break;
case ENTER:
if (typeahead.selectOnEnter) {
typeahead.selectHighlighted();
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadKeyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadKeyService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadKeyService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class InfiniteScrollLoadButtonDirective {
get visible() {
return this._visible;
}
set visible(value) {
if (value !== this._visible) {
if (value) {
const viewRef = this._viewContainer.createEmbeddedView(this._template);
this._renderer.listen(viewRef.rootNodes[0], 'click', this.onClick.bind(this));
}
else {
this._viewContainer.clear();
}
}
this._visible = value;
}
constructor() {
this._template = inject(TemplateRef);
this._viewContainer = inject(ViewContainerRef);
this._renderer = inject(Renderer2);
this._visible = false;
this._load = new Subject();
this.loading = this._load.asObservable();
}
onClick(event) {
this._load.next(event);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollLoadButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: InfiniteScrollLoadButtonDirective, isStandalone: false, selector: "[uxInfiniteScrollLoadButton]", inputs: { visible: ["uxInfiniteScrollLoadButton", "visible"] }, outputs: { loading: "loading" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollLoadButtonDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxInfiniteScrollLoadButton]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { visible: [{
type: Input,
args: ['uxInfiniteScrollLoadButton']
}], loading: [{
type: Output
}] } });
class InfiniteScrollLoadingDirective {
constructor() {
this._templateRef = inject(TemplateRef);
this._viewContainer = inject(ViewContainerRef);
this._visible = false;
}
get visible() {
return this._visible;
}
set visible(value) {
value = coerceBooleanProperty(value);
if (value !== this._visible) {
if (value) {
this._viewContainer.createEmbeddedView(this._templateRef);
}
else {
this._viewContainer.clear();
}
}
this._visible = value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollLoadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: InfiniteScrollLoadingDirective, isStandalone: false, selector: "[uxInfiniteScrollLoading]", inputs: { visible: ["uxInfiniteScrollLoading", "visible"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollLoadingDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxInfiniteScrollLoading]',
standalone: false,
}]
}], propDecorators: { visible: [{
type: Input,
args: ['uxInfiniteScrollLoading']
}] } });
class InfiniteScrollDirective {
get collection() {
return this._collection;
}
set collection(value) {
this.collectionChange.emit(value);
this._collection = value;
}
set scrollElement(element) {
this._scrollElement = element instanceof ElementRef ? element : new ElementRef(element);
}
constructor() {
this._element = inject(ElementRef);
this._collection = [];
this.enabled = true;
this.loadOnInit = true;
this.loadOnScroll = true;
this.pageSize = 20;
this.collectionChange = new EventEmitter();
this.loadingEvent = new EventEmitter();
this.loadedEvent = new EventEmitter();
this.loadErrorEvent = new EventEmitter();
this._nextPageNum = 0;
this._updateRequests = new Subject();
this._isLoading = new BehaviorSubject(false);
this._isExhausted = new BehaviorSubject(false);
this._loadButtonEnabled = new BehaviorSubject(false);
this._subscriptions = [];
this._loadButtonSubscriptions = [];
this._onDestroy = new Subject();
this._canLoadManually = this._isLoading.pipe(combineLatest$1(this._isExhausted, this._loadButtonEnabled, (isLoading, isExhausted, loadButtonEnabled) => {
return !isLoading && !isExhausted && loadButtonEnabled;
}));
}
ngOnInit() {
if (!this._scrollElement) {
this._scrollElement = this._element;
}
this._loadButtonEnabled.next(!this.loadOnScroll);
}
ngAfterContentInit() {
// There are two kinds of update requests: check and load.
// Check requests are throttled and will only cause an update if more data is required
// to fill the scrolling view, and it isn't already loading some.
// Load requests are not throttled and always request a page of data.
this._updateRequests
.pipe(filter(request => request.check), auditTime(200), takeUntil(this._onDestroy))
.subscribe(this.doRequest.bind(this));
this._updateRequests
.pipe(filter(request => !request.check), takeUntil(this._onDestroy))
.subscribe(this.doRequest.bind(this));
if (this.enabled) {
// Subscribe to scroll events and DOM changes.
this.attachEventHandlers();
}
// Connect the Load More button visible state.
this._canLoadManually.pipe(takeUntil(this._onDestroy)).subscribe(canLoad => {
this._loadButtonQuery.forEach(loadButton => {
loadButton.visible = canLoad;
});
});
// Connect the loading indicator visible state.
this._isLoading.pipe(takeUntil(this._onDestroy)).subscribe(isLoading => {
this._loadingIndicatorQuery.forEach(loading => {
loading.visible = isLoading;
});
});
// Link the Load More button click event to trigger an update.
this.attachLoadButtonEvents();
this._loadButtonQuery.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.attachLoadButtonEvents();
});
// Initial update.
if (this.loadOnInit) {
this.loadNextPage();
}
}
ngOnChanges(changes) {
let check = true;
if (changes.enabled && changes.enabled.currentValue !== changes.enabled.previousValue) {
if (changes.enabled.currentValue) {
this.attachEventHandlers();
this.reset();
check = false;
}
else {
this.detachEventHandlers();
}
}
if (this.enabled) {
if (changes.filter &&
this.coerceFilter(changes.filter.currentValue) !==
this.coerceFilter(changes.filter.previousValue)) {
this.reset();
check = false;
}
if (changes.loadOnScroll) {
this._loadButtonEnabled.next(!changes.loadOnScroll.currentValue);
}
if (changes.pageSize && changes.pageSize.currentValue !== changes.pageSize.previousValue) {
this.reset();
check = false;
}
this._updateRequests.next({
check,
pageNumber: this._nextPageNum,
pageSize: this.pageSize,
filter: this.coerceFilter(this.filter),
});
}
}
ngOnDestroy() {
this.detachEventHandlers();
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Request an additional page of data.
*/
loadNextPage() {
if (!this.enabled) {
return;
}
this._updateRequests.next({
check: false,
pageNumber: this._nextPageNum,
pageSize: this.pageSize,
filter: this.coerceFilter(this.filter),
});
}
/**
* Request a check for whether an additional page of data is required. This is throttled.
*/
check() {
if (!this.enabled) {
return;
}
this._updateRequests.next({
check: true,
pageNumber: this._nextPageNum,
pageSize: this.pageSize,
filter: this.coerceFilter(this.filter),
});
}
/**
* Clear the collection. Future requests will load from page 0.
*/
reset(clearSubscriptions = true) {
if (!this.enabled) {
return;
}
// Reset the page counter.
this._nextPageNum = 0;
// Clear the collection (without changing the reference).
if (this.collection) {
this.collection.length = 0;
}
if (this._subscriptions && clearSubscriptions) {
this._pages = [];
// Reset the exhausted flag, allowing the Load More button to appear.
this._isExhausted.next(false);
// reset the loading state
this._isLoading.next(false);
// Cancel any pending requests
this._subscriptions.forEach(request => request.unsubscribe());
}
}
/**
* Reload the data without clearing the view.
*/
reload() {
this._pages?.forEach((page, i) => this.reloadPage(i));
}
/**
* Reload the data in a specific page without clearing the view.
* @param pageNum Page number
*/
reloadPage(pageNum) {
if (!this.enabled) {
return;
}
this._updateRequests.next({
check: false,
pageNumber: pageNum,
pageSize: this.pageSize,
filter: this.coerceFilter(this.filter),
reload: true,
});
}
/** A filter value of null or undefined should be considered the same as an empty string */
coerceFilter(value) {
return value === undefined || value === null ? '' : value;
}
/**
* Attach scroll event handler and DOM observer.
*/
attachEventHandlers() {
// if the scrollElement is documentElement we must watch for a scroll event on the document
const target = this._scrollElement.nativeElement instanceof HTMLHtmlElement
? document
: this._scrollElement.nativeElement;
// Subscribe to the scroll event on the target element.
this._scrollEventSub = fromEvent(target, 'scroll').subscribe(this.check.bind(this));
// Subscribe to child DOM changes. The main effect of this is to check whether even more data is
// required after the initial load.
this._domObserver = new MutationObserver(this.check.bind(this));
this._domObserver.observe(this._scrollElement.nativeElement, {
childList: true,
subtree: true,
});
}
/**
* Detach scroll event handler and DOM observer.
*/
detachEventHandlers() {
if (this._scrollEventSub) {
this._scrollEventSub.unsubscribe();
this._scrollEventSub = null;
}
if (this._domObserver) {
this._domObserver.disconnect();
this._domObserver = null;
}
}
/**
* Remove any existing event subscriptions for the load button `loading` event, then attach
* subscriptions
* for any in the query.
*/
attachLoadButtonEvents() {
this._loadButtonSubscriptions.forEach(s => s.unsubscribe());
this._loadButtonSubscriptions = this._loadButtonQuery.map(loadButton => loadButton.loading.subscribe(this.loadNextPage.bind(this)));
}
/**
* Conditionally loads a page into the collection based on directive state and request parameters.
*/
doRequest(request) {
// Load a new page if the scroll position is beyond the threshhold and if the client code did not
// cancel.
if (this.needsData(request) && this.beginLoading(request)) {
// Invoke the callback load function, which returns a promose or plain data.
const loadResult = this.load(request.pageNumber, request.pageSize, request.filter);
if (loadResult === null) {
return;
}
const observable = Array.isArray(loadResult) ? of(loadResult) : from(loadResult);
let completed = false;
// subscription needs to be a let here if subscription completes right away the complete function can be called
// before the assignment. While browsers will ignore this currently as we transpile
// all const/let statements to var, when this is no longer the case (or we are running in a test environment)
// const will be used and this can throw an error if we try to access a const variable before it is assigned
const subscription = observable.pipe(first()).subscribe(items => {
// Make sure that the parameters have not changed since the load started;
// otherwise discard the results.
if (request.filter === this.coerceFilter(this.filter) &&
request.pageSize === this.pageSize) {
if (items && items.length) {
this.setPageItems(request.pageNumber, items);
}
// Emit the loaded event
this.endLoading(request, items);
}
}, (reason) => {
// Emit the loadError event
this.endLoadingWithError(request, reason);
}, () => {
// remove this request from the list
this._subscriptions = this._subscriptions.filter(s => s !== subscription);
completed = true;
});
// only add the subscription to the list of requests if it isnt complete.
if (!completed) {
this._subscriptions.push(subscription);
}
}
}
/**
* Returns true if the request should be fulfilled.
*/
needsData(request) {
if (!this.enabled) {
return false;
}
// Always load for a load request
if (!request.check) {
return true;
}
// Ignore a check request when the end of data has been detected, or if data is currently loading.
if (this._isExhausted.getValue() || this._isLoading.getValue()) {
return false;
}
// Load if the remaining scroll area is <= the element height.
if (this._scrollElement && this.loadOnScroll) {
const element = this._scrollElement.nativeElement;
const remainingScroll = element.scrollHeight - (element.scrollTop + element.clientHeight);
const isVisible = element.scrollHeight > 0;
return isVisible && remainingScroll <= element.clientHeight;
}
return false;
}
/**
* Updates state for the beginning of a load. Returns false if the `loading` event was cancelled.
*/
beginLoading(request) {
const event = new InfiniteScrollLoadingEvent(request.pageNumber, request.pageSize, request.filter);
this.loadingEvent.emit(event);
this._isLoading.next(!event.defaultPrevented());
return !event.defaultPrevented();
}
setPageItems(pageNum, items) {
this._pages[pageNum] = items;
this.collection = this._pages.reduce((previous, current) => previous.concat(current), []);
}
/**
* Updates state from a successful load. Raises the `loaded` event.
*/
endLoading(request, data) {
this._isLoading.next(false);
const isExhausted = !!(data && data.length < this.pageSize);
this._isExhausted.next(isExhausted);
this.loadedEvent.emit(new InfiniteScrollLoadedEvent(request.pageNumber, request.pageSize, request.filter, data, isExhausted));
if (!request.reload) {
this._nextPageNum += 1;
}
}
/**
* Updates state from a failed load. Raises the `loadError` event.
*/
endLoadingWithError(request, error) {
this._isLoading.next(false);
this.loadErrorEvent.emit(new InfiniteScrollLoadErrorEvent(request.pageNumber, request.pageSize, request.filter, error));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: InfiniteScrollDirective, isStandalone: false, selector: "[uxInfiniteScroll]", inputs: { load: ["uxInfiniteScroll", "load"], _collection: ["collection", "_collection"], scrollElement: "scrollElement", enabled: "enabled", filter: "filter", loadOnInit: "loadOnInit", loadOnScroll: "loadOnScroll", pageSize: "pageSize" }, outputs: { collectionChange: "collectionChange", loadingEvent: "loading", loadedEvent: "loaded", loadErrorEvent: "loadError" }, queries: [{ propertyName: "_loadButtonQuery", predicate: InfiniteScrollLoadButtonDirective }, { propertyName: "_loadingIndicatorQuery", predicate: InfiniteScrollLoadingDirective }], exportAs: ["uxInfiniteScroll"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxInfiniteScroll]',
exportAs: 'uxInfiniteScroll',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { load: [{
type: Input,
args: ['uxInfiniteScroll']
}], _collection: [{
type: Input,
args: ['collection']
}], scrollElement: [{
type: Input
}], enabled: [{
type: Input
}], filter: [{
type: Input
}], loadOnInit: [{
type: Input
}], loadOnScroll: [{
type: Input
}], pageSize: [{
type: Input
}], collectionChange: [{
type: Output
}], loadingEvent: [{
type: Output,
args: ['loading']
}], loadedEvent: [{
type: Output,
args: ['loaded']
}], loadErrorEvent: [{
type: Output,
args: ['loadError']
}], _loadButtonQuery: [{
type: ContentChildren,
args: [InfiniteScrollLoadButtonDirective]
}], _loadingIndicatorQuery: [{
type: ContentChildren,
args: [InfiniteScrollLoadingDirective]
}] } });
/**
* The internal data associated with a load/check request.
*/
class InfiniteScrollRequest {
}
/**
* Event raised before the `loading` function is called.
*/
class InfiniteScrollLoadingEvent {
constructor(
/**
* The index of the requested page, starting from 0.
*/
pageNumber,
/**
* The number of items requested.
*/
pageSize,
/**
* The filter details as provided via the `filter` binding.
*/
filter) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.filter = filter;
this._defaultPrevented = false;
}
/**
* Prevents the default behaviour of the `loading` event (loading function will not be called).
*/
preventDefault() {
this._defaultPrevented = true;
}
defaultPrevented() {
return this._defaultPrevented;
}
}
/**
* Event raised when the loading function result has been resolved and added to the collection.
*/
class InfiniteScrollLoadedEvent {
constructor(
/**
* The index of the requested page, starting from 0.
*/
pageNumber,
/**
* The number of items requested.
*/
pageSize,
/**
* The filter details as provided via the `filter` binding.
*/
filter,
/**
* The result of the promise returned from the loading function.
*/
data,
/**
* True if the data is considered exhausted (number of items returned less than `pageSize`).
*/
exhausted) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.filter = filter;
this.data = data;
this.exhausted = exhausted;
}
}
/**
* Event raised if the loading function returns a rejected promise.
*/
class InfiniteScrollLoadErrorEvent {
constructor(
/**
* The index of the requested page, starting from 0.
*/
pageNumber,
/**
* The number of items requested.
*/
pageSize,
/**
* The filter details as provided via the `filter` binding.
*/
filter,
/**
* The object provided when rejecting the promise.
*/
error) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.filter = filter;
this.error = error;
}
}
class InfiniteScrollModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollModule, declarations: [InfiniteScrollDirective,
InfiniteScrollLoadButtonDirective,
InfiniteScrollLoadingDirective], exports: [InfiniteScrollDirective,
InfiniteScrollLoadButtonDirective,
InfiniteScrollLoadingDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InfiniteScrollModule, decorators: [{
type: NgModule,
args: [{
imports: [],
exports: [
InfiniteScrollDirective,
InfiniteScrollLoadButtonDirective,
InfiniteScrollLoadingDirective,
],
declarations: [
InfiniteScrollDirective,
InfiniteScrollLoadButtonDirective,
InfiniteScrollLoadingDirective,
],
providers: [],
}]
}] });
class PopoverOrientationService {
constructor() {
this._resizeService = inject(ResizeService);
this._viewportRuler = inject(ViewportRuler);
}
createPopoverOrientationListener(element, parentElement) {
const nativeElement = element instanceof ElementRef ? element.nativeElement : element;
const nativeElementParent = parentElement instanceof ElementRef ? parentElement.nativeElement : element;
return new PopoverOrientationListener(nativeElement, nativeElementParent, this._resizeService, this._viewportRuler);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverOrientationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverOrientationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverOrientationService, decorators: [{
type: Injectable
}] });
class PopoverOrientationListener {
constructor(_element, _elementParent, _resizeService, _viewportRuler) {
this._element = _element;
this._elementParent = _elementParent;
this._resizeService = _resizeService;
this._viewportRuler = _viewportRuler;
/** Allow subscribing to state changes */
this.orientation$ = new BehaviorSubject(1);
/** Max value the height of the dropdown can be */
this.maxHeight = 250;
this._onDestroy = new Subject();
// watch for changes to the typeahead size
this._resizeService
.addResizeListener(this._element)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.onScrollOrResize();
});
// watch for changes to the typeahead position when scrolling or resizing
fromEvent(window, 'scroll', { passive: true })
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.onScrollOrResize());
fromEvent(window, 'resize', { passive: true })
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.onScrollOrResize());
}
destroy() {
this.orientation$.complete();
this._onDestroy.next();
this._onDestroy.complete();
this._resizeService.removeResizeListener(this._element);
}
onScrollOrResize() {
this._rect = this._elementParent
? this._elementParent.parentElement.getBoundingClientRect()
: this._element.parentElement.getBoundingClientRect();
// use the maxHeight input value if the element does not exist yet to prevent the direction from immediately changing when opened
const itemHeight = this._element.offsetHeight || this.maxHeight;
const viewportSize = this._viewportRuler.getViewportSize();
const bottomSpaceAvailable = viewportSize.height - this._rect.bottom - itemHeight;
this.orientation$.next(bottomSpaceAvailable <= 0 ? 0 /* PopoverOrientation.Up */ : 1 /* PopoverOrientation.Down */);
}
}
class TypeaheadService {
constructor() {
this.open$ = new BehaviorSubject(false);
this.highlightedElement$ = new BehaviorSubject(null);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadService, decorators: [{
type: Injectable
}] });
class SafeInnerHtmlDirective {
constructor() {
this._sanitizer = inject(DomSanitizer);
}
set safeInnerHtml(value) {
// Angular's DomSanitizer allows anchor tags, however it does remove any dangerous attributes. That being said
// we still want to escape any anchor tags regardless.
value = value.replace(/<a/g, '<a').replace(/<\/a>/g, '</a>');
this.safeHtml = this._sanitizer.sanitize(SecurityContext.HTML, value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SafeInnerHtmlDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SafeInnerHtmlDirective, isStandalone: true, selector: "[uxSafeInnerHtml]", inputs: { safeInnerHtml: ["uxSafeInnerHtml", "safeInnerHtml"] }, host: { properties: { "innerHtml": "this.safeHtml" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SafeInnerHtmlDirective, decorators: [{
type: Directive,
args: [{
standalone: true,
selector: '[uxSafeInnerHtml]',
}]
}], propDecorators: { safeHtml: [{
type: HostBinding,
args: ['innerHtml']
}], safeInnerHtml: [{
type: Input,
args: ['uxSafeInnerHtml']
}] } });
class ScrollIntoViewService {
scrollIntoView(elem, scrollParent) {
const offsetTop = elem.getBoundingClientRect().top +
scrollParent.scrollTop -
scrollParent.getBoundingClientRect().top;
if (offsetTop < scrollParent.scrollTop) {
scrollParent.scrollTop = offsetTop;
}
else {
const offsetBottom = offsetTop + elem.offsetHeight;
if (offsetBottom > scrollParent.scrollTop + scrollParent.clientHeight) {
scrollParent.scrollTop = offsetBottom - scrollParent.clientHeight;
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewService, decorators: [{
type: Injectable
}] });
class ScrollIntoViewIfDirective {
constructor() {
this._element = inject(ElementRef);
this._scrollIntoViewService = inject(ScrollIntoViewService);
this.condition = false;
}
ngOnChanges() {
if (this.condition) {
setTimeout(() => this._scrollIntoViewService.scrollIntoView(this._element.nativeElement, this.scrollParent));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ScrollIntoViewIfDirective, isStandalone: false, selector: "[uxScrollIntoViewIf]", inputs: { condition: ["uxScrollIntoViewIf", "condition"], scrollParent: "scrollParent" }, providers: [ScrollIntoViewService], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewIfDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxScrollIntoViewIf]',
providers: [ScrollIntoViewService],
standalone: false,
}]
}], propDecorators: { condition: [{
type: Input,
args: ['uxScrollIntoViewIf']
}], scrollParent: [{
type: Input
}] } });
class TypeaheadHighlightDirective {
constructor() {
this._service = inject(TypeaheadService);
this._elementRef = inject(ElementRef);
}
set highlight(value) {
if (value) {
this._service.highlightedElement$.next(this._elementRef.nativeElement);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadHighlightDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TypeaheadHighlightDirective, isStandalone: false, selector: "[uxTypeaheadHighlight]", inputs: { highlight: ["uxTypeaheadHighlight", "highlight"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadHighlightDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTypeaheadHighlight]',
standalone: false,
}]
}], propDecorators: { highlight: [{
type: Input,
args: ['uxTypeaheadHighlight']
}] } });
class TypeaheadOptionsListComponent {
constructor() {
this.startIndex = 0;
this.isMultiselectable = false;
this.optionMouseover = new EventEmitter();
this.optionMousedown = new EventEmitter();
this.optionClick = new EventEmitter();
}
trackByFn(_, option) {
return option.key;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadOptionsListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TypeaheadOptionsListComponent, isStandalone: false, selector: "ux-typeahead-options-list", inputs: { id: "id", startIndex: "startIndex", options: "options", highlighted: "highlighted", activeKey: "activeKey", disabledOptions: "disabledOptions", isMultiselectable: "isMultiselectable", optionTemplate: "optionTemplate", optionApi: "optionApi", typeaheadElement: "typeaheadElement", ariaLabel: "ariaLabel" }, outputs: { optionMouseover: "optionMouseover", optionMousedown: "optionMousedown", optionClick: "optionClick" }, ngImport: i0, template: "<ol role=\"listbox\" [attr.aria-label]=\"ariaLabel\" [attr.aria-multiselectable]=\"isMultiselectable\">\n @for (option of options; track trackByFn(i, option); let i = $index) {\n <li\n [attr.id]=\"id + '-option-' + (i + startIndex)\"\n [class.disabled]=\"optionApi.getDisabled(option.value)\"\n [class.highlighted]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n [class.active]=\"activeKey === option.key && !option.isRecentOption\"\n [uxTypeaheadHighlight]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n [uxScrollIntoViewIf]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n role=\"option\"\n [attr.aria-selected]=\"\n isMultiselectable\n ? optionApi.getDisabled(option.value)\n : activeKey === option.key\n ? true\n : null\n \"\n [scrollParent]=\"typeaheadElement.nativeElement\"\n (mouseover)=\"optionMouseover.emit({ option: option, event: $event })\"\n (mousedown)=\"optionMousedown.emit({ option: option, event: $event })\"\n (click)=\"optionClick.emit({ option: option, event: $event })\"\n >\n <ng-container\n [ngTemplateOutlet]=\"optionTemplate\"\n [ngTemplateOutletContext]=\"{ option: option.value, api: optionApi }\"\n >\n </ng-container>\n </li>\n }\n</ol>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ScrollIntoViewIfDirective, selector: "[uxScrollIntoViewIf]", inputs: ["uxScrollIntoViewIf", "scrollParent"] }, { kind: "directive", type: TypeaheadHighlightDirective, selector: "[uxTypeaheadHighlight]", inputs: ["uxTypeaheadHighlight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadOptionsListComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-typeahead-options-list', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ol role=\"listbox\" [attr.aria-label]=\"ariaLabel\" [attr.aria-multiselectable]=\"isMultiselectable\">\n @for (option of options; track trackByFn(i, option); let i = $index) {\n <li\n [attr.id]=\"id + '-option-' + (i + startIndex)\"\n [class.disabled]=\"optionApi.getDisabled(option.value)\"\n [class.highlighted]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n [class.active]=\"activeKey === option.key && !option.isRecentOption\"\n [uxTypeaheadHighlight]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n [uxScrollIntoViewIf]=\"\n highlighted?.key === option.key && highlighted?.isRecentOption === option.isRecentOption\n \"\n role=\"option\"\n [attr.aria-selected]=\"\n isMultiselectable\n ? optionApi.getDisabled(option.value)\n : activeKey === option.key\n ? true\n : null\n \"\n [scrollParent]=\"typeaheadElement.nativeElement\"\n (mouseover)=\"optionMouseover.emit({ option: option, event: $event })\"\n (mousedown)=\"optionMousedown.emit({ option: option, event: $event })\"\n (click)=\"optionClick.emit({ option: option, event: $event })\"\n >\n <ng-container\n [ngTemplateOutlet]=\"optionTemplate\"\n [ngTemplateOutletContext]=\"{ option: option.value, api: optionApi }\"\n >\n </ng-container>\n </li>\n }\n</ol>\n" }]
}], propDecorators: { id: [{
type: Input
}], startIndex: [{
type: Input
}], options: [{
type: Input
}], highlighted: [{
type: Input
}], activeKey: [{
type: Input
}], disabledOptions: [{
type: Input
}], isMultiselectable: [{
type: Input
}], optionTemplate: [{
type: Input
}], optionApi: [{
type: Input
}], typeaheadElement: [{
type: Input
}], ariaLabel: [{
type: Input
}], optionMouseover: [{
type: Output
}], optionMousedown: [{
type: Output
}], optionClick: [{
type: Output
}] } });
let uniqueId$9 = 0;
class TypeaheadComponent {
/** Specify if the typeahead is open */
get open() {
return this._service.open$.getValue();
}
set open(value) {
this._service.open$.next(value);
}
/** Specify which options are disabled */
set disabledOptions(options) {
this._disabledOptions = coerceArray(options);
}
get disabledOptions() {
return this._disabledOptions;
}
/** Specify the max height of the dropdown */
get maxHeight() {
return this._maxHeight;
}
set maxHeight(maxHeight) {
this._maxHeight = maxHeight;
if (this.maxHeight.endsWith('px')) {
this._popoverOrientationListener.maxHeight = Number(this.maxHeight.slice(0, -2));
}
}
/** Specify the currently active item */
set active(item) {
this.activeKey = this.getKey(item);
}
get highlighted() {
const value = this.highlighted$.getValue();
return value ? value.value : null;
}
constructor() {
this.typeaheadElement = inject(ElementRef);
this.popoverOrientation = inject(PopoverOrientationService);
this._changeDetector = inject(ChangeDetectorRef);
this._service = inject(TypeaheadService);
/** Define a unique id for the typeahead */
this.id = `ux-typeahead-${++uniqueId$9}`;
this._disabledOptions = [];
/** Specify the drop direction */
this.dropDirection = 'down';
/** Specify the aria multi selectable attribute value */
this.multiselectable = false;
/** Specify if the dropdown should appear when the filter appears */
this.openOnFilterChange = true;
/** Specify the page size */
this.pageSize = 20;
/** Specify if we should select the first item by default */
this.selectFirst = true;
/** Specify if we should select an item on enter key press */
this.selectOnEnter = false;
/** Specify the loading state */
this.loading = false;
/** Maximum number of displayed recently selected options. */
this.recentOptionsMaxCount = 5;
/** Emit when the open state changes */
this.openChange = new EventEmitter();
/** Emit when an option is selected */
this.optionSelected = new EventEmitter();
/** Emit whenever a highlighted item changes */
this.highlightedChange = new EventEmitter();
/** Emit the highlighted element when it changes */
this.highlightedElementChange = new EventEmitter();
/** Emits when recently selected options change.*/
this.recentOptionsChange = new EventEmitter();
this.activeKey = null;
this.clicking = false;
this.hasBeenOpened = false;
this.highlighted$ = new BehaviorSubject(null);
this.visibleOptions$ = new BehaviorSubject([]);
this.visibleRecentOptions$ = new BehaviorSubject([]);
this.allVisibleOptions = [];
this._onDestroy = new Subject();
this._maxHeight = '250px';
this.optionApi = {
getKey: this.getKey.bind(this),
getDisplay: this.getDisplay.bind(this),
getDisplayHtml: this.getDisplayHtml.bind(this),
getDisabled: this.isDisabled.bind(this),
};
this.loadOptionsCallback = (pageNum, pageSize, filter) => {
if (typeof this.options === 'function') {
// Invoke the callback which may return an array or a promise.
const arrayOrPromise = this.options(pageNum, pageSize, filter);
// Map the results to an array of TypeaheadVisibleOption.
return Promise.resolve(arrayOrPromise).then(newOptions => {
if (!Array.isArray(newOptions)) {
return newOptions;
}
return this.getVisibleOptions(newOptions, '');
});
}
return null;
};
this._service.open$
.pipe(distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(isOpen => {
this.openChange.emit(isOpen);
if (isOpen) {
this.hasBeenOpened = true;
this.initOptions();
}
});
this._popoverOrientationListener = this.popoverOrientation.createPopoverOrientationListener(this.typeaheadElement.nativeElement, this.typeaheadElement.nativeElement.parentElement);
this._popoverOrientationListener.orientation$
.pipe(takeUntil(this._onDestroy))
.subscribe(direction => {
if (this.dropDirection === 'auto') {
this.dropUp = direction === 0 /* PopoverOrientation.Up */;
}
});
this.highlighted$.pipe(takeUntil(this._onDestroy)).subscribe(next => {
this.highlightedChange.emit(next ? next.value : null);
});
combineLatest([this.visibleOptions$, this.visibleRecentOptions$])
.pipe(takeUntil(this._onDestroy))
.subscribe(([visibleOptions, visibleRecentOptions]) => {
this.allVisibleOptions = [...visibleRecentOptions, ...visibleOptions];
});
combineLatest([this._service.open$, this._service.highlightedElement$, this.visibleOptions$])
.pipe(takeUntil(this._onDestroy))
.subscribe(([open, highlightedElement, visibleOptions]) => {
this.highlightedElementChange.emit(open && visibleOptions.length > 0 ? highlightedElement : null);
});
}
ngOnChanges(changes) {
// Open the dropdown if the filter value updates
if (changes.filter) {
if (this.openOnFilterChange &&
changes.filter.currentValue &&
changes.filter.currentValue.length > 0) {
// if the dropdown item was just selected, and we set the filter value to match the
// selected value then open will have also just been set to `false`, in which case we do
// not want to set open to `true`
if (changes.open &&
changes.open.previousValue === true &&
changes.open.currentValue === false) {
return;
}
// show the dropdown
this.open = true;
}
}
// Cut off recentOptions at recentOptionsMaxCount
if (changes.recentOptions || changes.recentOptionsMaxCount) {
this._recentOptions = this.recentOptions
? this.recentOptions.slice(0, this.recentOptionsMaxCount)
: undefined;
if (changes.recentOptionsMaxCount) {
// Avoid ExpressionChangedAfterChecked error
setTimeout(() => {
this.recentOptionsChange.emit(this._recentOptions);
});
}
}
if (changes.dropDirection) {
if (changes.dropDirection.currentValue === 'auto') {
this.dropUp =
this._popoverOrientationListener.orientation$.getValue() === 0 /* PopoverOrientation.Up */;
}
else {
this.dropUp = changes.dropDirection.currentValue === 'up';
}
}
if (changes.options && changes.options.firstChange === false) {
this.infiniteScroll.reset(false);
this.infiniteScroll.reload();
}
// Re-filter visibleOptions
this.updateOptions();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this._popoverOrientationListener.destroy();
}
mousedownHandler(event, target) {
this.clicking = true;
/**
* Chrome Bug Workaround: (https://bugs.chromium.org/p/chromium/issues/detail?id=6759)
* In Chrome, if the typeahead is within a tabbable element, e.g. it has a parent with
* `tabindex="0"` like our side panel has, then clicking on a scrollbar will remove
* focus from the input element.
*
* To avoid this we can determine if the mousedown event occurred within the scrollbar
* region of the typeahead and if so preventDefault which will stop the input
* from losing focus
*/
if (event.clientX >= target.clientWidth || event.clientY >= target.clientHeight) {
event.preventDefault();
}
}
mouseupHandler() {
this.clicking = false;
}
optionMousedownHandler(event) {
// Workaround to prevent focus changing when an option is clicked
event.preventDefault();
}
optionClickHandler(_event, option) {
this.select(option, 'mouse');
}
/**
* Returns the unique key value of the given option.
*/
getKey(option) {
if (typeof this.key === 'function') {
return this.key(option);
}
// eslint-disable-next-line no-prototype-builtins
if (typeof this.key === 'string' && option && option.hasOwnProperty(this.key)) {
return option[this.key];
}
return this.getDisplay(option);
}
/**
* Returns the display value of the given option.
*/
getDisplay(option) {
if (typeof this.display === 'function') {
return this.display(option);
}
// eslint-disable-next-line no-prototype-builtins
if (typeof this.display === 'string' && option && option.hasOwnProperty(this.display)) {
return option[this.display];
}
if (typeof option === 'string') {
return option;
}
}
/**
* Returns the display value of the given option with HTML markup added to highlight the part which matches the current filter value.
* @param option
*/
getDisplayHtml(option) {
const displayText = this.getDisplay(option);
// getDisplay may return undefined in certain cases
if (!displayText) {
return '';
}
let displayHtml = displayText
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
if (this.filter) {
const length = this.filter.length;
const matchIndex = displayText.toLowerCase().indexOf(this.filter.toLowerCase());
if (matchIndex >= 0) {
const highlight = `<span class="ux-filter-match">${displayText.substr(matchIndex, length)}</span>`;
displayHtml =
displayText.substr(0, matchIndex) + highlight + displayText.substr(matchIndex + length);
}
}
return displayHtml;
}
/**
* Returns true if the infinite scroll component should load
*/
isInfiniteScroll() {
return typeof this.options === 'function';
}
/**
* Selects the given option, emitting the optionSelected event and closing the dropdown.
*/
select(option, origin) {
if (!this.isDisabled(option.value)) {
this.optionSelected.emit(new TypeaheadOptionEvent(option.value, origin));
this.highlighted$.next(null);
this.open = false;
this.addToRecentOptions(option.value);
}
}
addToRecentOptions(value) {
if (this._recentOptions) {
this._recentOptions = [
value,
...this._recentOptions.filter(recentOption => recentOption !== value),
].slice(0, this.recentOptionsMaxCount);
this.recentOptionsChange.emit(this._recentOptions);
}
}
/**
* Returns true if the given option is part of the disabledOptions array.
*/
isDisabled(option) {
return Array.isArray(this.disabledOptions)
? this.disabledOptions.some(selectedOption => this.getKey(selectedOption) === this.getKey(option))
: false;
}
/**
* Set the given option as the current highlighted option, available in the highlightedOption parameter.
*/
highlight(option) {
if (!this.isDisabled(option.value)) {
this.highlighted$.next(option);
this._changeDetector.detectChanges();
}
}
/**
* Increment or decrement the highlighted option in the list. Disabled options are skipped.
* @param d Value to be added to the index of the highlighted option, i.e. -1 to move backwards, +1 to move forwards.
*/
moveHighlight(d) {
const highlightIndex = this.indexOfVisibleOption(this.highlighted$.getValue());
let newIndex = highlightIndex;
let disabled = true;
let inBounds = true;
do {
newIndex = newIndex + d;
inBounds = newIndex >= 0 && newIndex < this.allVisibleOptions.length;
disabled = inBounds && this.isDisabled(this.allVisibleOptions[newIndex].value);
} while (inBounds && disabled);
if (!disabled && inBounds) {
this.highlight(this.allVisibleOptions[newIndex]);
}
return this.highlighted;
}
selectHighlighted() {
if (this.highlighted$.getValue()) {
this.select(this.highlighted$.getValue(), 'keyboard');
}
}
/**
* Set up the options before the dropdown is displayed.
*/
initOptions() {
// Clear previous highlight
this.highlighted$.next(null);
if (this.selectFirst) {
// This will highlight the first non-disabled option.
this.moveHighlight(1);
}
}
/**
* Display the first item as highlighted when there are several pages
*/
onLoadedHighlight(event) {
if (this.selectFirst && this.options && event.pageNumber === 0) {
// This will highlight the first non-disabled option.
this.moveHighlight(1);
}
}
/**
* Update the visibleOptions and visibleRecentOptions arrays with the current filter.
*/
updateOptions() {
const normalisedInput = (this.filter || '').toLowerCase();
// Create new visibleOptions only if `options` is not a function
if (typeof this.options === 'object') {
this.visibleOptions$.next(this.getVisibleOptions(this.options, normalisedInput));
}
this.visibleRecentOptions$.next(this.getVisibleOptions(this._recentOptions, normalisedInput, true));
this.initOptions();
this._changeDetector.detectChanges();
}
/**
* Convert a set of raw options into a filtered list of `TypeaheadVisibleOption` objects.
* @param options Set of raw options
* @param filter The filter expression
* @param isRecentOptions Whether `options` is a set of recent options
*/
getVisibleOptions(options, filter = '', isRecentOptions = false) {
if (options) {
return options
.filter(option => this.getDisplay(option).toLowerCase().indexOf(filter) >= 0)
.map(value => ({
value,
key: this.getKey(value),
isRecentOption: isRecentOptions,
}));
}
return [];
}
/**
* Return the index of the given option in the allVisibleOptions array.
* Returns -1 if the option is not currently visible.
*/
indexOfVisibleOption(option) {
if (option) {
return this.allVisibleOptions.findIndex(el => {
return el.key === option.key && el.isRecentOption === option.isRecentOption;
});
}
return -1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TypeaheadComponent, isStandalone: false, selector: "ux-typeahead", inputs: { id: "id", options: "options", filter: "filter", open: "open", display: "display", key: "key", disabledOptions: "disabledOptions", dropDirection: "dropDirection", maxHeight: "maxHeight", multiselectable: "multiselectable", ariaLabel: "ariaLabel", openOnFilterChange: "openOnFilterChange", pageSize: "pageSize", selectFirst: "selectFirst", selectOnEnter: "selectOnEnter", loading: "loading", loadingTemplate: "loadingTemplate", optionTemplate: "optionTemplate", noOptionsTemplate: "noOptionsTemplate", active: "active", recentOptions: "recentOptions", recentOptionsMaxCount: "recentOptionsMaxCount", recentOptionsHeadingTemplate: "recentOptionsHeadingTemplate", optionsHeadingTemplate: "optionsHeadingTemplate" }, outputs: { openChange: "openChange", optionSelected: "optionSelected", highlightedChange: "highlightedChange", highlightedElementChange: "highlightedElementChange", recentOptionsChange: "recentOptionsChange" }, host: { listeners: { "mousedown": "mousedownHandler($event,$event.target)", "mouseup": "mouseupHandler()" }, properties: { "class.open": "open", "style.maxHeight": "maxHeight", "attr.id": "this.id", "class.drop-up": "this.dropUp" } }, providers: [TypeaheadService, PopoverOrientationService], viewQueries: [{ propertyName: "infiniteScroll", first: true, predicate: InfiniteScrollDirective, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ux-typeahead-options\"\n [uxInfiniteScroll]=\"loadOptionsCallback\"\n [collection]=\"visibleOptions$ | async\"\n (collectionChange)=\"visibleOptions$.next($event)\"\n [enabled]=\"hasBeenOpened && isInfiniteScroll()\"\n [filter]=\"filter\"\n [loadOnScroll]=\"true\"\n [pageSize]=\"pageSize\"\n [scrollElement]=\"typeaheadElement\"\n (loading)=\"loading = true\"\n (loaded)=\"loading = false; onLoadedHighlight($event)\"\n>\n @if ((visibleRecentOptions$ | async).length > 0) {\n <div>\n <ng-container [ngTemplateOutlet]=\"recentOptionsHeadingTemplate\"></ng-container>\n </div>\n }\n\n <!-- Recent options -->\n @if ((visibleRecentOptions$ | async).length > 0) {\n <ux-typeahead-options-list\n class=\"ux-typeahead-recent-options\"\n [id]=\"id\"\n [options]=\"visibleRecentOptions$ | async\"\n [highlighted]=\"highlighted$ | async\"\n [activeKey]=\"activeKey\"\n [disabledOptions]=\"_disabledOptions\"\n [isMultiselectable]=\"multiselectable\"\n [optionTemplate]=\"optionTemplate || defaultOptionTemplate\"\n [optionApi]=\"optionApi\"\n [typeaheadElement]=\"typeaheadElement\"\n (optionMouseover)=\"highlight($event.option)\"\n (optionMousedown)=\"optionMousedownHandler($event.event)\"\n (optionClick)=\"optionClickHandler($event.event, $event.option)\"\n >\n </ux-typeahead-options-list>\n }\n\n <ng-container [ngTemplateOutlet]=\"optionsHeadingTemplate\"></ng-container>\n\n <!-- All options -->\n @if ((visibleOptions$ | async).length > 0) {\n <ux-typeahead-options-list\n class=\"ux-typeahead-all-options\"\n [id]=\"id\"\n [ariaLabel]=\"ariaLabel\"\n [startIndex]=\"(visibleRecentOptions$ | async).length\"\n [options]=\"visibleOptions$ | async\"\n [highlighted]=\"highlighted$ | async\"\n [activeKey]=\"activeKey\"\n [disabledOptions]=\"_disabledOptions\"\n [isMultiselectable]=\"multiselectable\"\n [optionTemplate]=\"optionTemplate || defaultOptionTemplate\"\n [optionApi]=\"optionApi\"\n [typeaheadElement]=\"typeaheadElement\"\n (optionMouseover)=\"highlight($event.option)\"\n (optionMousedown)=\"optionMousedownHandler($event.event)\"\n (optionClick)=\"optionClickHandler($event.event, $event.option)\"\n >\n </ux-typeahead-options-list>\n }\n\n <div *uxInfiniteScrollLoading>\n <ng-container [ngTemplateOutlet]=\"loadingTemplate || defaultLoadingTemplate\"></ng-container>\n </div>\n\n @if (isInfiniteScroll() === false && (visibleOptions$ | async).length === 0 && loading) {\n <div>\n <ng-container [ngTemplateOutlet]=\"loadingTemplate || defaultLoadingTemplate\"></ng-container>\n </div>\n }\n</div>\n@if ((visibleOptions$ | async).length === 0 && !loading) {\n <div>\n <ng-container [ngTemplateOutlet]=\"noOptionsTemplate || defaultNoOptionsTemplate\">\n </ng-container>\n </div>\n}\n\n<ng-template #defaultLoadingTemplate>\n <div class=\"ux-typeahead-loading\">\n <div class=\"spinner spinner-accent spinner-bounce-middle\"></div>\n <div>Loading...</div>\n </div>\n</ng-template>\n\n<ng-template #defaultOptionTemplate let-option=\"option\" let-api=\"api\">\n <span class=\"ux-typeahead-option\" [uxSafeInnerHtml]=\"api.getDisplayHtml(option)\"></span>\n</ng-template>\n\n<ng-template #defaultNoOptionsTemplate>\n <span class=\"ux-typeahead-no-options\">No results</span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: InfiniteScrollDirective, selector: "[uxInfiniteScroll]", inputs: ["uxInfiniteScroll", "collection", "scrollElement", "enabled", "filter", "loadOnInit", "loadOnScroll", "pageSize"], outputs: ["collectionChange", "loading", "loaded", "loadError"], exportAs: ["uxInfiniteScroll"] }, { kind: "directive", type: InfiniteScrollLoadingDirective, selector: "[uxInfiniteScrollLoading]", inputs: ["uxInfiniteScrollLoading"] }, { kind: "directive", type: SafeInnerHtmlDirective, selector: "[uxSafeInnerHtml]", inputs: ["uxSafeInnerHtml"] }, { kind: "component", type: TypeaheadOptionsListComponent, selector: "ux-typeahead-options-list", inputs: ["id", "startIndex", "options", "highlighted", "activeKey", "disabledOptions", "isMultiselectable", "optionTemplate", "optionApi", "typeaheadElement", "ariaLabel"], outputs: ["optionMouseover", "optionMousedown", "optionClick"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-typeahead', providers: [TypeaheadService, PopoverOrientationService], changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[class.open]': 'open',
'[style.maxHeight]': 'maxHeight',
}, standalone: false, template: "<div\n class=\"ux-typeahead-options\"\n [uxInfiniteScroll]=\"loadOptionsCallback\"\n [collection]=\"visibleOptions$ | async\"\n (collectionChange)=\"visibleOptions$.next($event)\"\n [enabled]=\"hasBeenOpened && isInfiniteScroll()\"\n [filter]=\"filter\"\n [loadOnScroll]=\"true\"\n [pageSize]=\"pageSize\"\n [scrollElement]=\"typeaheadElement\"\n (loading)=\"loading = true\"\n (loaded)=\"loading = false; onLoadedHighlight($event)\"\n>\n @if ((visibleRecentOptions$ | async).length > 0) {\n <div>\n <ng-container [ngTemplateOutlet]=\"recentOptionsHeadingTemplate\"></ng-container>\n </div>\n }\n\n <!-- Recent options -->\n @if ((visibleRecentOptions$ | async).length > 0) {\n <ux-typeahead-options-list\n class=\"ux-typeahead-recent-options\"\n [id]=\"id\"\n [options]=\"visibleRecentOptions$ | async\"\n [highlighted]=\"highlighted$ | async\"\n [activeKey]=\"activeKey\"\n [disabledOptions]=\"_disabledOptions\"\n [isMultiselectable]=\"multiselectable\"\n [optionTemplate]=\"optionTemplate || defaultOptionTemplate\"\n [optionApi]=\"optionApi\"\n [typeaheadElement]=\"typeaheadElement\"\n (optionMouseover)=\"highlight($event.option)\"\n (optionMousedown)=\"optionMousedownHandler($event.event)\"\n (optionClick)=\"optionClickHandler($event.event, $event.option)\"\n >\n </ux-typeahead-options-list>\n }\n\n <ng-container [ngTemplateOutlet]=\"optionsHeadingTemplate\"></ng-container>\n\n <!-- All options -->\n @if ((visibleOptions$ | async).length > 0) {\n <ux-typeahead-options-list\n class=\"ux-typeahead-all-options\"\n [id]=\"id\"\n [ariaLabel]=\"ariaLabel\"\n [startIndex]=\"(visibleRecentOptions$ | async).length\"\n [options]=\"visibleOptions$ | async\"\n [highlighted]=\"highlighted$ | async\"\n [activeKey]=\"activeKey\"\n [disabledOptions]=\"_disabledOptions\"\n [isMultiselectable]=\"multiselectable\"\n [optionTemplate]=\"optionTemplate || defaultOptionTemplate\"\n [optionApi]=\"optionApi\"\n [typeaheadElement]=\"typeaheadElement\"\n (optionMouseover)=\"highlight($event.option)\"\n (optionMousedown)=\"optionMousedownHandler($event.event)\"\n (optionClick)=\"optionClickHandler($event.event, $event.option)\"\n >\n </ux-typeahead-options-list>\n }\n\n <div *uxInfiniteScrollLoading>\n <ng-container [ngTemplateOutlet]=\"loadingTemplate || defaultLoadingTemplate\"></ng-container>\n </div>\n\n @if (isInfiniteScroll() === false && (visibleOptions$ | async).length === 0 && loading) {\n <div>\n <ng-container [ngTemplateOutlet]=\"loadingTemplate || defaultLoadingTemplate\"></ng-container>\n </div>\n }\n</div>\n@if ((visibleOptions$ | async).length === 0 && !loading) {\n <div>\n <ng-container [ngTemplateOutlet]=\"noOptionsTemplate || defaultNoOptionsTemplate\">\n </ng-container>\n </div>\n}\n\n<ng-template #defaultLoadingTemplate>\n <div class=\"ux-typeahead-loading\">\n <div class=\"spinner spinner-accent spinner-bounce-middle\"></div>\n <div>Loading...</div>\n </div>\n</ng-template>\n\n<ng-template #defaultOptionTemplate let-option=\"option\" let-api=\"api\">\n <span class=\"ux-typeahead-option\" [uxSafeInnerHtml]=\"api.getDisplayHtml(option)\"></span>\n</ng-template>\n\n<ng-template #defaultNoOptionsTemplate>\n <span class=\"ux-typeahead-no-options\">No results</span>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { infiniteScroll: [{
type: ViewChild,
args: [InfiniteScrollDirective]
}], id: [{
type: Input
}, {
type: HostBinding,
args: ['attr.id']
}], options: [{
type: Input
}], filter: [{
type: Input
}], open: [{
type: Input
}], display: [{
type: Input
}], key: [{
type: Input
}], disabledOptions: [{
type: Input
}], dropDirection: [{
type: Input
}], maxHeight: [{
type: Input
}], multiselectable: [{
type: Input
}], ariaLabel: [{
type: Input
}], openOnFilterChange: [{
type: Input
}], pageSize: [{
type: Input
}], selectFirst: [{
type: Input
}], selectOnEnter: [{
type: Input
}], loading: [{
type: Input
}], loadingTemplate: [{
type: Input
}], optionTemplate: [{
type: Input
}], noOptionsTemplate: [{
type: Input
}], active: [{
type: Input
}], recentOptions: [{
type: Input
}], recentOptionsMaxCount: [{
type: Input
}], recentOptionsHeadingTemplate: [{
type: Input
}], optionsHeadingTemplate: [{
type: Input
}], openChange: [{
type: Output
}], optionSelected: [{
type: Output
}], highlightedChange: [{
type: Output
}], highlightedElementChange: [{
type: Output
}], recentOptionsChange: [{
type: Output
}], dropUp: [{
type: HostBinding,
args: ['class.drop-up']
}], mousedownHandler: [{
type: HostListener,
args: ['mousedown', ['$event', '$event.target']]
}], mouseupHandler: [{
type: HostListener,
args: ['mouseup']
}] } });
class ScrollIntoViewDirective {
constructor() {
this._elementRef = inject(ElementRef);
/** Allow a condition around whether or not this should scroll into view */
this.uxScrollIntoView = true;
/** Allow user to provide the browser supported options */
this.scrollIntoViewOptions = true;
}
ngAfterViewInit() {
if (this.uxScrollIntoView) {
this._elementRef.nativeElement.scrollIntoView(this.scrollIntoViewOptions);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ScrollIntoViewDirective, isStandalone: false, selector: "[uxScrollIntoView]", inputs: { uxScrollIntoView: "uxScrollIntoView", scrollIntoViewOptions: "scrollIntoViewOptions" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollIntoViewDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxScrollIntoView]',
standalone: false,
}]
}], propDecorators: { uxScrollIntoView: [{
type: Input
}], scrollIntoViewOptions: [{
type: Input
}] } });
class ScrollModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ScrollModule, declarations: [ScrollIntoViewIfDirective, ScrollIntoViewDirective], exports: [ScrollIntoViewIfDirective, ScrollIntoViewDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ScrollModule, decorators: [{
type: NgModule,
args: [{
exports: [ScrollIntoViewIfDirective, ScrollIntoViewDirective],
declarations: [ScrollIntoViewIfDirective, ScrollIntoViewDirective],
}]
}] });
class TypeaheadModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadModule, declarations: [TypeaheadComponent, TypeaheadHighlightDirective, TypeaheadOptionsListComponent], imports: [CommonModule, InfiniteScrollModule, ResizeModule, ScrollModule, SafeInnerHtmlDirective], exports: [TypeaheadComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadModule, providers: [PopoverOrientationService], imports: [CommonModule, InfiniteScrollModule, ResizeModule, ScrollModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypeaheadModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, InfiniteScrollModule, ResizeModule, ScrollModule, SafeInnerHtmlDirective],
exports: [TypeaheadComponent],
declarations: [TypeaheadComponent, TypeaheadHighlightDirective, TypeaheadOptionsListComponent],
providers: [PopoverOrientationService],
}]
}] });
class FacetTypeaheadListItemComponent {
constructor() {
this.selected = false;
this.simplified = false;
this.tabbable = false;
this.itemFocus = new EventEmitter();
this.selectedChange = new EventEmitter();
}
get disabled() {
return this.facet && this.facet.disabled;
}
getLabel() {
return this.facet ? this.facet.title : null;
}
focus() {
this.option.nativeElement.focus();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetTypeaheadListItemComponent, isStandalone: false, selector: "ux-facet-typeahead-list-item", inputs: { facet: "facet", selected: "selected", simplified: "simplified", tabbable: "tabbable" }, outputs: { itemFocus: "itemFocus", selectedChange: "selectedChange" }, viewQueries: [{ propertyName: "option", first: true, predicate: ["option"], descendants: true, static: true }], ngImport: i0, template: "<div\n #option\n uxFocusIndicator\n role=\"option\"\n class=\"facet-typeahead-list-selected-option\"\n [attr.aria-checked]=\"selected\"\n [tabindex]=\"tabbable ? 0 : -1\"\n (focus)=\"itemFocus.emit()\"\n (click)=\"selectedChange.emit(facet)\"\n (keydown.enter)=\"selectedChange.emit(facet)\"\n (keydown.space)=\"selectedChange.emit(facet); $event.preventDefault()\"\n (keydown.spacebar)=\"selectedChange.emit(facet); $event.preventDefault()\"\n>\n <ux-checkbox\n [clickable]=\"false\"\n [value]=\"selected\"\n [simplified]=\"simplified\"\n [tabindex]=\"-1\"\n [disabled]=\"disabled\"\n >\n <span class=\"facet-typeahead-list-selected-option-title\">{{ facet?.title }}</span>\n @if (facet?.count !== undefined) {\n <span class=\"facet-typeahead-list-selected-option-count\">({{ facet?.count }})</span>\n }\n </ux-checkbox>\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: CheckboxComponent, selector: "ux-checkbox", inputs: ["id", "name", "value", "required", "tabindex", "clickable", "simplified", "indeterminateValue", "disabled", "aria-label", "aria-labelledby"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadListItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-typeahead-list-item', changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, standalone: false, template: "<div\n #option\n uxFocusIndicator\n role=\"option\"\n class=\"facet-typeahead-list-selected-option\"\n [attr.aria-checked]=\"selected\"\n [tabindex]=\"tabbable ? 0 : -1\"\n (focus)=\"itemFocus.emit()\"\n (click)=\"selectedChange.emit(facet)\"\n (keydown.enter)=\"selectedChange.emit(facet)\"\n (keydown.space)=\"selectedChange.emit(facet); $event.preventDefault()\"\n (keydown.spacebar)=\"selectedChange.emit(facet); $event.preventDefault()\"\n>\n <ux-checkbox\n [clickable]=\"false\"\n [value]=\"selected\"\n [simplified]=\"simplified\"\n [tabindex]=\"-1\"\n [disabled]=\"disabled\"\n >\n <span class=\"facet-typeahead-list-selected-option-title\">{{ facet?.title }}</span>\n @if (facet?.count !== undefined) {\n <span class=\"facet-typeahead-list-selected-option-count\">({{ facet?.count }})</span>\n }\n </ux-checkbox>\n</div>\n" }]
}], propDecorators: { facet: [{
type: Input
}], selected: [{
type: Input
}], simplified: [{
type: Input
}], tabbable: [{
type: Input
}], itemFocus: [{
type: Output
}], selectedChange: [{
type: Output
}], option: [{
type: ViewChild,
args: ['option', { static: true }]
}] } });
let uniqueId$8 = 1;
class FacetTypeaheadListComponent {
constructor() {
this.typeaheadKeyService = inject(TypeaheadKeyService);
this.facetService = inject(FacetService);
this._announcer = inject(LiveAnnouncer);
/** Defines whether or not the Facet Typeahead List should be initially expanded or not. */
this.expanded = true;
/** Defines a list of facets which will be displayed above the typeahead to allow the user to quickly select some facets. */
this.suggestions = [];
/** Defines whether or not the checkboxes displayed alongside suggestions will appear in simplified form. */
this.simplified = true;
/** Emits the current query when the value of the input field changes. */
this.queryChange = new EventEmitter();
/**
* This will be triggered when a facet is selected, deselected or all facets are deselected.
* The event will be an instance of either `FacetSelect`, `FacetDeselect` or `FacetDeselectAll` and
* will contain the facet being selected or deselected in a `facet` property (deselect all will not contain affected facets).
*/
this.events = new Subject();
/** If two-way binding is used this array will get updated any time the selected facets change. */
this.selectedChange = new EventEmitter();
this.query$ = new BehaviorSubject('');
this.loading = false;
this.activeIndex = 0;
this.typeaheadId = `ux-facet-typeahead-${uniqueId$8++}`;
this.typeaheadOpen = false;
this.typeaheadOptions = [];
this._facets = [];
this._selected = [];
this._onDestroy = new Subject();
this._config = { placeholder: '', maxResults: 50, minCharacters: 1 };
}
/** This will allow you to define an initial set of selected facets. */
set selected(selection) {
if (Array.isArray(selection)) {
selection.forEach(facet => this.facetService.select(facet));
}
}
/** Defines the query displayed in the input field. */
set query(query) {
if (query !== this.query$.value) {
this.query$.next(query);
}
}
/**
* Allows configuration of the typeahead control. The possible values are:
* - `placeholder` - **string** - Sets the placeholder of the typeahead.
* - `minCharacters` - **number** - Defines the minimum number of characters that are required before results will be shown. **Default**: `1`.
* - `maxResults` - **number** - Sets the maximum number of results to display. **Default**: `50`.
* - `delay` - **number** - Defines the number of milliseconds to wait before the results are filtered. **Default**: `0`.
*/
set typeaheadConfig(config) {
this._config = { placeholder: '', maxResults: 50, minCharacters: 1, ...config };
}
get typeaheadConfig() {
return this._config;
}
ngOnInit() {
this.facetService.events$.pipe(takeUntil(this._onDestroy)).subscribe(event => {
// deselect all events should always be emitted
if (event instanceof FacetDeselectAll) {
this.events.next(event);
this._selected = [];
this.selectedChange.next(this._selected);
}
// if deselected remove the facet from our internal list of selected facets
if (event instanceof FacetDeselect && this.isOwnFacet(event.facet)) {
if (this._selected.indexOf(event.facet) !== -1) {
this.events.next(event);
this._selected = this._selected.filter(_facet => _facet !== event.facet);
this.selectedChange.next(this._selected);
}
}
// selection and deselection events should only be emitted when the facet belongs to this component
if (event instanceof FacetSelect && this.isOwnFacet(event.facet)) {
if (this._selected.indexOf(event.facet) === -1) {
this.events.next(event);
this._selected = [...this._selected, event.facet];
this.selectedChange.next(this._selected);
}
}
});
// store the original list of all possible facets
this.getFacetObservable()
.pipe(first())
.subscribe(facets => (this._facets = facets));
}
ngAfterViewInit() {
// emit the latest query whenever it changes
this.query$
.pipe(takeUntil(this._onDestroy), distinctUntilChanged())
.subscribe(query => this.queryChange.emit(query));
// set up search query subscription
this.query$
.pipe(takeUntil(this._onDestroy), tick(), tap(() => {
this.loading = true;
this.typeaheadOptions = [];
}), mergeMap(() => this.getFacetObservable().pipe(map(facets => {
return facets
.filter(facet => !facet.disabled &&
!this.facetService.facets$.value.find(selectedFacet => selectedFacet === facet))
.slice(0, this._config.maxResults);
}))))
.subscribe(facets => {
this.loading = false;
this.typeaheadOptions = facets;
});
this._focusKeyManager = new FocusKeyManager(this.options).withVerticalOrientation();
this._focusKeyManager.change
.pipe(takeUntil(this._onDestroy))
.subscribe(index => (this.activeIndex = index));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onKeydown(event) {
this._focusKeyManager.onKeydown(event);
}
onFocus(index) {
if (this._focusKeyManager.activeItemIndex === -1) {
this._focusKeyManager.setActiveItem(index);
}
}
toggleFacet(index, facet) {
// toggle selection
this.facetService.isSelected(facet)
? this.facetService.deselect(facet)
: this.facetService.select(facet);
// focus the correct item
this._focusKeyManager.setActiveItem(index);
}
/** Only show typeahead if we have enough characters */
updateTypeahead(query = '') {
this.typeaheadOpen = query.length >= this._config.minCharacters;
}
getFacetObservable() {
return isObservable(this.facets) ? this.facets : of(this.facets);
}
select(event) {
// check to make sure that the item is not currently selected
if (this.facetService.isSelected(event.option)) {
return;
}
// select the facet
this.facetService.select(event.option);
// clear the typeahead
this.query$.next('');
// announce the selected facet
this._announcer.announce(`${event.option.title} selected.`);
}
isOwnFacet(facet) {
return this._facets ? this._facets.indexOf(facet) !== -1 : false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FacetTypeaheadListComponent, isStandalone: false, selector: "ux-facet-typeahead-list", inputs: { selected: "selected", facets: "facets", header: "header", expanded: "expanded", suggestions: "suggestions", simplified: "simplified", query: "query", typeaheadConfig: "typeaheadConfig" }, outputs: { queryChange: "queryChange", events: "events", selectedChange: "selectedChange" }, viewQueries: [{ propertyName: "options", predicate: FacetTypeaheadListItemComponent, descendants: true }], ngImport: i0, template: "<ux-facet-header [header]=\"header\" [(expanded)]=\"expanded\"></ux-facet-header>\n\n@if (expanded) {\n <div class=\"facet-typeahead-list-container\" role=\"listbox\">\n @if (suggestions?.length > 0) {\n <div class=\"facet-typeahead-list-selected-container\" tabindex=\"-1\">\n @for (facet of suggestions; track facet; let index = $index) {\n <ux-facet-typeahead-list-item\n [facet]=\"facet\"\n [tabbable]=\"activeIndex === index\"\n [selected]=\"facetService.isSelected(facet)\"\n (selectedChange)=\"toggleFacet(index, facet)\"\n (keydown)=\"onKeydown($event)\"\n (itemFocus)=\"onFocus(index)\"\n >\n </ux-facet-typeahead-list-item>\n }\n </div>\n }\n <div class=\"facet-typeahead-list-control\">\n <!-- Create Typeahead Control -->\n <input\n type=\"text\"\n class=\"form-control\"\n [placeholder]=\"typeaheadConfig?.placeholder\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n aria-autocomplete=\"list\"\n aria-multiline=\"false\"\n [attr.aria-controls]=\"typeaheadId\"\n [ngModel]=\"query$ | async\"\n (ngModelChange)=\"query$.next($event); updateTypeahead($event)\"\n (keydown)=\"typeaheadKeyService.handleKey($event, typeahead)\"\n (blur)=\"typeaheadOpen = false\"\n />\n <ux-typeahead\n #typeahead\n [id]=\"typeaheadId\"\n [(open)]=\"typeaheadOpen\"\n [loading]=\"loading\"\n display=\"title\"\n [options]=\"typeaheadOptions\"\n [optionTemplate]=\"facetOptionTemplate\"\n [selectOnEnter]=\"true\"\n (optionSelected)=\"select($event)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n >\n </ux-typeahead>\n </div>\n </div>\n}\n\n<ng-template #facetOptionTemplate let-option=\"option\" let-api=\"api\">\n <p class=\"facet-typeahead-list-option\" [attr.aria-label]=\"option.title\">\n <span [uxSafeInnerHtml]=\"option.title | facetTypeaheadHighlight: (query$ | async)\"></span>\n @if (option.count) {\n <span class=\"facet-typeahead-list-option-count\"> ({{ option.count }}) </span>\n }\n </p>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i0.forwardRef(() => i2$2.DefaultValueAccessor), selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i0.forwardRef(() => i2$2.NgControlStatus), selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i0.forwardRef(() => i2$2.NgModel), selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i0.forwardRef(() => TypeaheadComponent), selector: "ux-typeahead", inputs: ["id", "options", "filter", "open", "display", "key", "disabledOptions", "dropDirection", "maxHeight", "multiselectable", "ariaLabel", "openOnFilterChange", "pageSize", "selectFirst", "selectOnEnter", "loading", "loadingTemplate", "optionTemplate", "noOptionsTemplate", "active", "recentOptions", "recentOptionsMaxCount", "recentOptionsHeadingTemplate", "optionsHeadingTemplate"], outputs: ["openChange", "optionSelected", "highlightedChange", "highlightedElementChange", "recentOptionsChange"] }, { kind: "directive", type: i0.forwardRef(() => SafeInnerHtmlDirective), selector: "[uxSafeInnerHtml]", inputs: ["uxSafeInnerHtml"] }, { kind: "component", type: i0.forwardRef(() => FacetHeaderComponent), selector: "ux-facet-header", inputs: ["header", "canExpand", "expanded"], outputs: ["expandedChange"] }, { kind: "component", type: i0.forwardRef(() => FacetTypeaheadListItemComponent), selector: "ux-facet-typeahead-list-item", inputs: ["facet", "selected", "simplified", "tabbable"], outputs: ["itemFocus", "selectedChange"] }, { kind: "pipe", type: i0.forwardRef(() => i1.AsyncPipe), name: "async" }, { kind: "pipe", type: i0.forwardRef(() => FacetTypeaheadHighlight), name: "facetTypeaheadHighlight" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadListComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-facet-typeahead-list', standalone: false, template: "<ux-facet-header [header]=\"header\" [(expanded)]=\"expanded\"></ux-facet-header>\n\n@if (expanded) {\n <div class=\"facet-typeahead-list-container\" role=\"listbox\">\n @if (suggestions?.length > 0) {\n <div class=\"facet-typeahead-list-selected-container\" tabindex=\"-1\">\n @for (facet of suggestions; track facet; let index = $index) {\n <ux-facet-typeahead-list-item\n [facet]=\"facet\"\n [tabbable]=\"activeIndex === index\"\n [selected]=\"facetService.isSelected(facet)\"\n (selectedChange)=\"toggleFacet(index, facet)\"\n (keydown)=\"onKeydown($event)\"\n (itemFocus)=\"onFocus(index)\"\n >\n </ux-facet-typeahead-list-item>\n }\n </div>\n }\n <div class=\"facet-typeahead-list-control\">\n <!-- Create Typeahead Control -->\n <input\n type=\"text\"\n class=\"form-control\"\n [placeholder]=\"typeaheadConfig?.placeholder\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n aria-autocomplete=\"list\"\n aria-multiline=\"false\"\n [attr.aria-controls]=\"typeaheadId\"\n [ngModel]=\"query$ | async\"\n (ngModelChange)=\"query$.next($event); updateTypeahead($event)\"\n (keydown)=\"typeaheadKeyService.handleKey($event, typeahead)\"\n (blur)=\"typeaheadOpen = false\"\n />\n <ux-typeahead\n #typeahead\n [id]=\"typeaheadId\"\n [(open)]=\"typeaheadOpen\"\n [loading]=\"loading\"\n display=\"title\"\n [options]=\"typeaheadOptions\"\n [optionTemplate]=\"facetOptionTemplate\"\n [selectOnEnter]=\"true\"\n (optionSelected)=\"select($event)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n >\n </ux-typeahead>\n </div>\n </div>\n}\n\n<ng-template #facetOptionTemplate let-option=\"option\" let-api=\"api\">\n <p class=\"facet-typeahead-list-option\" [attr.aria-label]=\"option.title\">\n <span [uxSafeInnerHtml]=\"option.title | facetTypeaheadHighlight: (query$ | async)\"></span>\n @if (option.count) {\n <span class=\"facet-typeahead-list-option-count\"> ({{ option.count }}) </span>\n }\n </p>\n</ng-template>\n" }]
}], propDecorators: { selected: [{
type: Input
}], facets: [{
type: Input
}], header: [{
type: Input
}], expanded: [{
type: Input
}], suggestions: [{
type: Input
}], simplified: [{
type: Input
}], query: [{
type: Input
}], queryChange: [{
type: Output
}], events: [{
type: Output
}], selectedChange: [{
type: Output
}], typeaheadConfig: [{
type: Input
}], options: [{
type: ViewChildren,
args: [FacetTypeaheadListItemComponent]
}] } });
class FacetTypeaheadHighlight {
transform(value, searchQuery) {
const regex = new RegExp(searchQuery, 'i');
return value.replace(regex, `<b class="facet-typeahead-highlighted">${value.match(regex)}</b>`);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadHighlight, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadHighlight, isStandalone: false, name: "facetTypeaheadHighlight" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetTypeaheadHighlight, decorators: [{
type: Pipe,
args: [{
name: 'facetTypeaheadHighlight',
standalone: false,
}]
}] });
class ReorderableHandleDirective extends CdkDragHandle {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableHandleDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ReorderableHandleDirective, isStandalone: false, selector: "[uxReorderableHandle]", providers: [{ provide: CDK_DRAG_HANDLE, useExisting: ReorderableHandleDirective }], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableHandleDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxReorderableHandle]',
providers: [{ provide: CDK_DRAG_HANDLE, useExisting: ReorderableHandleDirective }],
standalone: false,
}]
}] });
class ReorderableModelDirective extends CdkDrag {
constructor() {
super(...arguments);
/** Apply the dragula preview class to avoid backwards compatibility issues */
this.previewClass = 'gu-mirror';
/** Preserve the column widths */
this._widths = new Map();
/** Unsubscribe on destroy */
this._destroy$ = new Subject();
}
// allow the user to specify a model for the item - allows use with ngFor
set uxReorderableModel(model) {
this.data = model;
}
ngOnInit() {
// cast the drop container as we have replaced it with our directive
const dropContainer = this.dropContainer;
this._dragRef.beforeStarted
.pipe(takeUntil(this._destroy$))
.subscribe(() => this.captureTableCellStyles());
this.started.pipe(takeUntil(this._destroy$)).subscribe(() => {
dropContainer.reorderStart.emit({
element: this.element.nativeElement,
model: this.data,
});
this.setTableCellWidths();
});
this.dropped.pipe(takeUntil(this._destroy$)).subscribe((dragEvent) => {
if (dragEvent.container === dragEvent.previousContainer &&
dragEvent.currentIndex === dragEvent.previousIndex) {
dropContainer.reorderCancel.emit({
element: this.element.nativeElement,
model: this.data,
});
}
else {
dropContainer.reorderEnd.emit({
element: this.element.nativeElement,
model: this.data,
});
}
});
}
ngOnDestroy() {
super.ngOnDestroy();
this._destroy$.next();
this._destroy$.complete();
}
/**
* When table elements are being dragged they are hoisted to the document level
* when dragged they lose the sizing and spacing provided when they are used inside a table.
*
* This function will capture the styles so we can inline them to preseve the styling.
*/
captureTableCellStyles() {
// if it is not a table row then skip this
if (this.element.nativeElement.tagName !== 'TR') {
return;
}
// iterate each cell and store the styles and enforce the width by using a minWidth
Array.from(this.element.nativeElement.children).forEach((cell, index) => {
this._widths.set(index, getComputedStyle(cell).getPropertyValue('width'));
});
}
setTableCellWidths() {
// if it is not a table row then skip this
if (this.element.nativeElement.tagName !== 'TR') {
return;
}
// access the preview element, this is private but there is no public way to access
// it and the UI is incorrect when draggingtable rows without this.
const previewElement = this._dragRef._preview;
// re-apply all the stored styles
Array.from(previewElement.children).forEach((cell, index) => (cell.style.minWidth = this._widths.get(index)));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModelDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ReorderableModelDirective, isStandalone: false, selector: "[uxReorderableModel]", inputs: { uxReorderableModel: "uxReorderableModel" }, host: { properties: { "class.ux-reorderable-moving": "_dragRef.isDragging()" } }, providers: [{ provide: CDK_DRAG_PARENT, useExisting: ReorderableModelDirective }], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModelDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxReorderableModel]',
providers: [{ provide: CDK_DRAG_PARENT, useExisting: ReorderableModelDirective }],
host: {
'[class.ux-reorderable-moving]': '_dragRef.isDragging()',
},
standalone: false,
}]
}], propDecorators: { uxReorderableModel: [{
type: Input
}] } });
class ReorderableDirective extends CdkDropList {
constructor() {
super(...arguments);
/**
* This event will be triggered when the order changes and will contain an updated dataset containing the items
* in their current order. This should be used when the list of items is generated using ngFor to ensure the
* data remains in the same order for both the `uxReorderable` and `ngFor` directives.
*/
this.reorderableModelChange = new EventEmitter();
/** This event is triggered when a user begins dragging an item. The event will contain the element being moved. */
this.reorderStart = new EventEmitter();
/** This event is triggered when the item being dragged is returned to the same location as it began. The event will contain the element that was being moved. */
this.reorderCancel = new EventEmitter();
/** This event is triggered when a user has relocated an item. The event will contain the element that was moved. */
this.reorderEnd = new EventEmitter();
this._destroy$ = new Subject();
}
/**
* The name of the reorderable group which this container belongs to `uxReorderable` elements which belong to
* the same group can have items dragged between them. Only required if multiple drop containers are being created.
*/
set reorderableGroup(group) {
const groups = ReorderableDirective._groups$.value;
this._reorderableGroup = group;
ReorderableDirective._groups$.next({
...groups,
[group]: [...(groups[group] ?? []), this.id],
});
}
get reorderableGroup() {
return this._reorderableGroup;
}
/** Determines if reordering is disabled. */
set reorderingDisabled(isDisabled) {
this.disabled = isDisabled;
}
/** Store all the group ids so we can identify which lists can interact */
static { this._groups$ = new BehaviorSubject({}); }
ngOnInit() {
this.dropped.pipe(takeUntil(this._destroy$)).subscribe((dropEvent) => {
if (dropEvent.previousContainer === dropEvent.container) {
moveItemInArray(this.reorderableModel, dropEvent.previousIndex, dropEvent.currentIndex);
}
else {
const previousContainer = dropEvent.previousContainer;
const currentContainer = dropEvent.container;
transferArrayItem(previousContainer.reorderableModel, currentContainer.reorderableModel, dropEvent.previousIndex, dropEvent.currentIndex);
}
this.reorderableModelChange.emit(this.reorderableModel);
});
// if the available groups are updated we need to update the lists we can drag to
ReorderableDirective._groups$
.pipe(takeUntil(this._destroy$))
.subscribe(groups => (this.connectedTo = (groups[this.reorderableGroup] ?? []).filter(group => group !== this.id)));
}
ngOnDestroy() {
this._destroy$.next();
this._destroy$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ReorderableDirective, isStandalone: false, selector: "[uxReorderable]", inputs: { reorderableModel: "reorderableModel", reorderableGroup: "reorderableGroup", reorderingDisabled: "reorderingDisabled" }, outputs: { reorderableModelChange: "reorderableModelChange", reorderStart: "reorderStart", reorderCancel: "reorderCancel", reorderEnd: "reorderEnd" }, providers: [{ provide: CDK_DROP_LIST, useExisting: ReorderableDirective }], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxReorderable]',
providers: [{ provide: CDK_DROP_LIST, useExisting: ReorderableDirective }],
standalone: false,
}]
}], propDecorators: { reorderableModel: [{
type: Input
}], reorderableGroup: [{
type: Input
}], reorderingDisabled: [{
type: Input
}], reorderableModelChange: [{
type: Output
}], reorderStart: [{
type: Output
}], reorderCancel: [{
type: Output
}], reorderEnd: [{
type: Output
}] } });
class ReorderableModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModule, declarations: [ReorderableDirective, ReorderableHandleDirective, ReorderableModelDirective], imports: [CommonModule, DragDropModule], exports: [ReorderableDirective, ReorderableHandleDirective, ReorderableModelDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModule, imports: [CommonModule, DragDropModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ReorderableModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, DragDropModule],
declarations: [ReorderableDirective, ReorderableHandleDirective, ReorderableModelDirective],
exports: [ReorderableDirective, ReorderableHandleDirective, ReorderableModelDirective],
}]
}] });
const DECLARATIONS$8 = [
FacetContainerComponent,
FacetHeaderComponent,
FacetCheckListComponent,
FacetCheckListItemComponent,
FacetTypeaheadListComponent,
FacetTypeaheadListItemComponent,
FacetTypeaheadHighlight,
FacetClearButtonDirective,
];
class FacetsModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FacetsModule, declarations: [FacetContainerComponent,
FacetHeaderComponent,
FacetCheckListComponent,
FacetCheckListItemComponent,
FacetTypeaheadListComponent,
FacetTypeaheadListItemComponent,
FacetTypeaheadHighlight,
FacetClearButtonDirective], imports: [A11yModule,
AccessibilityModule,
CheckboxModule,
CommonModule,
FormsModule,
IconModule,
ReorderableModule,
TooltipModule,
TypeaheadModule,
DragDropModule,
SafeInnerHtmlDirective], exports: [FacetContainerComponent,
FacetHeaderComponent,
FacetCheckListComponent,
FacetCheckListItemComponent,
FacetTypeaheadListComponent,
FacetTypeaheadListItemComponent,
FacetTypeaheadHighlight,
FacetClearButtonDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetsModule, imports: [A11yModule,
AccessibilityModule,
CheckboxModule,
CommonModule,
FormsModule,
IconModule,
ReorderableModule,
TooltipModule,
TypeaheadModule,
DragDropModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FacetsModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
CheckboxModule,
CommonModule,
FormsModule,
IconModule,
ReorderableModule,
TooltipModule,
TypeaheadModule,
DragDropModule,
SafeInnerHtmlDirective,
],
exports: DECLARATIONS$8,
declarations: DECLARATIONS$8,
}]
}] });
class Facet {
constructor(title, data = {}, count, disabled = false, id) {
this.title = title;
this.data = data;
this.count = count;
this.disabled = disabled;
this.id = id;
}
}
class FilterAddEvent {
constructor(filter) {
this.filter = filter;
}
}
class FilterRemoveAllEvent {
}
class FilterRemoveEvent {
constructor(filter) {
this.filter = filter;
}
}
class FilterService {
constructor() {
/** The list of active filters */
this.filters$ = new BehaviorSubject([]);
/** Emit all the events when they occur */
this.events$ = new Subject();
}
add(filter) {
// if the filter is already selected or it is the intial filter then do nothing
if (this.isSelected(filter) || filter.initial) {
return;
}
// update the list of active filters
this.filters$.next([...this.filters$.value, filter]);
// emit the event
this.events$.next(new FilterAddEvent(filter));
}
remove(filter) {
// if the filter is not selected then do nothing
if (!this.isSelected(filter)) {
return;
}
// update the list of active filters
this.filters$.next(this.filters$.value.filter(_filter => _filter !== filter));
// emit the event
this.events$.next(new FilterRemoveEvent(filter));
}
removeAll() {
// empty the list of active filters
this.filters$.next([]);
// emit the event
this.events$.next(new FilterRemoveAllEvent());
}
isSelected(filter) {
return this.filters$.value.indexOf(filter) > -1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterService, decorators: [{
type: Injectable
}] });
class FilterContainerComponent {
/** Allow filters to set from outside the component */
set filters(filters) {
this.filterService.filters$.next(filters);
}
constructor() {
this.filterService = inject(FilterService);
/** Defines the aria-label for the clear all button */
this.clearAriaLabel = 'Clear All Filters';
/** Emit when the active filters chance */
this.filtersChange = new EventEmitter();
/** Emit when a specific event occurs */
this.events = new EventEmitter();
/** Unsubscribe from the subscriptions on destroy */
this._onDestroy = new Subject();
// subscribe to changes to the active filters
this.filterService.filters$
.pipe(distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(filters => this.filtersChange.emit(filters));
// relay any events to the event emitter
this.filterService.events$
.pipe(takeUntil(this._onDestroy))
.subscribe(event => this.events.emit(event));
}
/** Destroy all subscriptions */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FilterContainerComponent, isStandalone: false, selector: "ux-filter-container", inputs: { filters: "filters", clearTooltip: "clearTooltip", clearAriaLabel: "clearAriaLabel" }, outputs: { filtersChange: "filtersChange", events: "events" }, providers: [FilterService], queries: [{ propertyName: "clearAllTemplate", first: true, predicate: ["clearAllTemplate"], descendants: true }], ngImport: i0, template: "<ng-content></ng-content>\n\n<!-- Add a Clear Button -->\n@if ((filterService.filters$ | async).length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-link btn-secondary m-l-xs\"\n [class.table-filter-clear]=\"clearAllTemplate\"\n [class.btn-icon]=\"!clearAllTemplate\"\n tabindex=\"0\"\n [attr.aria-label]=\"clearAriaLabel\"\n [uxTooltip]=\"clearTooltip || 'Clear All'\"\n (click)=\"filterService.removeAll()\"\n >\n <ng-container [ngTemplateOutlet]=\"clearAllTemplate || defaultClearAllTemplate\"> </ng-container>\n </button>\n}\n\n<ng-template #defaultClearAllTemplate>\n <svg\n class=\"filter-selected-clear-graphic\"\n width=\"100%\"\n viewBox=\"0 0 19 12\"\n shape-rendering=\"geometricPrecision\"\n >\n <rect class=\"light-grey\" x=\"0\" y=\"2\" width=\"7\" height=\"2\"></rect>\n <rect class=\"dark-grey\" x=\"0\" y=\"5\" width=\"9\" height=\"2\"></rect>\n <rect class=\"light-grey\" x=\"0\" y=\"8\" width=\"7\" height=\"2\"></rect>\n <path class=\"dark-grey\" d=\"M9,1 h1 l9,9 v1 h-1 l-9,-9 v-1 Z\"></path>\n <path class=\"dark-grey\" d=\"M9,11 v-1 l9,-9 h1 v1 l-9,9 h-1 Z\"></path>\n </svg>\n</ng-template>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterContainerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-filter-container', providers: [FilterService], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ng-content></ng-content>\n\n<!-- Add a Clear Button -->\n@if ((filterService.filters$ | async).length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-link btn-secondary m-l-xs\"\n [class.table-filter-clear]=\"clearAllTemplate\"\n [class.btn-icon]=\"!clearAllTemplate\"\n tabindex=\"0\"\n [attr.aria-label]=\"clearAriaLabel\"\n [uxTooltip]=\"clearTooltip || 'Clear All'\"\n (click)=\"filterService.removeAll()\"\n >\n <ng-container [ngTemplateOutlet]=\"clearAllTemplate || defaultClearAllTemplate\"> </ng-container>\n </button>\n}\n\n<ng-template #defaultClearAllTemplate>\n <svg\n class=\"filter-selected-clear-graphic\"\n width=\"100%\"\n viewBox=\"0 0 19 12\"\n shape-rendering=\"geometricPrecision\"\n >\n <rect class=\"light-grey\" x=\"0\" y=\"2\" width=\"7\" height=\"2\"></rect>\n <rect class=\"dark-grey\" x=\"0\" y=\"5\" width=\"9\" height=\"2\"></rect>\n <rect class=\"light-grey\" x=\"0\" y=\"8\" width=\"7\" height=\"2\"></rect>\n <path class=\"dark-grey\" d=\"M9,1 h1 l9,9 v1 h-1 l-9,-9 v-1 Z\"></path>\n <path class=\"dark-grey\" d=\"M9,11 v-1 l9,-9 h1 v1 l-9,9 h-1 Z\"></path>\n </svg>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { filters: [{
type: Input
}], clearTooltip: [{
type: Input
}], clearAriaLabel: [{
type: Input
}], filtersChange: [{
type: Output
}], events: [{
type: Output
}], clearAllTemplate: [{
type: ContentChild,
args: ['clearAllTemplate', { static: false }]
}] } });
let uniqueId$7 = 0;
class FilterDropdownComponent {
/** Defined the closeOnBlur state for the ux-menu trigger */
set closeOnBlur(value) {
this._closeOnBlur = coerceBooleanProperty(value);
}
get closeOnBlur() {
return this._closeOnBlur;
}
get filterId() {
return this.id ?? `ux-filter-dropdown-${this._uniqueId}`;
}
constructor() {
this._filterService = inject(FilterService);
this._changeDetector = inject(ChangeDetectorRef);
/** Store the unique id so we only increment the counter once per instance */
this._uniqueId = uniqueId$7++;
/** The list of items to display in the dropdown */
this.filters = [];
/** Emit when the filter menu is closed */
this.closed = new EventEmitter();
this._onDestroy = new Subject();
this._closeOnBlur = false;
this._filterService.events$
.pipe(filter(event => event instanceof FilterRemoveAllEvent), takeUntil(this._onDestroy))
.subscribe(() => this.removeFilter());
// ensure that the current selected filter is still selected when the active filters change
this._filterService.filters$.pipe(takeUntil(this._onDestroy)).subscribe(filters => {
if (this.selected && filters.indexOf(this.selected) === -1) {
this.removeFilter();
}
});
}
ngOnInit() {
this.selected = this.initial;
// check to see if any of the filters have been preselected or changes to selected filters
this._filterService.filters$.pipe(takeUntil(this._onDestroy)).subscribe(filters => {
filters.forEach(filter => {
if (this.filters.indexOf(filter) !== -1) {
this.selected = filter;
}
});
this._changeDetector.markForCheck();
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
selectFilter(filter, event) {
this.removeFilter();
this.selected = filter;
this._filterService.add(this.selected);
event.stopPropagation();
event.preventDefault();
}
removeFilter() {
this._filterService.remove(this.selected);
this.selected = this.initial;
this._changeDetector.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FilterDropdownComponent, isStandalone: false, selector: "ux-filter-dropdown", inputs: { id: "id", filters: "filters", initial: "initial", closeOnBlur: "closeOnBlur" }, outputs: { closed: "closed" }, ngImport: i0, template: "<div class=\"btn-group\">\n <button\n type=\"button\"\n class=\"filter-dropdown btn dropdown-toggle\"\n [id]=\"filterId + '-trigger'\"\n [class.active]=\"selected !== initial\"\n [uxMenuTriggerFor]=\"menu\"\n [closeOnBlur]=\"closeOnBlur\"\n (closed)=\"closed.emit()\"\n >\n {{ selected?.group }}\n @if (selected !== initial) {\n <span class=\"filter-header\"> ({{ selected?.name }}) </span>\n }\n <ux-icon name=\"down\"></ux-icon>\n </button>\n\n <ux-menu #menu menuClass=\"ux-filter-menu\">\n @for (filter of filters; track filter; let index = $index) {\n <button\n type=\"button\"\n uxMenuItem\n [id]=\"filter.id || filterId + '-item-' + index\"\n [attr.aria-selected]=\"filter === selected\"\n (click)=\"selectFilter(filter, $event)\"\n (keydown.enter)=\"selectFilter(filter, $event)\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"filter === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">{{ filter.name }}</span>\n </button>\n }\n </ux-menu>\n</div>\n", dependencies: [{ kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "component", type: MenuItemComponent, selector: "[uxMenuItem]", inputs: ["disabled", "closeOnSelect", "role"], outputs: ["activate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterDropdownComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-filter-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"btn-group\">\n <button\n type=\"button\"\n class=\"filter-dropdown btn dropdown-toggle\"\n [id]=\"filterId + '-trigger'\"\n [class.active]=\"selected !== initial\"\n [uxMenuTriggerFor]=\"menu\"\n [closeOnBlur]=\"closeOnBlur\"\n (closed)=\"closed.emit()\"\n >\n {{ selected?.group }}\n @if (selected !== initial) {\n <span class=\"filter-header\"> ({{ selected?.name }}) </span>\n }\n <ux-icon name=\"down\"></ux-icon>\n </button>\n\n <ux-menu #menu menuClass=\"ux-filter-menu\">\n @for (filter of filters; track filter; let index = $index) {\n <button\n type=\"button\"\n uxMenuItem\n [id]=\"filter.id || filterId + '-item-' + index\"\n [attr.aria-selected]=\"filter === selected\"\n (click)=\"selectFilter(filter, $event)\"\n (keydown.enter)=\"selectFilter(filter, $event)\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"filter === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">{{ filter.name }}</span>\n </button>\n }\n </ux-menu>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { id: [{
type: Input
}], filters: [{
type: Input
}], initial: [{
type: Input
}], closeOnBlur: [{
type: Input
}], closed: [{
type: Output
}] } });
class FilterTypeaheadHighlight {
transform(value, searchQuery) {
const regex = new RegExp(searchQuery, 'i');
return value.replace(regex, `<b class="filter-typeahead-highlighted">${value.match(regex)}</b>`);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterTypeaheadHighlight, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FilterTypeaheadHighlight, isStandalone: false, name: "filterTypeaheadHighlight" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterTypeaheadHighlight, decorators: [{
type: Pipe,
args: [{
name: 'filterTypeaheadHighlight',
standalone: false,
}]
}] });
let uniqueId$6 = 0;
class FilterDynamicComponent {
/** Defined the closeOnBlur state for the ux-menu trigger */
set closeOnBlur(value) {
this._closeOnBlur = coerceBooleanProperty(value);
}
get closeOnBlur() {
return this._closeOnBlur;
}
/** Specify the typeahead options */
set options(options) {
this._options = options;
}
/** Get the options with the defaults for any missing options */
get options() {
return { ...this._defaultOptions, ...this._options };
}
/** Get the user provided id or fallback to a default ID */
get filterId() {
return this.id ?? `ux-filter-dynamic-${this._uniqueId}`;
}
constructor() {
this.typeaheadKeyService = inject(TypeaheadKeyService);
this._filterService = inject(FilterService);
this._changeDetector = inject(ChangeDetectorRef);
/** The unique id is used multiple times - this is to ensure we only increment it once per instance */
this._uniqueId = uniqueId$6++;
/** The list of possible filter options */
this.filters = [];
/** Emit when the filter menu is closed */
this.closed = new EventEmitter();
/** Generate a unique id for the typeahead */
this.typeaheadId = `ux-filter-dynamic-typeahead-${this._uniqueId}`;
/** Store the current search query */
this.query$ = new BehaviorSubject('');
/** Indicate whether or not the typeahead should be shown */
this.showTypeahead = true;
/** Store the items that should be displayed in the typeahead */
this.typeaheadItems = [];
/** Store the open state of the typeahead */
this.typeaheadOpen = false;
/** The default options */
this._defaultOptions = {
placeholder: '',
minCharacters: 3,
maxResults: Infinity,
};
/** Store the user specified typeahead options */
this._options = { ...this._defaultOptions };
/** Unsubscribe from all subscriptions */
this._onDestroy = new Subject();
this._closeOnBlur = false;
// listen for remove all events in which case we should deselect event initial filters
this._filterService.events$
.pipe(filter(event => event instanceof FilterRemoveAllEvent), takeUntil(this._onDestroy))
.subscribe(() => this.removeFilter());
// ensure that the current selected filter is still selected when the active filters change
this._filterService.filters$.pipe(takeUntil(this._onDestroy)).subscribe(filters => {
if (this.selected && filters.indexOf(this.selected) === -1) {
this.removeFilter();
}
});
}
/** Set up the initial conditions */
ngOnInit() {
// The initially selected item should be set the the specified initial item
this.selected = this.initial;
// watch for changes to the selected filters
this._filterService.filters$.pipe(takeUntil(this._onDestroy)).subscribe(filters => {
filters.forEach(filter => {
if (this.filters.indexOf(filter) !== -1) {
this.selected = filter;
}
});
this._changeDetector.markForCheck();
});
// get the items to be displayed in the typeahead
this.typeaheadItems = this.getItems();
// hide the typeahead if the number of filters always visible equals or exceeds the
// total number of filters as there would be no additional filters to display in the typeahead
const shouldHideTypeahead = this.options?.maxIndividualItems + 1 >= this.filters.length;
if (shouldHideTypeahead) {
this.showTypeahead = false;
}
}
/** Cleanup all subscriptions */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Get the items to display in the typeahead based on the search query */
getItems() {
const query = this.query$.value.toLowerCase();
return this.filters
.filter(item => item !== this.initial && item.name.toLowerCase().indexOf(query) !== -1)
.map(item => item.name)
.slice(0, this._options.maxResults);
}
/** When the dropdown is closed clear the query */
onClose() {
this.query$.next('');
}
/** If a filter needs removed, and is not the initial filter then remove it */
removeFilter() {
// check if the filter we want to remove is the initial filter
if (this.selected !== this.initial) {
this._filterService.remove(this.selected);
this.selected = this.initial;
}
// clear the search query
this.query$.next('');
this._changeDetector.markForCheck();
}
/** Select a specific filter */
selectFilter(filter) {
// clear any current filters
this.removeFilter();
// store the newly selected filter
this.selected = filter;
// store the filter in the service
this._filterService.add(this.selected);
}
/** Update typeahead items and visibility */
updateTypeahead(query) {
this.typeaheadOpen = query.length >= this._options.minCharacters;
this.typeaheadItems = this.getItems();
}
/** Select a filter from a typeahead item */
select(event) {
// find the filter with the matching name
const filter = this.filters.find(_filter => _filter.name === event.option);
if (filter) {
this.selectFilter(filter);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterDynamicComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FilterDynamicComponent, isStandalone: false, selector: "ux-filter-dynamic", inputs: { id: "id", filters: "filters", initial: "initial", closeOnBlur: "closeOnBlur", options: "options" }, outputs: { closed: "closed" }, ngImport: i0, template: "<div class=\"btn-group ux-dynamic-filter\">\n <button\n type=\"button\"\n [class.active]=\"selected !== initial\"\n class=\"filter-dropdown btn dropdown-toggle\"\n [id]=\"filterId + '-trigger'\"\n [uxMenuTriggerFor]=\"menu\"\n [closeOnBlur]=\"closeOnBlur\"\n (closed)=\"closed.emit()\"\n #trigger=\"ux-menu-trigger\"\n >\n {{ selected?.group }}\n @if (selected !== initial) {\n <span class=\"filter-header\"> ({{ selected?.name }}) </span>\n }\n <ux-icon name=\"down\"></ux-icon>\n </button>\n\n <ux-menu #menu menuClass=\"ux-dynamic-filter-menu\" (closed)=\"onClose()\">\n <!-- Initial Option -->\n @if (showTypeahead) {\n <button\n uxMenuItem\n [id]=\"initial.id || filterId + '-initial-option'\"\n (click)=\"removeFilter()\"\n (keydown.enter)=\"removeFilter()\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"initial === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">\n {{ initial.name }}\n </span>\n </button>\n }\n\n <!-- Selected Options -->\n @if (selected !== initial && showTypeahead) {\n <button uxMenuItem [id]=\"selected.id || filterId + '-selection'\">\n <ux-icon name=\"checkmark\"></ux-icon>\n <span class=\"filter-dropdown-title\">{{ selected.name }}</span>\n </button>\n }\n\n @if (showTypeahead) {\n <ux-menu-divider></ux-menu-divider>\n }\n\n @if (showTypeahead) {\n <div class=\"typeahead-box\" role=\"none\">\n <input\n type=\"text\"\n class=\"form-control\"\n [placeholder]=\"options?.placeholder\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n [attr.aria-controls]=\"typeaheadId\"\n aria-autocomplete=\"list\"\n aria-multiline=\"false\"\n [ngModel]=\"query$ | async\"\n (ngModelChange)=\"query$.next($event); updateTypeahead($event)\"\n (keydown)=\"typeaheadKeyService.handleKey($event, typeahead); $event.stopPropagation()\"\n (keydown.enter)=\"$event.preventDefault()\"\n (blur)=\"typeaheadOpen = false\"\n (click)=\"$event.stopPropagation()\"\n />\n <ux-typeahead\n #typeahead\n [id]=\"typeaheadId\"\n [(open)]=\"typeaheadOpen\"\n display=\"title\"\n [selectOnEnter]=\"true\"\n [options]=\"typeaheadItems\"\n [optionTemplate]=\"filterOptionTemplate\"\n (optionSelected)=\"select($event); trigger.closeMenu($event.origin)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n >\n </ux-typeahead>\n </div>\n }\n\n @if (!showTypeahead) {\n @for (filter of filters; track filter; let index = $index) {\n <button\n type=\"button\"\n uxMenuItem\n [id]=\"filter.id || filterId + '-item-' + index\"\n (click)=\"selectFilter(filter); trigger.closeMenu('mouse')\"\n (keydown.enter)=\"selectFilter(filter); trigger.closeMenu('keyboard')\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"filter === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">{{ filter.name }}</span>\n </button>\n }\n }\n </ux-menu>\n</div>\n\n<ng-template #filterOptionTemplate let-option=\"option\" let-api=\"api\">\n <span\n [attr.aria-label]=\"option\"\n [uxSafeInnerHtml]=\"option | filterTypeaheadHighlight: (query$ | async)\"\n ></span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "component", type: MenuItemComponent, selector: "[uxMenuItem]", inputs: ["disabled", "closeOnSelect", "role"], outputs: ["activate"] }, { kind: "component", type: MenuDividerComponent, selector: "ux-menu-divider" }, { kind: "component", type: TypeaheadComponent, selector: "ux-typeahead", inputs: ["id", "options", "filter", "open", "display", "key", "disabledOptions", "dropDirection", "maxHeight", "multiselectable", "ariaLabel", "openOnFilterChange", "pageSize", "selectFirst", "selectOnEnter", "loading", "loadingTemplate", "optionTemplate", "noOptionsTemplate", "active", "recentOptions", "recentOptionsMaxCount", "recentOptionsHeadingTemplate", "optionsHeadingTemplate"], outputs: ["openChange", "optionSelected", "highlightedChange", "highlightedElementChange", "recentOptionsChange"] }, { kind: "directive", type: SafeInnerHtmlDirective, selector: "[uxSafeInnerHtml]", inputs: ["uxSafeInnerHtml"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: FilterTypeaheadHighlight, name: "filterTypeaheadHighlight" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterDynamicComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-filter-dynamic', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"btn-group ux-dynamic-filter\">\n <button\n type=\"button\"\n [class.active]=\"selected !== initial\"\n class=\"filter-dropdown btn dropdown-toggle\"\n [id]=\"filterId + '-trigger'\"\n [uxMenuTriggerFor]=\"menu\"\n [closeOnBlur]=\"closeOnBlur\"\n (closed)=\"closed.emit()\"\n #trigger=\"ux-menu-trigger\"\n >\n {{ selected?.group }}\n @if (selected !== initial) {\n <span class=\"filter-header\"> ({{ selected?.name }}) </span>\n }\n <ux-icon name=\"down\"></ux-icon>\n </button>\n\n <ux-menu #menu menuClass=\"ux-dynamic-filter-menu\" (closed)=\"onClose()\">\n <!-- Initial Option -->\n @if (showTypeahead) {\n <button\n uxMenuItem\n [id]=\"initial.id || filterId + '-initial-option'\"\n (click)=\"removeFilter()\"\n (keydown.enter)=\"removeFilter()\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"initial === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">\n {{ initial.name }}\n </span>\n </button>\n }\n\n <!-- Selected Options -->\n @if (selected !== initial && showTypeahead) {\n <button uxMenuItem [id]=\"selected.id || filterId + '-selection'\">\n <ux-icon name=\"checkmark\"></ux-icon>\n <span class=\"filter-dropdown-title\">{{ selected.name }}</span>\n </button>\n }\n\n @if (showTypeahead) {\n <ux-menu-divider></ux-menu-divider>\n }\n\n @if (showTypeahead) {\n <div class=\"typeahead-box\" role=\"none\">\n <input\n type=\"text\"\n class=\"form-control\"\n [placeholder]=\"options?.placeholder\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n [attr.aria-controls]=\"typeaheadId\"\n aria-autocomplete=\"list\"\n aria-multiline=\"false\"\n [ngModel]=\"query$ | async\"\n (ngModelChange)=\"query$.next($event); updateTypeahead($event)\"\n (keydown)=\"typeaheadKeyService.handleKey($event, typeahead); $event.stopPropagation()\"\n (keydown.enter)=\"$event.preventDefault()\"\n (blur)=\"typeaheadOpen = false\"\n (click)=\"$event.stopPropagation()\"\n />\n <ux-typeahead\n #typeahead\n [id]=\"typeaheadId\"\n [(open)]=\"typeaheadOpen\"\n display=\"title\"\n [selectOnEnter]=\"true\"\n [options]=\"typeaheadItems\"\n [optionTemplate]=\"filterOptionTemplate\"\n (optionSelected)=\"select($event); trigger.closeMenu($event.origin)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n >\n </ux-typeahead>\n </div>\n }\n\n @if (!showTypeahead) {\n @for (filter of filters; track filter; let index = $index) {\n <button\n type=\"button\"\n uxMenuItem\n [id]=\"filter.id || filterId + '-item-' + index\"\n (click)=\"selectFilter(filter); trigger.closeMenu('mouse')\"\n (keydown.enter)=\"selectFilter(filter); trigger.closeMenu('keyboard')\"\n >\n <ux-icon name=\"checkmark\" [style.visibility]=\"filter === selected ? 'visible' : 'hidden'\">\n </ux-icon>\n <span class=\"filter-dropdown-title\">{{ filter.name }}</span>\n </button>\n }\n }\n </ux-menu>\n</div>\n\n<ng-template #filterOptionTemplate let-option=\"option\" let-api=\"api\">\n <span\n [attr.aria-label]=\"option\"\n [uxSafeInnerHtml]=\"option | filterTypeaheadHighlight: (query$ | async)\"\n ></span>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { id: [{
type: Input
}], filters: [{
type: Input
}], initial: [{
type: Input
}], closeOnBlur: [{
type: Input
}], options: [{
type: Input
}], closed: [{
type: Output
}] } });
const DECLARATIONS$7 = [
FilterContainerComponent,
FilterDropdownComponent,
FilterDynamicComponent,
FilterTypeaheadHighlight,
];
class FilterModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FilterModule, declarations: [FilterContainerComponent,
FilterDropdownComponent,
FilterDynamicComponent,
FilterTypeaheadHighlight], imports: [A11yModule,
AccessibilityModule,
CommonModule,
FormsModule,
IconModule,
MenuModule,
TooltipModule,
TypeaheadModule,
SafeInnerHtmlDirective], exports: [FilterContainerComponent,
FilterDropdownComponent,
FilterDynamicComponent,
FilterTypeaheadHighlight] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterModule, imports: [A11yModule,
AccessibilityModule,
CommonModule,
FormsModule,
IconModule,
MenuModule,
TooltipModule,
TypeaheadModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FilterModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
CommonModule,
FormsModule,
IconModule,
MenuModule,
TooltipModule,
TypeaheadModule,
SafeInnerHtmlDirective,
],
exports: DECLARATIONS$7,
declarations: DECLARATIONS$7,
}]
}] });
class FlippableCardComponent {
constructor() {
this.focusIndicatorService = inject(FocusIndicatorService);
this.elementRef = inject(ElementRef);
/** Determines whether the card should flip horizontally or vertically. */
this.direction = 'horizontal';
/**
* Determines when the card should flip. Possible options are `click`, `hover` and `manual`.
* The manual option should be used if you want complete control over when the card should flip.
*/
this.trigger = 'hover';
/** Sets the width (in pixels) of the card. */
this.width = 280;
/** Sets the height (in pixels) of the card. */
this.height = 200;
/** Determines whether or not the card is flipped. */
this.flipped = false;
/** If two way binding is used this value will be updated when the state of the card changes. */
this.flippedChange = new EventEmitter();
this._focusIndicator = this.focusIndicatorService.monitor(this.elementRef.nativeElement);
}
ngOnDestroy() {
this._focusIndicator.destroy();
}
setFlipped(state) {
this.flipped = state;
this.flippedChange.emit(this.flipped);
}
toggleFlipped() {
this.setFlipped(!this.flipped);
}
clickTrigger() {
// add or remove the class depending on whether or not the card has been flipped
if (this.trigger === 'click') {
this.toggleFlipped();
}
}
hoverEnter() {
// if the trigger is hover then begin to flip
if (this.trigger === 'hover') {
this.setFlipped(true);
}
}
hoverExit() {
if (this.trigger === 'hover') {
this.setFlipped(false);
}
}
onKeyDown(event) {
if (this.trigger !== 'manual') {
this.toggleFlipped();
event.preventDefault();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: FlippableCardComponent, isStandalone: false, selector: "ux-flippable-card", inputs: { direction: "direction", trigger: "trigger", width: "width", height: "height", flipped: "flipped" }, outputs: { flippedChange: "flippedChange" }, host: { attributes: { "tabindex": "0" }, listeners: { "click": "clickTrigger()", "mouseenter": "hoverEnter()", "mouseleave": "hoverExit()", "keydown.enter": "onKeyDown($event)", "keydown.space": "onKeyDown($event)", "keydown.spacebar": "onKeyDown($event)" }, properties: { "class.horizontal": "direction === \"horizontal\"", "class.vertical": "direction === \"vertical\"" } }, exportAs: ["ux-flippable-card"], ngImport: i0, template: "<div\n class=\"ux-flipper\"\n [class.ux-flip-card]=\"flipped\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n>\n <div\n class=\"ux-flippable-card-front\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n [attr.aria-hidden]=\"flipped\"\n >\n <ng-content select=\"ux-flippable-card-front\"></ng-content>\n </div>\n\n <div\n class=\"ux-flippable-card-back\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n [attr.aria-hidden]=\"!flipped\"\n >\n <ng-content select=\"ux-flippable-card-back\"></ng-content>\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-flippable-card', host: {
tabindex: '0',
'[class.horizontal]': 'direction === "horizontal"',
'[class.vertical]': 'direction === "vertical"',
}, exportAs: 'ux-flippable-card', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n class=\"ux-flipper\"\n [class.ux-flip-card]=\"flipped\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n>\n <div\n class=\"ux-flippable-card-front\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n [attr.aria-hidden]=\"flipped\"\n >\n <ng-content select=\"ux-flippable-card-front\"></ng-content>\n </div>\n\n <div\n class=\"ux-flippable-card-back\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n [attr.aria-hidden]=\"!flipped\"\n >\n <ng-content select=\"ux-flippable-card-back\"></ng-content>\n </div>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { direction: [{
type: Input
}], trigger: [{
type: Input
}], width: [{
type: Input
}], height: [{
type: Input
}], flipped: [{
type: Input
}], flippedChange: [{
type: Output
}], clickTrigger: [{
type: HostListener,
args: ['click']
}], hoverEnter: [{
type: HostListener,
args: ['mouseenter']
}], hoverExit: [{
type: HostListener,
args: ['mouseleave']
}], onKeyDown: [{
type: HostListener,
args: ['keydown.enter', ['$event']]
}, {
type: HostListener,
args: ['keydown.space', ['$event']]
}, {
type: HostListener,
args: ['keydown.spacebar', ['$event']]
}] } });
class FlippableCardFrontDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardFrontDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FlippableCardFrontDirective, isStandalone: false, selector: "ux-flippable-card-front", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardFrontDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-flippable-card-front',
standalone: false,
}]
}] });
class FlippableCardBackDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardBackDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FlippableCardBackDirective, isStandalone: false, selector: "ux-flippable-card-back", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardBackDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-flippable-card-back',
standalone: false,
}]
}] });
class FlippableCardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardModule, declarations: [FlippableCardComponent, FlippableCardBackDirective, FlippableCardFrontDirective], imports: [AccessibilityModule], exports: [FlippableCardComponent, FlippableCardBackDirective, FlippableCardFrontDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardModule, imports: [AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FlippableCardModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule],
exports: [FlippableCardComponent, FlippableCardBackDirective, FlippableCardFrontDirective],
declarations: [FlippableCardComponent, FlippableCardBackDirective, FlippableCardFrontDirective],
}]
}] });
class FloatingActionButtonsService {
constructor() {
this.open$ = new BehaviorSubject(false);
this.direction$ = new BehaviorSubject('top');
}
open() {
this.open$.next(true);
}
toggle() {
this.open$.next(!this.open$.getValue());
}
close() {
this.open$.next(false);
// make the first button tabbable again
this.setPrimaryButtonFocusable();
}
isHorizontal() {
return this.direction$.value === 'left' || this.direction$.value === 'right';
}
isVertical() {
return this.direction$.value === 'top' || this.direction$.value === 'bottom';
}
setButtons(buttons) {
this._buttons = buttons;
// make the first button tabbable (after a delay to prevent expression changed error)
requestAnimationFrame(() => this.setPrimaryButtonFocusable());
}
/** Make only the first button tabbable */
setPrimaryButtonFocusable() {
this._buttons.forEach(btn => btn.tabindex$.next(btn.primary ? 0 : -1));
}
focusPrimaryButton() {
this.focus(this._buttons.find(btn => btn.primary));
}
focus(button) {
// if the button is not defined then do nothing
if (!button) {
return;
}
// set the button tab index
this._buttons.forEach(btn => btn.tabindex$.next(button === btn ? 0 : -1));
// apply the focus
button.focus();
}
focusSibling(next) {
// if the buttons are not visible then do nothing
if (this.open$.value === false) {
return;
}
// get the current focused item
const button = this.getFocusedButton();
if (next && button === this._buttons.last) {
return this.focus(this._buttons.first);
}
else if (!next && button === this._buttons.first) {
return this.focus(this._buttons.last);
}
// find the sibling button
const sibling = this._buttons.toArray()[this.getButtonIndex(button) + (next ? 1 : -1)];
// focus the next button
this.focus(sibling);
}
getFocusedButton() {
return this._buttons.find(btn => btn.tabindex$.value === 0);
}
getButtonIndex(button) {
return this._buttons.toArray().findIndex(btn => btn === button);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsService, decorators: [{
type: Injectable
}] });
class FloatingActionButtonComponent {
constructor(primary) {
this.fab = inject(FloatingActionButtonsService);
this._tooltip = inject(TooltipDirective, { optional: true });
/** Determine if this is the primary button in the set */
this.primary = false;
/** Store the tabindex */
this.tabindex$ = new BehaviorSubject(-1);
/** Unsubscribe from all observables on component destroy */
this._onDestroy = new Subject();
this.primary = primary !== null;
}
ngAfterViewInit() {
if (this._tooltip) {
// ensure the tooltip gets hidden when the button is hidden
this.fab.open$
.pipe(takeUntil(this._onDestroy), filter(isOpen => !isOpen && !this.primary))
.subscribe(() => this._tooltip.hide());
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
focus() {
this.button.nativeElement.focus();
}
onFocus() {
// ensure the tooltip gets shown
if (this._tooltip) {
this._tooltip.show();
}
}
onBlur() {
// ensure the tooltip gets hidden
if (this._tooltip) {
this._tooltip.hide();
}
}
close() {
this.fab.close();
}
onKeydown(event) {
switch (event.which) {
case UP_ARROW:
if (this.fab.isVertical()) {
this.fab.focusSibling(this.fab.direction$.value !== 'bottom');
event.preventDefault();
}
break;
case DOWN_ARROW:
if (this.fab.isVertical()) {
this.fab.focusSibling(this.fab.direction$.value === 'bottom');
event.preventDefault();
}
break;
case LEFT_ARROW:
if (this.fab.isHorizontal()) {
this.fab.focusSibling(this.fab.direction$.value !== 'right');
event.preventDefault();
}
break;
case RIGHT_ARROW:
if (this.fab.isHorizontal()) {
this.fab.focusSibling(this.fab.direction$.value === 'right');
event.preventDefault();
}
break;
case ENTER:
this.fab.focusPrimaryButton();
break;
case ESCAPE:
this.fab.focusPrimaryButton();
this.fab.close();
break;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonComponent, deps: [{ token: 'fab-primary', attribute: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: FloatingActionButtonComponent, isStandalone: false, selector: "ux-floating-action-button", inputs: { ariaLabel: ["aria-label", "ariaLabel"] }, host: { listeners: { "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "button", first: true, predicate: ["button"], descendants: true, static: true }], ngImport: i0, template: "<button\n #button\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n type=\"button\"\n class=\"btn floating-action-button\"\n [class.button-primary]=\"primary\"\n [class.button-secondary]=\"!primary\"\n [attr.aria-label]=\"ariaLabel\"\n [tabIndex]=\"tabindex$ | async\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (click)=\"primary ? fab.toggle() : close()\"\n>\n <ng-content></ng-content>\n</button>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-floating-action-button', changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, standalone: false, template: "<button\n #button\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n type=\"button\"\n class=\"btn floating-action-button\"\n [class.button-primary]=\"primary\"\n [class.button-secondary]=\"!primary\"\n [attr.aria-label]=\"ariaLabel\"\n [tabIndex]=\"tabindex$ | async\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (click)=\"primary ? fab.toggle() : close()\"\n>\n <ng-content></ng-content>\n</button>\n" }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Attribute,
args: ['fab-primary']
}] }], propDecorators: { ariaLabel: [{
type: Input,
args: ['aria-label']
}], button: [{
type: ViewChild,
args: ['button', { static: true }]
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class FloatingActionButtonsComponent {
/** Specify the direction that the FAB should display */
set direction(direction) {
this.fab.direction$.next(direction);
}
constructor() {
this.fab = inject(FloatingActionButtonsService);
this._elementRef = inject(ElementRef);
/** Emit whenever the open state changes */
this.openChange = new EventEmitter();
this._subscription = new Subscription();
this._subscription.add(this.fab.open$.subscribe(value => this.openChange.emit(value)));
}
ngAfterViewInit() {
this.fab.setButtons(this.buttons);
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
/*
* Detect any clicks to trigger close of the menu
*/
close(target) {
if (!this._elementRef.nativeElement.contains(target)) {
this.fab.close();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: FloatingActionButtonsComponent, isStandalone: false, selector: "ux-floating-action-buttons", inputs: { direction: "direction" }, outputs: { openChange: "openChange" }, host: { listeners: { "document:click": "close($event.target)" } }, providers: [FloatingActionButtonsService], queries: [{ propertyName: "buttons", predicate: FloatingActionButtonComponent }], ngImport: i0, template: "<ng-content select=\"[fab-primary]\"></ng-content>\n\n@if (fab.open$ | async) {\n <div\n class=\"floating-action-button-list\"\n [@fabAnimation]=\"fab.open$ | async\"\n [ngClass]=\"fab.direction$ | async\"\n >\n <ng-content></ng-content>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], animations: [
trigger('fabAnimation', [
transition('void => true', [
query('ux-floating-action-button', style({ opacity: 0 })),
query('ux-floating-action-button', stagger(50, animate(250, style({ opacity: 1 })))),
]),
transition('true => void', [
query('ux-floating-action-button', stagger(-50, animate(250, style({ opacity: 0 })))),
]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-floating-action-buttons', providers: [FloatingActionButtonsService], changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, animations: [
trigger('fabAnimation', [
transition('void => true', [
query('ux-floating-action-button', style({ opacity: 0 })),
query('ux-floating-action-button', stagger(50, animate(250, style({ opacity: 1 })))),
]),
transition('true => void', [
query('ux-floating-action-button', stagger(-50, animate(250, style({ opacity: 0 })))),
]),
]),
], standalone: false, template: "<ng-content select=\"[fab-primary]\"></ng-content>\n\n@if (fab.open$ | async) {\n <div\n class=\"floating-action-button-list\"\n [@fabAnimation]=\"fab.open$ | async\"\n [ngClass]=\"fab.direction$ | async\"\n >\n <ng-content></ng-content>\n </div>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { direction: [{
type: Input
}], openChange: [{
type: Output
}], buttons: [{
type: ContentChildren,
args: [FloatingActionButtonComponent]
}], close: [{
type: HostListener,
args: ['document:click', ['$event.target']]
}] } });
class FloatingActionButtonsModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsModule, declarations: [FloatingActionButtonsComponent, FloatingActionButtonComponent], imports: [AccessibilityModule, CommonModule, IconModule], exports: [FloatingActionButtonsComponent, FloatingActionButtonComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsModule, imports: [AccessibilityModule, CommonModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatingActionButtonsModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, IconModule],
exports: [FloatingActionButtonsComponent, FloatingActionButtonComponent],
declarations: [FloatingActionButtonsComponent, FloatingActionButtonComponent],
}]
}] });
class HierarchyBarNodeIconDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarNodeIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: HierarchyBarNodeIconDirective, isStandalone: false, selector: "[uxHierarchyBarNodeIcon]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarNodeIconDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxHierarchyBarNodeIcon]',
standalone: false,
}]
}] });
class HierarchyBarService {
constructor() {
/** Define the list of selected nodes */
this.nodes$ = new BehaviorSubject([]);
/** Define the events that show the popover when interacting with the arrows */
this.popoverShowTriggers = ['click'];
/** Define the events that hide the popover when interacting with the arrows */
this.popoverHideTriggers = ['click', 'clickoutside', 'escape'];
/** Emit the selected node when it changes */
this.selection$ = new Subject();
/** Define the aria label for the show siblings popover button */
this.showSiblingsAriaLabel = 'Show Siblings';
/** Store nodes as a flattened list */
this._nodes = [];
}
/**
* Store the root node of the hierarchy tree
*/
setRootNode(root) {
// if the node is null or undefined then do nothing
if (!root) {
return;
}
// store the root node
this._root = root;
// create a flat structure of nodes
this._nodes = this.getNodeList(root);
// flatten the array - based on the selected node
this.nodes$.next(this.getSelectedChildren(root));
}
/**
* Select a node. This causes all nodes to be
* deselected and the path to the selected node
* to be selected
*/
selectNode(node) {
// deselect all nodes
this.deselectAll();
// if the node is undefined then do nothing
if (!node) {
return;
}
// ensure the current node is selected and its parents
this.select(node);
// emit a new node list to trigger change detection
this.nodes$.next(this.getSelectedChildren(this._root));
// emit the new selection
this.selection$.next(node);
}
/**
* Handles getting children with support for both arrays and observables
*/
getChildren(node) {
if (Array.isArray(node.children)) {
return of({ loading: false, children: node.children });
}
const children$ = node.children;
// if it is an observable then handle loading
return Observable.create((observer) => {
// emit initial value
observer.next({ loading: true, children: [] });
// now wait until the children observable completes
children$.pipe(first()).subscribe(children => {
// replace the observable with an array for future loading
node.children = children;
// rebuild the node tree
this.setRootNode(this._root);
// emit the latest value
observer.next({ loading: false, children });
// close the observable stream
observer.complete();
});
});
}
/**
* Utility function to get the sibling nodes, taking into account that
* a node may be a root node and may not have a parent.
*/
getSiblings(node) {
return node.parent ? this.getChildren(node.parent) : of({ loading: false, children: [] });
}
/**
* Traverses all the parents to ensure they are selected
*/
select(node) {
node.selected = true;
if (node.parent) {
this.select(node.parent);
}
}
/**
* Deselects all nodes
*/
deselectAll() {
this._nodes.forEach(node => (node.selected = false));
}
/**
* Gets all the nodes in the tree as a flat array.
* It also stores the parent node in a parent property
* on the node for easy traversal in both directions
*/
getNodeList(node) {
// if there are no children then return only itself
if (!node.children || isObservable(node.children) || node.children.length === 0) {
return [node];
}
// store the parent property
node.children.forEach(child => (child.parent = node));
// get all descendants of this node
const descendants = node.children.reduce((nodes, current) => [...nodes, ...this.getNodeList(current)], []);
return [node, ...descendants];
}
/**
* Gets all selected nodes from the parent node.
*/
getSelectedChildren(node) {
if (isObservable(node.children)) {
return [node];
}
// get the children - and account for when there is none
const children = node.children || [];
// check if any child is selected
const child = children.find(_child => _child.selected);
// return the remaining chain of selected items
return child ? [node, ...this.getSelectedChildren(child)] : [node];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarService, decorators: [{
type: Injectable
}] });
class ClickOutsideDirective {
constructor() {
this._elementRef = inject(ElementRef);
this.uxClickOutside = new EventEmitter();
/** Often a click event makes the element appear - if so we can end up closing it immediately */
this._initialised = false;
setTimeout(() => (this._initialised = true));
}
click(event) {
if (this._initialised &&
this._elementRef.nativeElement !== event.target &&
!this._elementRef.nativeElement.contains(event.target)) {
this.uxClickOutside.emit(event);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ClickOutsideDirective, isStandalone: false, selector: "[uxClickOutside]", outputs: { uxClickOutside: "uxClickOutside" }, host: { listeners: { "document:click": "click($event)" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxClickOutside]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxClickOutside: [{
type: Output
}], click: [{
type: HostListener,
args: ['document:click', ['$event']]
}] } });
let uniquePopoverId = 0;
class PopoverComponent extends TooltipComponent {
constructor() {
super();
/** Define a unique id for each popover */
this.id = `ux-popover-${++uniquePopoverId}`;
/** This will emit an event any time the user clicks outside the popover */
this.clickOutside$ = new Subject();
}
/** This will update the title of the popover and trigger change detection */
setTitle(title) {
this.title = title;
this._changeDetectorRef.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PopoverComponent, isStandalone: false, selector: "ux-popover", usesInheritance: true, ngImport: i0, template: "<div\n class=\"popover show\"\n [ngClass]=\"[positionClass, 'popover-align-' + alignment, customClass]\"\n [id]=\"id\"\n [attr.role]=\"role\"\n (uxClickOutside)=\"clickOutside$.next($event)\"\n>\n <div class=\"arrow\"></div>\n @if (title) {\n <h3 class=\"popover-title\">{{ title }}</h3>\n }\n <div class=\"popover-content\" (uxResize)=\"reposition()\">\n @if (!isTemplateRef) {\n {{ content }}\n }\n @if (isTemplateRef) {\n <ng-container\n [ngTemplateOutlet]=\"$any(content)\"\n [ngTemplateOutletContext]=\"context\"\n ></ng-container>\n }\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[uxClickOutside]", outputs: ["uxClickOutside"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-popover', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n class=\"popover show\"\n [ngClass]=\"[positionClass, 'popover-align-' + alignment, customClass]\"\n [id]=\"id\"\n [attr.role]=\"role\"\n (uxClickOutside)=\"clickOutside$.next($event)\"\n>\n <div class=\"arrow\"></div>\n @if (title) {\n <h3 class=\"popover-title\">{{ title }}</h3>\n }\n <div class=\"popover-content\" (uxResize)=\"reposition()\">\n @if (!isTemplateRef) {\n {{ content }}\n }\n @if (isTemplateRef) {\n <ng-container\n [ngTemplateOutlet]=\"$any(content)\"\n [ngTemplateOutletContext]=\"context\"\n ></ng-container>\n }\n </div>\n</div>\n" }]
}], ctorParameters: () => [] });
class PopoverDirective extends TooltipDirective {
constructor() {
super();
/** All the user to add a custom class to the popover */
this.customClass = '';
/** All the user to add a role to the popover - default is tooltip */
this.role = 'tooltip';
/** Provide the TemplateRef a context object */
this.context = {};
/** Delay the showing of the popover by a number of miliseconds */
this.delay = 0;
/** Specify which events should show the popover */
this.showTriggers = ['click'];
/** Specify which events should hide the popover */
this.hideTriggers = ['click', 'clickoutside', 'escape'];
/** Keep track of the tooltip visibility and update aria-expanded attribute */
this.isVisible = false;
/** Define the overlay class */
this._overlayClass = 'ux-overlay-pane';
/** Internally store the type of this component - usual for distinctions when extending the tooltip class */
this._type = 'popover';
}
/** Set up the triggers and bind to the show/hide events to keep visibility in sync */
ngOnInit() {
// set up the event triggers
fromEvent(document, 'keydown')
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onKeyDown.bind(this));
// check if there is an aria-described by attribute
this._ariaDescribedBy = this._elementRef.nativeElement.hasAttribute('aria-describedby');
// set up the default event triggers
super.ngOnInit();
}
/**
* We need to send input changes to the popover component
* We can't use setters as they may trigger before popover initialised and can't resend once initialised
**/
ngOnChanges(changes) {
super.ngOnChanges(changes);
if (this._instance && changes.title) {
this._instance.setTitle(changes.title.currentValue);
}
}
createInstance(overlayRef) {
const instance = overlayRef.attach(this._portal).instance;
// supply the tooltip with the correct properties
instance.setTitle(this.title);
instance.setContent(this.content);
instance.setPlacement(this.placement);
instance.setAlignment(this.alignment);
instance.setClass(this.customClass);
instance.setContext(this.context);
instance.setRole(this.role);
// Update the aria-describedby attribute
this.setAriaDescribedBy(instance.id);
// subscribe to the outside click event
instance.clickOutside$
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onClickOutside.bind(this));
return instance;
}
createPortal() {
return this._portal || new ComponentPortal(PopoverComponent, this._viewContainerRef);
}
onKeyDown(event) {
// if visible and the escape key is pressed and it is one of the hide triggers
if (this.isVisible && event.keyCode === ESCAPE && this.includes(this.hideTriggers, 'escape')) {
this.hide();
}
}
onClickOutside() {
// if visible and it is one of the hide triggers
if (this.isVisible && this.includes(this.hideTriggers, 'clickoutside')) {
this.hide();
}
}
/** Programmatically update the aria-describedby property */
setAriaDescribedBy(id) {
// we only want to set the aria-describedby attr when the content is a string and there was no user defined attribute already
if (this._ariaDescribedBy === false && typeof this.content === 'string') {
super.setAriaDescribedBy(id);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: PopoverDirective, isStandalone: false, selector: "[uxPopover]", inputs: { content: ["uxPopover", "content"], title: ["popoverTitle", "title"], disabled: ["popoverDisabled", "disabled"], customClass: ["popoverClass", "customClass"], role: ["popoverRole", "role"], context: ["popoverContext", "context"], delay: ["popoverDelay", "delay"], showTriggers: "showTriggers", hideTriggers: "hideTriggers" }, host: { properties: { "attr.aria-expanded": "this.isVisible" } }, exportAs: ["ux-popover"], usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxPopover]',
exportAs: 'ux-popover',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { content: [{
type: Input,
args: ['uxPopover']
}], title: [{
type: Input,
args: ['popoverTitle']
}], disabled: [{
type: Input,
args: ['popoverDisabled']
}], customClass: [{
type: Input,
args: ['popoverClass']
}], role: [{
type: Input,
args: ['popoverRole']
}], context: [{
type: Input,
args: ['popoverContext']
}], delay: [{
type: Input,
args: ['popoverDelay']
}], showTriggers: [{
type: Input
}], hideTriggers: [{
type: Input
}], isVisible: [{
type: HostBinding,
args: ['attr.aria-expanded']
}] } });
class HierarchyBarNodeComponent {
constructor() {
this.hierarchyBar = inject(HierarchyBarService);
this._elementRef = inject(ElementRef);
/** Optionally define the horizontal offset */
this.offset = 0;
/** Emit when the node is selected */
this.selected = new EventEmitter();
/** Determine if this node should be hidden due to overflow */
this.visible = true;
}
/** Get the width of the element */
get width() {
return this._elementRef.nativeElement.offsetWidth;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarNodeComponent, isStandalone: false, selector: "ux-hierarchy-bar-node", inputs: { node: "node", popoverTemplate: "popoverTemplate", mode: "mode", readonly: "readonly", offset: "offset" }, outputs: { selected: "selected" }, host: { properties: { "style.visibility": "visible ? \"visible\" : \"hidden\"" } }, ngImport: i0, template: "<div\n class=\"hierarchy-bar-node\"\n [class.hierarchy-bar-node-readonly]=\"readonly\"\n [class.hierarchy-bar-node-child-indicator]=\"node.children\"\n [style.transform]=\"offset ? 'translateX(' + offset + 'px)' : null\"\n>\n @if (mode === 'dropdown') {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n #popover=\"ux-popover\"\n aria-label=\"Show children\"\n class=\"hierarchy-bar-node-content\"\n [disabled]=\"readonly\"\n [uxPopover]=\"popoverTemplate\"\n [popoverContext]=\"{ node: node, popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n [showTriggers]=\"node.children ? hierarchyBar.popoverShowTriggers : []\"\n [hideTriggers]=\"node.children ? hierarchyBar.popoverHideTriggers : []\"\n tabindex=\"0\"\n [attr.aria-label]=\"node.title\"\n >\n <!-- Show a custom icon if specified -->\n @if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n ></ng-container>\n </div>\n }\n <!-- Show an icon if specified -->\n @if (node.icon && !hierarchyBar.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n }\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-node-title\">{{ node.title }}</span>\n <!-- Show a dropdown arrow if there are children -->\n <div [class.readonly-arrow]=\"readonly\" class=\"hierarchy-bar-node-arrow-icon-dropdown\">\n @if (node.children) {\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"> </ux-icon>\n }\n </div>\n </button>\n } @else {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"hierarchy-bar-node-content\"\n [disabled]=\"readonly\"\n [attr.aria-label]=\"node.title\"\n (click)=\"selected.emit(node)\"\n >\n <!-- Show a custom icon if specified -->\n @if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n ></ng-container>\n </div>\n }\n <!-- Show an icon if specified -->\n @if (node.icon && !hierarchyBar.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n }\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-node-title\">{{ node.title }}</span>\n </button>\n <!-- Show a dropdown arrow if there are children -->\n @if (node.children) {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n #popover=\"ux-popover\"\n aria-label=\"Show children\"\n role=\"button\"\n class=\"hierarchy-bar-node-arrow\"\n [disabled]=\"readonly\"\n [uxPopover]=\"popoverTemplate\"\n [popoverContext]=\"{ node: node, popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n tabindex=\"0\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"> </ux-icon>\n </button>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: PopoverDirective, selector: "[uxPopover]", inputs: ["uxPopover", "popoverTitle", "popoverDisabled", "popoverClass", "popoverRole", "popoverContext", "popoverDelay", "showTriggers", "hideTriggers"], exportAs: ["ux-popover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarNodeComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar-node', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[style.visibility]': 'visible ? "visible" : "hidden"',
}, standalone: false, template: "<div\n class=\"hierarchy-bar-node\"\n [class.hierarchy-bar-node-readonly]=\"readonly\"\n [class.hierarchy-bar-node-child-indicator]=\"node.children\"\n [style.transform]=\"offset ? 'translateX(' + offset + 'px)' : null\"\n>\n @if (mode === 'dropdown') {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n #popover=\"ux-popover\"\n aria-label=\"Show children\"\n class=\"hierarchy-bar-node-content\"\n [disabled]=\"readonly\"\n [uxPopover]=\"popoverTemplate\"\n [popoverContext]=\"{ node: node, popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n [showTriggers]=\"node.children ? hierarchyBar.popoverShowTriggers : []\"\n [hideTriggers]=\"node.children ? hierarchyBar.popoverHideTriggers : []\"\n tabindex=\"0\"\n [attr.aria-label]=\"node.title\"\n >\n <!-- Show a custom icon if specified -->\n @if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n ></ng-container>\n </div>\n }\n <!-- Show an icon if specified -->\n @if (node.icon && !hierarchyBar.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n }\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-node-title\">{{ node.title }}</span>\n <!-- Show a dropdown arrow if there are children -->\n <div [class.readonly-arrow]=\"readonly\" class=\"hierarchy-bar-node-arrow-icon-dropdown\">\n @if (node.children) {\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"> </ux-icon>\n }\n </div>\n </button>\n } @else {\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"hierarchy-bar-node-content\"\n [disabled]=\"readonly\"\n [attr.aria-label]=\"node.title\"\n (click)=\"selected.emit(node)\"\n >\n <!-- Show a custom icon if specified -->\n @if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n ></ng-container>\n </div>\n }\n <!-- Show an icon if specified -->\n @if (node.icon && !hierarchyBar.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n }\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-node-title\">{{ node.title }}</span>\n </button>\n <!-- Show a dropdown arrow if there are children -->\n @if (node.children) {\n <button\n type=\"button\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n #popover=\"ux-popover\"\n aria-label=\"Show children\"\n role=\"button\"\n class=\"hierarchy-bar-node-arrow\"\n [disabled]=\"readonly\"\n [uxPopover]=\"popoverTemplate\"\n [popoverContext]=\"{ node: node, popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n tabindex=\"0\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"> </ux-icon>\n </button>\n }\n }\n</div>\n" }]
}], propDecorators: { node: [{
type: Input
}], popoverTemplate: [{
type: Input
}], mode: [{
type: Input
}], readonly: [{
type: Input
}], offset: [{
type: Input
}], selected: [{
type: Output
}] } });
class HierarchyBarPopoverItemComponent {
constructor() {
this.focusOriginService = inject(FocusIndicatorOriginService);
this.elementRef = inject(ElementRef);
this.renderer = inject(Renderer2);
this.hierarchyBar = inject(HierarchyBarService);
/**
* Emit when a click or enter key press occurs.
* Note this is an `async` EventEmitter to ensure that
* the event handlers in the `FocusIndicatorOrigin` set
* the origin before we emit the select event, otherwise
* the item may not get a focus ring when the keyboard is used.
*/
this.selected = new EventEmitter(true);
this._focusOrigin = new FocusIndicatorOrigin(this.focusOriginService, this.elementRef, this.renderer);
}
ngOnDestroy() {
this._focusOrigin.destroy();
}
onSelect() {
this.selected.emit(this.node);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarPopoverItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarPopoverItemComponent, isStandalone: false, selector: "ux-hierarchy-bar-popover-item", inputs: { node: "node" }, outputs: { selected: "selected" }, host: { listeners: { "click": "onSelect()", "keydown.enter": "onSelect()" } }, ngImport: i0, template: "<!-- Show an icon if specified -->\n@if (!hierarchyBar.icon && node?.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n}\n\n<!-- Show a custom icon if specified -->\n@if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n >\n </ng-container>\n </div>\n}\n\n<!-- Show the name of the current node -->\n<span class=\"hierarchy-bar-node-title\">{{ node?.title }}</span>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarPopoverItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar-popover-item', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Show an icon if specified -->\n@if (!hierarchyBar.icon && node?.icon) {\n <img class=\"hierarchy-bar-node-icon\" [src]=\"node.icon\" alt=\"Hierarchy Bar Icon\" />\n}\n\n<!-- Show a custom icon if specified -->\n@if (hierarchyBar.icon) {\n <div class=\"hierarchy-bar-node-icon\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.icon\"\n [ngTemplateOutletContext]=\"{ node: node, $implicit: node }\"\n >\n </ng-container>\n </div>\n}\n\n<!-- Show the name of the current node -->\n<span class=\"hierarchy-bar-node-title\">{{ node?.title }}</span>\n" }]
}], ctorParameters: () => [], propDecorators: { node: [{
type: Input
}], selected: [{
type: Output
}], onSelect: [{
type: HostListener,
args: ['click']
}, {
type: HostListener,
args: ['keydown.enter']
}] } });
class HierarchyBarPopoverComponent {
constructor() {
this.hierarchyBar = inject(HierarchyBarService);
/** Define the nodes to display */
this.nodes = [];
/** Defines if dropdown items should have separators between them to distinguish if nodes are siblings or ancestors */
this.separator = false;
/** Emit a select event when an item ahs been clicked or enter key pressed */
this.selected = new EventEmitter();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarPopoverComponent, isStandalone: false, selector: "ux-hierarchy-bar-popover", inputs: { nodes: "nodes", loading: "loading", separator: "separator" }, outputs: { selected: "selected" }, ngImport: i0, template: "<!-- Loading Indicator -->\n@if (loading) {\n <ul class=\"hierarchy-bar-node-list\">\n <li class=\"hierarchy-bar-loading-indicator\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.loadingIndicator || defaultLoadingIndicator\"\n ></ng-container>\n </li>\n </ul>\n}\n\n<!-- List of children -->\n@if (!loading) {\n <div\n class=\"hierarchy-bar-node-list\"\n [class.hierarchy-bar-node-list-separator]=\"separator\"\n uxTabbableList\n [returnFocus]=\"true\"\n >\n @for (node of nodes; track node; let first = $first) {\n <ux-hierarchy-bar-popover-item\n uxFocusIndicator\n [node]=\"node\"\n [focusIf]=\"first\"\n uxTabbableListItem\n (selected)=\"selected.emit($event)\"\n >\n </ux-hierarchy-bar-popover-item>\n }\n </div>\n}\n\n<!-- Loading Indicator Template -->\n<ng-template #defaultLoadingIndicator>\n <div class=\"hierarchy-bar-loading-icon\" alt=\"Hierarchy Bar Loading Indicator\">\n <div class=\"spinner spinner-accent spinner-bounce-middle\"></div>\n </div>\n\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-loading-title\">Loading...</span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: HierarchyBarPopoverItemComponent, selector: "ux-hierarchy-bar-popover-item", inputs: ["node"], outputs: ["selected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarPopoverComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar-popover', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Loading Indicator -->\n@if (loading) {\n <ul class=\"hierarchy-bar-node-list\">\n <li class=\"hierarchy-bar-loading-indicator\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.loadingIndicator || defaultLoadingIndicator\"\n ></ng-container>\n </li>\n </ul>\n}\n\n<!-- List of children -->\n@if (!loading) {\n <div\n class=\"hierarchy-bar-node-list\"\n [class.hierarchy-bar-node-list-separator]=\"separator\"\n uxTabbableList\n [returnFocus]=\"true\"\n >\n @for (node of nodes; track node; let first = $first) {\n <ux-hierarchy-bar-popover-item\n uxFocusIndicator\n [node]=\"node\"\n [focusIf]=\"first\"\n uxTabbableListItem\n (selected)=\"selected.emit($event)\"\n >\n </ux-hierarchy-bar-popover-item>\n }\n </div>\n}\n\n<!-- Loading Indicator Template -->\n<ng-template #defaultLoadingIndicator>\n <div class=\"hierarchy-bar-loading-icon\" alt=\"Hierarchy Bar Loading Indicator\">\n <div class=\"spinner spinner-accent spinner-bounce-middle\"></div>\n </div>\n\n <!-- Show the name of the current node -->\n <span class=\"hierarchy-bar-loading-title\">Loading...</span>\n</ng-template>\n" }]
}], propDecorators: { nodes: [{
type: Input
}], loading: [{
type: Input
}], separator: [{
type: Input
}], selected: [{
type: Output
}] } });
class HierarchyBarStandardComponent {
constructor() {
this.hierarchyBar = inject(HierarchyBarService);
/** Value in pixels to translate the visible nodes by to fill the empty space occupied by hidden nodes */
this.overflowTranslateOffset = 0;
/** Identify which nodes are overflowing */
this.overflow$ = new BehaviorSubject([]);
/** Determine if there is any overflow */
this.isOverflowing$ = new BehaviorSubject(false);
/** Unsubscribe from all subscriptions when component is destroyed */
this._onDestroy = new Subject();
// subscribe to changes in the selected node - update the UI after the render
this.hierarchyBar.nodes$
.pipe(takeUntil(this._onDestroy))
.subscribe(() => requestAnimationFrame(this.scrollIntoView.bind(this)));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* When there is overflow ensure that the rightmost
* node remains in view at all times. The nodes no longer
* visible should be displayed in a popover available on the
* overflow indicator
*/
scrollIntoView() {
if (!this.nodelist) {
return;
}
// get the native element
const { nativeElement } = this.nodelist;
const isOverflowing = nativeElement.scrollWidth > nativeElement.offsetWidth;
// emit whether we are overflowing or not
this.isOverflowing$.next(isOverflowing);
// we don't need to do anything else if there is no overflow
if (!isOverflowing) {
this.nodeInstances.forEach(node => (node.visible = true));
this.overflowTranslateOffset = 0;
return;
}
let isFull = false;
// find the nodes that should be visible (we start at the rightmost node)
const nodes = this.nodeInstances
.toArray()
.reduceRight((visibleNodes, node) => {
// there must always be one visible node
if (visibleNodes.length === 0) {
return [node];
}
// if the hierarchy bar is already occupying the available space then we can skip calculations
if (isFull) {
// hide the node
node.visible = false;
return visibleNodes;
}
// get the cumulative width of all the visible nodes
const consumedWidth = visibleNodes.reduce((totalWidth, visibleNode) => totalWidth + visibleNode.width, 0);
// get the width that would be consumed if this node was included
const width = node.width + consumedWidth;
isFull = width > nativeElement.offsetWidth;
node.visible = !isFull;
if (isFull) {
this.overflowTranslateOffset = this.nodelist.nativeElement.clientWidth - consumedWidth;
}
return isFull ? visibleNodes : [...visibleNodes, node];
}, []);
if (nodes.length === 1 && this.nodelist.nativeElement.offsetWidth < nodes[0].width) {
this.overflowTranslateOffset = 0;
}
// move the scroll position to always show the last item
this.nodelist.nativeElement.scrollLeft = nativeElement.scrollWidth - nativeElement.offsetWidth;
// determine which nodes should be hidden
const nodesHidden = this.nodeInstances.filter(node => {
return !node.visible;
});
this.overflow$.next(nodesHidden.map(node => node.node));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarStandardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarStandardComponent, isStandalone: false, selector: "ux-hierarchy-bar-standard", inputs: { mode: "mode", readonly: "readonly" }, viewQueries: [{ propertyName: "nodelist", first: true, predicate: ["nodelist"], descendants: true, static: true }, { propertyName: "nodes", predicate: HierarchyBarNodeComponent, descendants: true, read: ElementRef }, { propertyName: "nodeInstances", predicate: HierarchyBarNodeComponent, descendants: true }, { propertyName: "barNodes", predicate: ["barNodes"], descendants: true, read: ElementRef }], ngImport: i0, template: "<!-- Allow content to be placed on the left of the items -->\n<div class=\"hierarchy-bar-addons\">\n @if (isOverflowing$ | async) {\n <div\n #popover=\"ux-popover\"\n class=\"hierarchy-bar-overflow-indicator\"\n [uxPopover]=\"overflow\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n [popoverContext]=\"{ popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n >\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.overflowTemplate || defaultOverflowTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: overflow$ | async }\"\n >\n </ng-container>\n <button\n [attr.aria-label]=\"hierarchyBar.showSiblingsAriaLabel\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"hierarchy-bar-node-arrow\"\n placement=\"bottom\"\n [uxPopover]=\"nodeList\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n [disabled]=\"readonly\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n type=\"button\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"></ux-icon>\n </button>\n </div>\n }\n <ng-content select=\"left-addons\"></ng-content>\n</div>\n\n<div #nodelist class=\"hierarchy-bar-nodes\" (uxResize)=\"scrollIntoView()\">\n @for (node of hierarchyBar.nodes$ | async; track node; let i = $index; let last = $last) {\n <ux-hierarchy-bar-node\n #barNodes\n [mode]=\"mode\"\n [readonly]=\"readonly\"\n [node]=\"node\"\n [popoverTemplate]=\"content\"\n (selected)=\"hierarchyBar.selectNode(node)\"\n [offset]=\"-overflowTranslateOffset\"\n [style.max-width.px]=\"(isOverflowing$ | async) && last ? nodelist.offsetWidth : null\"\n >\n </ux-hierarchy-bar-node>\n }\n\n <!-- Allow content to be placed after the last node -->\n <div class=\"hierarchy-bar-addons\">\n <ng-content select=\"trailing-addons\"></ng-content>\n </div>\n</div>\n\n<!-- Allow content to be placed on the right of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"right-addons\"></ng-content>\n</div>\n\n<!-- Template for the popover list -->\n<ng-template #content let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [loading]=\"(hierarchyBar.getChildren(node) | async)?.loading\"\n [nodes]=\"(hierarchyBar.getChildren(node) | async)?.children\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the popover list -->\n<ng-template #nodeList let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"node\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the overflow popover list -->\n<ng-template #overflow let-popover=\"popover\">\n <div uxTabbableList [returnFocus]=\"true\">\n @for (child of overflow$ | async; track child; let first = $first) {\n <ux-hierarchy-bar-popover-item\n uxFocusIndicator\n [node]=\"child\"\n [focusIf]=\"first\"\n uxTabbableListItem\n (selected)=\"hierarchyBar.selectNode(child); popover.hide()\"\n >\n </ux-hierarchy-bar-popover-item>\n }\n </div>\n</ng-template>\n\n<!-- Default Overflow Template -->\n<ng-template #defaultOverflowTemplate>\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [disabled]=\"readonly\"\n class=\"overflow-button\"\n aria-label=\"Show parents\"\n role=\"button\"\n type=\"button\"\n >\n <ux-icon name=\"more\"></ux-icon>\n </button>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: PopoverDirective, selector: "[uxPopover]", inputs: ["uxPopover", "popoverTitle", "popoverDisabled", "popoverClass", "popoverRole", "popoverContext", "popoverDelay", "showTriggers", "hideTriggers"], exportAs: ["ux-popover"] }, { kind: "directive", type: ResizeDirective, selector: "[uxResize]", inputs: ["throttle"], outputs: ["uxResize"] }, { kind: "component", type: HierarchyBarNodeComponent, selector: "ux-hierarchy-bar-node", inputs: ["node", "popoverTemplate", "mode", "readonly", "offset"], outputs: ["selected"] }, { kind: "component", type: HierarchyBarPopoverComponent, selector: "ux-hierarchy-bar-popover", inputs: ["nodes", "loading", "separator"], outputs: ["selected"] }, { kind: "component", type: HierarchyBarPopoverItemComponent, selector: "ux-hierarchy-bar-popover-item", inputs: ["node"], outputs: ["selected"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarStandardComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar-standard', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Allow content to be placed on the left of the items -->\n<div class=\"hierarchy-bar-addons\">\n @if (isOverflowing$ | async) {\n <div\n #popover=\"ux-popover\"\n class=\"hierarchy-bar-overflow-indicator\"\n [uxPopover]=\"overflow\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n [popoverContext]=\"{ popover: popover }\"\n placement=\"bottom\"\n popoverClass=\"hierarchy-bar-popover\"\n >\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.overflowTemplate || defaultOverflowTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: overflow$ | async }\"\n >\n </ng-container>\n <button\n [attr.aria-label]=\"hierarchyBar.showSiblingsAriaLabel\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"hierarchy-bar-node-arrow\"\n placement=\"bottom\"\n [uxPopover]=\"nodeList\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n [disabled]=\"readonly\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n type=\"button\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"></ux-icon>\n </button>\n </div>\n }\n <ng-content select=\"left-addons\"></ng-content>\n</div>\n\n<div #nodelist class=\"hierarchy-bar-nodes\" (uxResize)=\"scrollIntoView()\">\n @for (node of hierarchyBar.nodes$ | async; track node; let i = $index; let last = $last) {\n <ux-hierarchy-bar-node\n #barNodes\n [mode]=\"mode\"\n [readonly]=\"readonly\"\n [node]=\"node\"\n [popoverTemplate]=\"content\"\n (selected)=\"hierarchyBar.selectNode(node)\"\n [offset]=\"-overflowTranslateOffset\"\n [style.max-width.px]=\"(isOverflowing$ | async) && last ? nodelist.offsetWidth : null\"\n >\n </ux-hierarchy-bar-node>\n }\n\n <!-- Allow content to be placed after the last node -->\n <div class=\"hierarchy-bar-addons\">\n <ng-content select=\"trailing-addons\"></ng-content>\n </div>\n</div>\n\n<!-- Allow content to be placed on the right of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"right-addons\"></ng-content>\n</div>\n\n<!-- Template for the popover list -->\n<ng-template #content let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [loading]=\"(hierarchyBar.getChildren(node) | async)?.loading\"\n [nodes]=\"(hierarchyBar.getChildren(node) | async)?.children\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the popover list -->\n<ng-template #nodeList let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"node\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the overflow popover list -->\n<ng-template #overflow let-popover=\"popover\">\n <div uxTabbableList [returnFocus]=\"true\">\n @for (child of overflow$ | async; track child; let first = $first) {\n <ux-hierarchy-bar-popover-item\n uxFocusIndicator\n [node]=\"child\"\n [focusIf]=\"first\"\n uxTabbableListItem\n (selected)=\"hierarchyBar.selectNode(child); popover.hide()\"\n >\n </ux-hierarchy-bar-popover-item>\n }\n </div>\n</ng-template>\n\n<!-- Default Overflow Template -->\n<ng-template #defaultOverflowTemplate>\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [disabled]=\"readonly\"\n class=\"overflow-button\"\n aria-label=\"Show parents\"\n role=\"button\"\n type=\"button\"\n >\n <ux-icon name=\"more\"></ux-icon>\n </button>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { mode: [{
type: Input
}], readonly: [{
type: Input
}], nodelist: [{
type: ViewChild,
args: ['nodelist', { static: true }]
}], nodes: [{
type: ViewChildren,
args: [HierarchyBarNodeComponent, { read: ElementRef }]
}], nodeInstances: [{
type: ViewChildren,
args: [HierarchyBarNodeComponent]
}], barNodes: [{
type: ViewChildren,
args: ['barNodes', { read: ElementRef }]
}] } });
class HierarchyBarCollapsedComponent {
constructor() {
this.hierarchyBar = inject(HierarchyBarService);
this._renderer = inject(Renderer2);
this._resizeService = inject(ResizeService);
this._elementRef = inject(ElementRef);
this._changeDetector = inject(ChangeDetectorRef);
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
}
/** Get all the sibling nodes */
get _siblings() {
return this.hierarchyBar.getSiblings(this._last);
}
/** Get all the nodes between the first and last nodes */
get _parents() {
return this._nodes.filter(node => node !== this._first && node !== this._last);
}
/** Get the nodes as an array */
get _nodes() {
return this.hierarchyBar.nodes$.value;
}
ngAfterViewInit() {
// Update the UI when the selected nodes change
this.hierarchyBar.nodes$.pipe(takeUntil(this._onDestroy)).subscribe(this.update.bind(this));
// watch for the host element size changing
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.updateOverflow());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
// remove the resize event listener
this._resizeService.removeResizeListener(this._elementRef.nativeElement);
}
update(nodes) {
this._first = nodes[0];
this._last = nodes.length > 1 ? nodes[nodes.length - 1] : null;
this.updateOverflow();
this._changeDetector.detectChanges();
}
updateOverflow() {
// remove the class if it is present
this._renderer.removeClass(this.nodeContainer.nativeElement, 'hierarchy-bar-nodes-overflow');
// check if there is overflow
if (this.nodeContainer.nativeElement.scrollWidth > this.nodeContainer.nativeElement.offsetWidth) {
this._renderer.addClass(this.nodeContainer.nativeElement, 'hierarchy-bar-nodes-overflow');
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarCollapsedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarCollapsedComponent, isStandalone: false, selector: "ux-hierarchy-bar-collapsed", inputs: { readonly: "readonly" }, viewQueries: [{ propertyName: "nodeContainer", first: true, predicate: ["nodes"], descendants: true, static: true }], ngImport: i0, template: "<!-- Allow content to be placed on the left of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"left-addons\"></ng-content>\n</div>\n\n<div #nodes class=\"hierarchy-bar-nodes\">\n @if (_first) {\n <ux-hierarchy-bar-node\n [readonly]=\"readonly\"\n [popoverTemplate]=\"content\"\n [node]=\"_first\"\n (selected)=\"hierarchyBar.selectNode($event)\"\n >\n </ux-hierarchy-bar-node>\n }\n\n @if (_parents.length > 0) {\n <div class=\"hierarchy-bar-overflow\">\n <div class=\"hierarchy-bar-overflow-container\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.overflowTemplate || defaultOverflowTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: _parents }\"\n >\n </ng-container>\n </div>\n <button\n [attr.aria-label]=\"hierarchyBar.showSiblingsAriaLabel\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"hierarchy-bar-node-arrow\"\n placement=\"bottom\"\n [uxPopover]=\"siblingsTemplate\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n [disabled]=\"readonly\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n type=\"button\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"></ux-icon>\n </button>\n </div>\n }\n\n @if (_last) {\n <ux-hierarchy-bar-node\n [readonly]=\"readonly\"\n [popoverTemplate]=\"content\"\n [node]=\"_last\"\n (selected)=\"hierarchyBar.selectNode($event)\"\n >\n </ux-hierarchy-bar-node>\n }\n\n <!-- Allow content to be placed after the last node -->\n <div class=\"hierarchy-bar-addons\">\n <ng-content select=\"trailing-addons\"></ng-content>\n </div>\n</div>\n\n<!-- Allow content to be placed on the right of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"right-addons\"></ng-content>\n</div>\n\n<!-- Template for the popover list -->\n<ng-template #content let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [loading]=\"(hierarchyBar.getChildren(node) | async)?.loading\"\n [nodes]=\"(hierarchyBar.getChildren(node) | async)?.children\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the siblings popover list -->\n<ng-template #siblingsTemplate let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"(_siblings | async)?.children\"\n [loading]=\"(_siblings | async)?.loading\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the parents popover list -->\n<ng-template #parentsTemplate let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"_parents\"\n [separator]=\"true\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Default Overflow Template -->\n<ng-template #defaultOverflowTemplate>\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [disabled]=\"readonly\"\n class=\"overflow-button\"\n aria-label=\"Show parents\"\n [uxPopover]=\"parentsTemplate\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n placement=\"bottom\"\n type=\"button\"\n >\n <ux-icon name=\"more\"></ux-icon>\n </button>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: PopoverDirective, selector: "[uxPopover]", inputs: ["uxPopover", "popoverTitle", "popoverDisabled", "popoverClass", "popoverRole", "popoverContext", "popoverDelay", "showTriggers", "hideTriggers"], exportAs: ["ux-popover"] }, { kind: "component", type: HierarchyBarNodeComponent, selector: "ux-hierarchy-bar-node", inputs: ["node", "popoverTemplate", "mode", "readonly", "offset"], outputs: ["selected"] }, { kind: "component", type: HierarchyBarPopoverComponent, selector: "ux-hierarchy-bar-popover", inputs: ["nodes", "loading", "separator"], outputs: ["selected"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarCollapsedComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar-collapsed', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Allow content to be placed on the left of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"left-addons\"></ng-content>\n</div>\n\n<div #nodes class=\"hierarchy-bar-nodes\">\n @if (_first) {\n <ux-hierarchy-bar-node\n [readonly]=\"readonly\"\n [popoverTemplate]=\"content\"\n [node]=\"_first\"\n (selected)=\"hierarchyBar.selectNode($event)\"\n >\n </ux-hierarchy-bar-node>\n }\n\n @if (_parents.length > 0) {\n <div class=\"hierarchy-bar-overflow\">\n <div class=\"hierarchy-bar-overflow-container\">\n <ng-container\n [ngTemplateOutlet]=\"hierarchyBar.overflowTemplate || defaultOverflowTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: _parents }\"\n >\n </ng-container>\n </div>\n <button\n [attr.aria-label]=\"hierarchyBar.showSiblingsAriaLabel\"\n uxFocusIndicator\n uxFocusIndicatorOrigin\n class=\"hierarchy-bar-node-arrow\"\n placement=\"bottom\"\n [uxPopover]=\"siblingsTemplate\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n [disabled]=\"readonly\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n type=\"button\"\n >\n <ux-icon name=\"next\" class=\"hierarchy-bar-node-arrow-icon\"></ux-icon>\n </button>\n </div>\n }\n\n @if (_last) {\n <ux-hierarchy-bar-node\n [readonly]=\"readonly\"\n [popoverTemplate]=\"content\"\n [node]=\"_last\"\n (selected)=\"hierarchyBar.selectNode($event)\"\n >\n </ux-hierarchy-bar-node>\n }\n\n <!-- Allow content to be placed after the last node -->\n <div class=\"hierarchy-bar-addons\">\n <ng-content select=\"trailing-addons\"></ng-content>\n </div>\n</div>\n\n<!-- Allow content to be placed on the right of the items -->\n<div class=\"hierarchy-bar-addons\">\n <ng-content select=\"right-addons\"></ng-content>\n</div>\n\n<!-- Template for the popover list -->\n<ng-template #content let-node=\"node\" let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [loading]=\"(hierarchyBar.getChildren(node) | async)?.loading\"\n [nodes]=\"(hierarchyBar.getChildren(node) | async)?.children\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the siblings popover list -->\n<ng-template #siblingsTemplate let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"(_siblings | async)?.children\"\n [loading]=\"(_siblings | async)?.loading\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Template for the parents popover list -->\n<ng-template #parentsTemplate let-popover=\"popover\">\n <ux-hierarchy-bar-popover\n [nodes]=\"_parents\"\n [separator]=\"true\"\n (selected)=\"hierarchyBar.selectNode($event); popover.hide()\"\n >\n </ux-hierarchy-bar-popover>\n</ng-template>\n\n<!-- Default Overflow Template -->\n<ng-template #defaultOverflowTemplate>\n <button\n uxFocusIndicator\n uxFocusIndicatorOrigin\n [disabled]=\"readonly\"\n class=\"overflow-button\"\n aria-label=\"Show parents\"\n [uxPopover]=\"parentsTemplate\"\n popoverClass=\"hierarchy-bar-popover\"\n role=\"button\"\n tabindex=\"0\"\n #popover=\"ux-popover\"\n [popoverContext]=\"{ popover: popover }\"\n [showTriggers]=\"hierarchyBar.popoverShowTriggers\"\n [hideTriggers]=\"hierarchyBar.popoverHideTriggers\"\n placement=\"bottom\"\n type=\"button\"\n >\n <ux-icon name=\"more\"></ux-icon>\n </button>\n</ng-template>\n" }]
}], propDecorators: { readonly: [{
type: Input
}], nodeContainer: [{
type: ViewChild,
args: ['nodes', { static: true }]
}] } });
class HierarchyBarComponent {
/** Define the root node of the hierarchy bar */
set root(node) {
this._hierarchyBar.setRootNode(node);
}
/** Define the selected node in the hierarchy bar */
set selected(node) {
this._hierarchyBar.selectNode(node);
}
/** Provide a custom loading indicator */
set loadingIndicator(loadingIndicator) {
this._hierarchyBar.loadingIndicator = loadingIndicator;
}
/** Provide a custom overflow template */
set overflowTemplate(overflowTemplate) {
this._hierarchyBar.overflowTemplate = overflowTemplate;
}
/** Define the events that show the popover when interacting with the arrows */
set popoverShowTriggers(popoverShowTriggers) {
this._hierarchyBar.popoverShowTriggers = popoverShowTriggers;
}
/** Define the events that hide the popover when interacting with the arrows */
set popoverHideTriggers(popoverHideTriggers) {
this._hierarchyBar.popoverHideTriggers = popoverHideTriggers;
}
/** Define the aria label for the show siblings popover button */
set showSiblingsAriaLabel(label) {
this._hierarchyBar.showSiblingsAriaLabel = label;
}
/** Allow a custom icon to be specified */
set icon(icon) {
this._hierarchyBar.icon = icon;
}
constructor() {
this._hierarchyBar = inject(HierarchyBarService);
/** Define which presentational mode we should display */
this.mode = 'standard';
/** hierarchy bar as being readonly - default false */
this.readonly = false;
/** Emit when the selected node changes */
this.selectedChange = new EventEmitter();
/** Unsubscribe from all subscriptions when component is destroyed */
this._onDestroy = new Subject();
// emit the latest selection value
this._hierarchyBar.selection$
.pipe(takeUntil(this._onDestroy))
.subscribe(selection => this.selectedChange.next(selection));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: HierarchyBarComponent, isStandalone: false, selector: "ux-hierarchy-bar", inputs: { mode: "mode", readonly: "readonly", root: "root", selected: "selected", loadingIndicator: "loadingIndicator", overflowTemplate: "overflowTemplate", popoverShowTriggers: "popoverShowTriggers", popoverHideTriggers: "popoverHideTriggers", showSiblingsAriaLabel: "showSiblingsAriaLabel" }, outputs: { selectedChange: "selectedChange" }, queries: [{ propertyName: "icon", first: true, predicate: HierarchyBarNodeIconDirective, descendants: true, read: TemplateRef }], ngImport: i0, template: "<!-- Hierarchy Bar - Standard Layout -->\n@if (mode !== 'collapsed') {\n <ux-hierarchy-bar-standard [readonly]=\"readonly\" [mode]=\"mode\">\n <!-- Forward the content to the correct layout -->\n <ng-container ngProjectAs=\"left-addons\" [ngTemplateOutlet]=\"leftAddons\"></ng-container>\n <ng-container ngProjectAs=\"trailing-addons\" [ngTemplateOutlet]=\"trailingAddons\"></ng-container>\n <ng-container ngProjectAs=\"right-addons\" [ngTemplateOutlet]=\"rightAddons\"></ng-container>\n </ux-hierarchy-bar-standard>\n}\n\n<!-- Hierarchy Bar - Collapsed Layout -->\n@if (mode === 'collapsed') {\n <ux-hierarchy-bar-collapsed [readonly]=\"readonly\">\n <!-- Forward the content to the correct layout -->\n <ng-container ngProjectAs=\"left-addons\" [ngTemplateOutlet]=\"leftAddons\"></ng-container>\n <ng-container ngProjectAs=\"trailing-addons\" [ngTemplateOutlet]=\"trailingAddons\"></ng-container>\n <ng-container ngProjectAs=\"right-addons\" [ngTemplateOutlet]=\"rightAddons\"></ng-container>\n </ux-hierarchy-bar-collapsed>\n}\n\n<!-- We can only have one ng-content so this allows us to use it more than once -->\n<ng-template #leftAddons>\n <ng-content select=\"[uxHierarchyBarLeftAddon]\"></ng-content>\n</ng-template>\n\n<ng-template #trailingAddons>\n <ng-content select=\"[uxHierarchyBarTrailingAddon]\"></ng-content>\n</ng-template>\n\n<ng-template #rightAddons>\n <ng-content select=\"[uxHierarchyBarRightAddon]\"></ng-content>\n</ng-template>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: HierarchyBarStandardComponent, selector: "ux-hierarchy-bar-standard", inputs: ["mode", "readonly"] }, { kind: "component", type: HierarchyBarCollapsedComponent, selector: "ux-hierarchy-bar-collapsed", inputs: ["readonly"] }], viewProviders: [HierarchyBarService], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-hierarchy-bar', changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [HierarchyBarService], standalone: false, template: "<!-- Hierarchy Bar - Standard Layout -->\n@if (mode !== 'collapsed') {\n <ux-hierarchy-bar-standard [readonly]=\"readonly\" [mode]=\"mode\">\n <!-- Forward the content to the correct layout -->\n <ng-container ngProjectAs=\"left-addons\" [ngTemplateOutlet]=\"leftAddons\"></ng-container>\n <ng-container ngProjectAs=\"trailing-addons\" [ngTemplateOutlet]=\"trailingAddons\"></ng-container>\n <ng-container ngProjectAs=\"right-addons\" [ngTemplateOutlet]=\"rightAddons\"></ng-container>\n </ux-hierarchy-bar-standard>\n}\n\n<!-- Hierarchy Bar - Collapsed Layout -->\n@if (mode === 'collapsed') {\n <ux-hierarchy-bar-collapsed [readonly]=\"readonly\">\n <!-- Forward the content to the correct layout -->\n <ng-container ngProjectAs=\"left-addons\" [ngTemplateOutlet]=\"leftAddons\"></ng-container>\n <ng-container ngProjectAs=\"trailing-addons\" [ngTemplateOutlet]=\"trailingAddons\"></ng-container>\n <ng-container ngProjectAs=\"right-addons\" [ngTemplateOutlet]=\"rightAddons\"></ng-container>\n </ux-hierarchy-bar-collapsed>\n}\n\n<!-- We can only have one ng-content so this allows us to use it more than once -->\n<ng-template #leftAddons>\n <ng-content select=\"[uxHierarchyBarLeftAddon]\"></ng-content>\n</ng-template>\n\n<ng-template #trailingAddons>\n <ng-content select=\"[uxHierarchyBarTrailingAddon]\"></ng-content>\n</ng-template>\n\n<ng-template #rightAddons>\n <ng-content select=\"[uxHierarchyBarRightAddon]\"></ng-content>\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { mode: [{
type: Input
}], readonly: [{
type: Input
}], root: [{
type: Input
}], selected: [{
type: Input
}], loadingIndicator: [{
type: Input
}], overflowTemplate: [{
type: Input
}], popoverShowTriggers: [{
type: Input
}], popoverHideTriggers: [{
type: Input
}], showSiblingsAriaLabel: [{
type: Input
}], selectedChange: [{
type: Output
}], icon: [{
type: ContentChild,
args: [HierarchyBarNodeIconDirective, { read: TemplateRef, static: false }]
}] } });
class ClickOutsideModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideModule, declarations: [ClickOutsideDirective], exports: [ClickOutsideDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ClickOutsideModule, decorators: [{
type: NgModule,
args: [{
exports: [ClickOutsideDirective],
declarations: [ClickOutsideDirective],
}]
}] });
class PopoverModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: PopoverModule, declarations: [PopoverComponent, PopoverDirective], imports: [CommonModule, OverlayModule, ObserversModule$1, ClickOutsideModule, TooltipModule], exports: [PopoverDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverModule, imports: [CommonModule, OverlayModule, ObserversModule$1, ClickOutsideModule, TooltipModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PopoverModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, OverlayModule, ObserversModule$1, ClickOutsideModule, TooltipModule],
exports: [PopoverDirective],
declarations: [PopoverComponent, PopoverDirective],
}]
}] });
class HierarchyBarModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarModule, declarations: [HierarchyBarComponent,
HierarchyBarStandardComponent,
HierarchyBarCollapsedComponent,
HierarchyBarNodeComponent,
HierarchyBarPopoverComponent,
HierarchyBarPopoverItemComponent,
HierarchyBarNodeIconDirective], imports: [AccessibilityModule,
CommonModule,
FocusIfModule,
IconModule,
PopoverModule,
ResizeModule], exports: [HierarchyBarComponent,
HierarchyBarStandardComponent,
HierarchyBarCollapsedComponent,
HierarchyBarNodeIconDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarModule, imports: [AccessibilityModule,
CommonModule,
FocusIfModule,
IconModule,
PopoverModule,
ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HierarchyBarModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
FocusIfModule,
IconModule,
PopoverModule,
ResizeModule,
],
exports: [
HierarchyBarComponent,
HierarchyBarStandardComponent,
HierarchyBarCollapsedComponent,
HierarchyBarNodeIconDirective,
],
declarations: [
HierarchyBarComponent,
HierarchyBarStandardComponent,
HierarchyBarCollapsedComponent,
HierarchyBarNodeComponent,
HierarchyBarPopoverComponent,
HierarchyBarPopoverItemComponent,
HierarchyBarNodeIconDirective,
],
}]
}] });
class InputDropdownComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Filter text */
this.filter = '';
/** Controls the disabled state of the input-dropdown. */
this.disabled = false;
/** Define the placeholder for the filter input */
this.placeholder = 'Type to filter...';
/** Aria label of the filter field. If not specified, the placeholder will be used. */
this.ariaLabel = '';
/** Aria label of the search button icon. */
this.searchFilterButtonAriaLabel = 'Search';
/** Aria label of the clear button icon. */
this.clearFilterButtonAriaLabel = 'Clear';
/** Emit when the selected item is changed */
this.selectedChange = new EventEmitter();
/** Emit when the filter text is changed */
this.filterChange = new EventEmitter();
/** Emits when `dropdownOpen` changes. */
this.dropdownOpenChange = new EventEmitter();
/** The status of the dropdown. */
this.dropdownOpen = false;
/** Store the filter button aria label */
this._filterButtonAriaLabel = this.searchFilterButtonAriaLabel;
/** Store the change callback provided by Angular Forms */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChange = () => { };
/** Store the touched callback provided by Angular Forms */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouched = () => { };
/** Unsubscribe from all observables on component destroy */
this._onDestroy$ = new Subject();
}
/** Define the max height of the dropdown */
set maxHeight(value) {
this._maxHeight = coerceCssPixelValue(value);
}
ngOnChanges(changes) {
// if the dropdownOpen state changes via the input we should show or hide the input accordingly
if (changes.dropdownOpen &&
!changes.dropdownOpen.firstChange &&
changes.dropdownOpen.currentValue !== changes.dropdownOpen.previousValue) {
changes.dropdownOpen.currentValue
? this.menuTrigger.openMenu()
: this.menuTrigger.closeMenu();
}
if (changes.selected) {
// if an item is programmatically selected we should close the menu if it is open
if (this.menuTrigger && !changes.selected.firstChange) {
this.menuTrigger.closeMenu();
}
this.resetFilter();
this.selectedChange.emit(changes.selected.currentValue);
this.onChange(changes.selected.currentValue);
this.onTouched();
}
if (changes.filter) {
this.setFilterButtonAriaLabel();
}
}
ngAfterViewInit() {
// if the user has initially set the dropdownOpen input to true we should open the menu
// once we have access to the ViewChild menu trigger directive
if (this.dropdownOpen) {
// trigger menu open on the next tick to avoid expression changed issues)
Promise.resolve().then(() => this.menuTrigger.openMenu());
}
this._changeDetector.detectChanges();
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
resetFilter() {
this.filter = '';
this.filterChange.emit(this.filter);
this._focusFilter();
}
registerOnChange(onChange) {
this.onChange = onChange;
}
registerOnTouched(onTouched) {
this.onTouched = onTouched;
}
writeValue(value) {
this.selected = value;
this._changeDetector.markForCheck();
}
resetValue(event) {
if (this.disabled) {
return;
}
this.writeValue(undefined);
this.selectedChange.emit(undefined);
event.stopPropagation();
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
onMenuOpen() {
if (this.dropdownOpen !== true) {
this.dropdownOpen = true;
this.dropdownOpenChange.emit(this.dropdownOpen);
this._focusFilter();
}
}
onMenuClose() {
if (this.dropdownOpen !== false) {
this.dropdownOpen = false;
this.dropdownOpenChange.emit(this.dropdownOpen);
}
}
_focusFilter() {
if (this.filterInputElement) {
this.filterInputElement.nativeElement.focus();
}
}
inputFocusHandler() {
if (!this.dropdownOpen) {
this.dropdownOpen = true;
this.dropdownOpenChange.emit(this.dropdownOpen);
}
}
toggleMenu() {
if (this.disabled) {
return;
}
this.dropdownOpen = !this.dropdownOpen;
this.dropdownOpenChange.emit(this.dropdownOpen);
this.menuTrigger.toggleMenu();
}
setFilterButtonAriaLabel() {
this._filterButtonAriaLabel =
this.filter === '' ? this.searchFilterButtonAriaLabel : this.clearFilterButtonAriaLabel;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: InputDropdownComponent, isStandalone: false, selector: "ux-input-dropdown", inputs: { selected: "selected", filter: "filter", hideFilter: "hideFilter", maxHeight: "maxHeight", disabled: "disabled", allowNull: "allowNull", placeholder: "placeholder", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: "ariaLabelledby", searchFilterButtonAriaLabel: "searchFilterButtonAriaLabel", clearFilterButtonAriaLabel: "clearFilterButtonAriaLabel", dropdownOpen: "dropdownOpen" }, outputs: { selectedChange: "selectedChange", filterChange: "filterChange", dropdownOpenChange: "dropdownOpenChange" }, host: { properties: { "class.ux-select-disabled": "disabled", "attr.aria-label": "null" } }, providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => InputDropdownComponent),
},
], queries: [{ propertyName: "displayContentRef", first: true, predicate: ["displayContent"], descendants: true }], viewQueries: [{ propertyName: "menuTrigger", first: true, predicate: MenuTriggerDirective, descendants: true }, { propertyName: "filterInputElement", first: true, predicate: ["filterInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ux-select-container\">\n <button\n #button\n type=\"button\"\n class=\"form-control\"\n [uxMenuTriggerFor]=\"menu\"\n [disabled]=\"disabled\"\n >\n <ng-template #defaultDisplayContent>{{ selected ? (selected | json) : '-' }}</ng-template>\n <ng-container [ngTemplateOutlet]=\"displayContentRef || defaultDisplayContent\"></ng-container>\n </button>\n <div class=\"ux-select-icons\">\n @if (allowNull && selected) {\n <ux-icon\n name=\"close\"\n uxFocusIndicator\n class=\"ux-select-icon ux-select-clear-icon\"\n (click)=\"resetValue($event)\"\n (keydown.enter)=\"resetValue($event)\"\n tabindex=\"0\"\n >\n </ux-icon>\n }\n <ux-icon\n name=\"chevron-down\"\n class=\"ux-select-icon ux-select-chevron-icon\"\n (click)=\"toggleMenu(); $event.stopPropagation()\"\n >\n </ux-icon>\n </div>\n</div>\n\n<ux-menu #menu menuClass=\"select-menu\" (opened)=\"onMenuOpen()\" (closed)=\"onMenuClose()\">\n <div [style.max-height]=\"_maxHeight\" [style.width.px]=\"button.offsetWidth\">\n @if (!hideFilter) {\n <div class=\"filter-container\">\n <input\n #filterInput\n type=\"text\"\n [placeholder]=\"placeholder\"\n class=\"form-control\"\n [(ngModel)]=\"filter\"\n (input)=\"filterChange.emit(filter)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"ariaLabel || placeholder\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n (focus)=\"inputFocusHandler()\"\n />\n <button\n type=\"button\"\n class=\"btn btn-flat filter-button\"\n [attr.aria-label]=\"_filterButtonAriaLabel\"\n (click)=\"resetFilter(); $event.stopPropagation()\"\n [tabindex]=\"filter.length > 0 ? 0 : -1\"\n >\n <ux-icon [name]=\"filter.length === 0 ? 'search' : 'close'\"></ux-icon>\n </button>\n </div>\n }\n\n <ng-content></ng-content>\n </div>\n</ux-menu>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "pipe", type: i1.JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-input-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => InputDropdownComponent),
},
], host: {
'[class.ux-select-disabled]': 'disabled',
'[attr.aria-label]': 'null',
}, standalone: false, template: "<div class=\"ux-select-container\">\n <button\n #button\n type=\"button\"\n class=\"form-control\"\n [uxMenuTriggerFor]=\"menu\"\n [disabled]=\"disabled\"\n >\n <ng-template #defaultDisplayContent>{{ selected ? (selected | json) : '-' }}</ng-template>\n <ng-container [ngTemplateOutlet]=\"displayContentRef || defaultDisplayContent\"></ng-container>\n </button>\n <div class=\"ux-select-icons\">\n @if (allowNull && selected) {\n <ux-icon\n name=\"close\"\n uxFocusIndicator\n class=\"ux-select-icon ux-select-clear-icon\"\n (click)=\"resetValue($event)\"\n (keydown.enter)=\"resetValue($event)\"\n tabindex=\"0\"\n >\n </ux-icon>\n }\n <ux-icon\n name=\"chevron-down\"\n class=\"ux-select-icon ux-select-chevron-icon\"\n (click)=\"toggleMenu(); $event.stopPropagation()\"\n >\n </ux-icon>\n </div>\n</div>\n\n<ux-menu #menu menuClass=\"select-menu\" (opened)=\"onMenuOpen()\" (closed)=\"onMenuClose()\">\n <div [style.max-height]=\"_maxHeight\" [style.width.px]=\"button.offsetWidth\">\n @if (!hideFilter) {\n <div class=\"filter-container\">\n <input\n #filterInput\n type=\"text\"\n [placeholder]=\"placeholder\"\n class=\"form-control\"\n [(ngModel)]=\"filter\"\n (input)=\"filterChange.emit(filter)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"ariaLabel || placeholder\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n (focus)=\"inputFocusHandler()\"\n />\n <button\n type=\"button\"\n class=\"btn btn-flat filter-button\"\n [attr.aria-label]=\"_filterButtonAriaLabel\"\n (click)=\"resetFilter(); $event.stopPropagation()\"\n [tabindex]=\"filter.length > 0 ? 0 : -1\"\n >\n <ux-icon [name]=\"filter.length === 0 ? 'search' : 'close'\"></ux-icon>\n </button>\n </div>\n }\n\n <ng-content></ng-content>\n </div>\n</ux-menu>\n" }]
}], propDecorators: { selected: [{
type: Input
}], filter: [{
type: Input
}], hideFilter: [{
type: Input
}], maxHeight: [{
type: Input
}], disabled: [{
type: Input
}], allowNull: [{
type: Input
}], placeholder: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input
}], searchFilterButtonAriaLabel: [{
type: Input
}], clearFilterButtonAriaLabel: [{
type: Input
}], selectedChange: [{
type: Output
}], filterChange: [{
type: Output
}], dropdownOpenChange: [{
type: Output
}], dropdownOpen: [{
type: Input
}], displayContentRef: [{
type: ContentChild,
args: ['displayContent', { static: false }]
}], menuTrigger: [{
type: ViewChild,
args: [MenuTriggerDirective, { static: false }]
}], filterInputElement: [{
type: ViewChild,
args: ['filterInput', { static: false }]
}] } });
class InputDropdownModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownModule, declarations: [InputDropdownComponent], imports: [CommonModule, FormsModule, IconModule, MenuModule, AccessibilityModule], exports: [InputDropdownComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownModule, imports: [CommonModule, FormsModule, IconModule, MenuModule, AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: InputDropdownModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, FormsModule, IconModule, MenuModule, AccessibilityModule],
declarations: [InputDropdownComponent],
exports: [InputDropdownComponent],
}]
}] });
var SidePanelAnimationState;
(function (SidePanelAnimationState) {
SidePanelAnimationState["Closed"] = "closed";
SidePanelAnimationState["Open"] = "open";
SidePanelAnimationState["OpenImmediate"] = "openImmediate";
})(SidePanelAnimationState || (SidePanelAnimationState = {}));
const sidePanelStateAnimation = trigger('panelState', [
state(SidePanelAnimationState.Closed, style({ visibility: 'hidden' })),
state(`${SidePanelAnimationState.Open}, ${SidePanelAnimationState.OpenImmediate}`, style({ visibility: 'visible', transform: 'none' })),
transition(`void <=> ${SidePanelAnimationState.Open}`, animate('0.2s cubic-bezier(0.49, 1, 0.38, 0.98)')),
transition(`void <=> ${SidePanelAnimationState.OpenImmediate}`, animate('0s')),
]);
class SidePanelService {
constructor() {
/** Emit the open state when it changes */
this.open$ = new BehaviorSubject(false);
}
open() {
this.open$.next(true);
}
close() {
this.open$.next(false);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelService, decorators: [{
type: Injectable
}] });
class SidePanelComponent {
constructor() {
this.service = inject(SidePanelService);
this._elementRef = inject(ElementRef);
this._focusOrigin = inject(FocusIndicatorOriginService);
this.inline = false;
this.attachTo = 'window';
this.width = '50%';
this.top = '0';
this.modal = false;
this.animate = false;
this.closeOnExternalClick = false;
this.focusOnShow = false;
this.openChange = new EventEmitter();
this.closeOnEscape = true;
this.animationPanelState = SidePanelAnimationState.Closed;
this._onDestroy = new Subject();
}
get open() {
return this.service.open$.value;
}
set open(value) {
this.service.open$.next(value);
}
get position() {
if (this.inline) {
return 'static';
}
if (this.attachTo === 'container') {
return 'absolute';
}
return 'fixed';
}
get cssWidth() {
return this.getCssValue(this.width);
}
get cssTop() {
return this.getCssValue(this.top);
}
get cssMinWidth() {
return this.getCssValue(this.minWidth);
}
get cssMaxWidth() {
return this.getCssValue(this.maxWidth);
}
get componentWidth() {
if (this.inline) {
return this.open ? this.cssWidth : '0';
}
return null;
}
get hostWidth() {
return this.inline ? '100%' : this.cssWidth;
}
get hostMinWidth() {
return this.inline ? undefined : this.cssMinWidth;
}
get hostMaxWidth() {
return this.inline ? undefined : this.cssMaxWidth;
}
ngOnInit() {
this.service.open$
.pipe(skip(1), distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(isOpen => this.openChange.emit(isOpen));
this.service.open$
.pipe(distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(isOpen => {
this.animationPanelState = isOpen
? this.animate
? SidePanelAnimationState.Open
: SidePanelAnimationState.OpenImmediate
: SidePanelAnimationState.Closed;
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
openPanel() {
this.service.open();
}
closePanel() {
this.service.close();
}
_onDocumentEscape() {
if (this.open && this.closeOnEscape) {
this._focusOrigin.setOrigin('keyboard');
this.closePanel();
}
}
_onDocumentClick(target) {
if (!this.open || !this.closeOnExternalClick) {
return;
}
if (!this._elementRef.nativeElement.contains(target) ||
(target && target.classList.contains('modal-backdrop'))) {
this.closePanel();
}
}
getCssValue(value) {
if (typeof value === 'number') {
return value === 0 ? '0' : value + 'px';
}
return value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SidePanelComponent, isStandalone: false, selector: "ux-side-panel", inputs: { open: "open", inline: "inline", attachTo: "attachTo", width: "width", minWidth: "minWidth", maxWidth: "maxWidth", top: "top", modal: "modal", animate: "animate", closeOnExternalClick: "closeOnExternalClick", focusOnShow: "focusOnShow", closeOnEscape: "closeOnEscape" }, outputs: { openChange: "openChange" }, host: { listeners: { "document:keyup.escape": "_onDocumentEscape()", "document:click": "_onDocumentClick($event.target)" }, properties: { "class.open": "this.open", "class.inline": "this.inline", "attr.aria-modal": "this.modal", "class.animate": "this.animate", "style.width": "this.componentWidth" }, classAttribute: "ux-side-panel" }, providers: [SidePanelService], exportAs: ["ux-side-panel"], ngImport: i0, template: "@if (modal && open) {\n <div class=\"modal-backdrop\" [style.position]=\"position\" [style.top]=\"cssTop\"></div>\n}\n\n@if (open) {\n <div\n class=\"ux-side-panel-host\"\n [class.modal-panel]=\"modal\"\n [style.position]=\"position\"\n [style.width]=\"hostWidth\"\n [style.min-width]=\"hostMinWidth\"\n [style.max-width]=\"hostMaxWidth\"\n [style.top]=\"cssTop\"\n [tabindex]=\"open ? 0 : -1\"\n [@panelState]=\"animationPanelState\"\n [focusIf]=\"open && focusOnShow\"\n [focusIfScroll]=\"false\"\n [cdkTrapFocus]=\"open && modal\"\n >\n <ng-content></ng-content>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1$1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }], animations: [sidePanelStateAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-side-panel', exportAs: 'ux-side-panel', providers: [SidePanelService], animations: [sidePanelStateAnimation], changeDetection: ChangeDetectionStrategy.OnPush, host: {
class: 'ux-side-panel',
}, standalone: false, template: "@if (modal && open) {\n <div class=\"modal-backdrop\" [style.position]=\"position\" [style.top]=\"cssTop\"></div>\n}\n\n@if (open) {\n <div\n class=\"ux-side-panel-host\"\n [class.modal-panel]=\"modal\"\n [style.position]=\"position\"\n [style.width]=\"hostWidth\"\n [style.min-width]=\"hostMinWidth\"\n [style.max-width]=\"hostMaxWidth\"\n [style.top]=\"cssTop\"\n [tabindex]=\"open ? 0 : -1\"\n [@panelState]=\"animationPanelState\"\n [focusIf]=\"open && focusOnShow\"\n [focusIfScroll]=\"false\"\n [cdkTrapFocus]=\"open && modal\"\n >\n <ng-content></ng-content>\n </div>\n}\n" }]
}], propDecorators: { open: [{
type: Input
}, {
type: HostBinding,
args: ['class.open']
}], inline: [{
type: Input
}, {
type: HostBinding,
args: ['class.inline']
}], attachTo: [{
type: Input
}], width: [{
type: Input
}], minWidth: [{
type: Input
}], maxWidth: [{
type: Input
}], top: [{
type: Input
}], modal: [{
type: Input
}, {
type: HostBinding,
args: ['attr.aria-modal']
}], animate: [{
type: Input
}, {
type: HostBinding,
args: ['class.animate']
}], closeOnExternalClick: [{
type: Input
}], focusOnShow: [{
type: Input
}], openChange: [{
type: Output
}], closeOnEscape: [{
type: Input
}], componentWidth: [{
type: HostBinding,
args: ['style.width']
}], _onDocumentEscape: [{
type: HostListener,
args: ['document:keyup.escape']
}], _onDocumentClick: [{
type: HostListener,
args: ['document:click', ['$event.target']]
}] } });
class ItemDisplayPanelContentDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ItemDisplayPanelContentDirective, isStandalone: false, selector: "[uxItemDisplayPanelContent]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelContentDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxItemDisplayPanelContent]',
standalone: false,
}]
}] });
class ItemDisplayPanelFooterDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ItemDisplayPanelFooterDirective, isStandalone: false, selector: "[uxItemDisplayPanelFooter]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelFooterDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxItemDisplayPanelFooter]',
standalone: false,
}]
}] });
class ItemDisplayPanelComponent extends SidePanelComponent {
get preventClose() {
return !this.closeOnExternalClick;
}
set preventClose(value) {
this.closeOnExternalClick = !value;
}
set visible(visible) {
this.open = visible;
}
get visible() {
return this.open;
}
constructor() {
super();
this.boxShadow = true;
this.closeVisible = true;
/** Defines the aria-label for the close button */
this.closeAriaLabel = 'Close';
this.shadow = false;
this.visibleChange = new EventEmitter();
this.animate = false;
this.closeOnExternalClick = true;
}
ngOnInit() {
super.ngOnInit();
this.service.open$
.pipe(distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(isVisible => this.visibleChange.emit(isVisible));
}
focus() {
if (this.panel) {
this.panel.nativeElement.focus();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ItemDisplayPanelComponent, isStandalone: false, selector: "ux-item-display-panel", inputs: { header: "header", boxShadow: "boxShadow", closeVisible: "closeVisible", preventClose: "preventClose", closeAriaLabel: "closeAriaLabel", shadow: "shadow", visible: "visible" }, outputs: { visibleChange: "visibleChange" }, host: { classAttribute: "ux-side-panel ux-item-display-panel" }, providers: [SidePanelService], queries: [{ propertyName: "footer", first: true, predicate: ItemDisplayPanelFooterDirective, descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "@if (open) {\n <div\n class=\"ux-side-panel-host ux-item-display-panel\"\n #panel\n [class.box-shadow]=\"boxShadow\"\n [style.position]=\"position\"\n [style.width]=\"hostWidth\"\n [style.top]=\"cssTop\"\n [@panelState]=\"animationPanelState\"\n [tabindex]=\"open ? 0 : -1\"\n [focusIf]=\"open && focusOnShow\"\n >\n <div class=\"ux-side-panel-header\" [class.item-display-panel-shadow]=\"shadow\">\n <h3>{{ header }}</h3>\n @if (closeVisible) {\n <button\n uxFocusIndicator\n [attr.aria-label]=\"closeAriaLabel\"\n type=\"button\"\n class=\"btn btn-lg btn-link btn-icon button-secondary\"\n (click)=\"visible = false\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n </div>\n <div class=\"ux-side-panel-content\">\n <ng-content select=\"[uxItemDisplayPanelContent]\"></ng-content>\n </div>\n @if (footer) {\n <div class=\"ux-side-panel-footer\">\n <ng-content select=\"[uxItemDisplayPanelFooter]\"></ng-content>\n </div>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], animations: [sidePanelStateAnimation] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-item-display-panel', providers: [SidePanelService], animations: [sidePanelStateAnimation], host: {
class: 'ux-side-panel ux-item-display-panel',
}, standalone: false, template: "@if (open) {\n <div\n class=\"ux-side-panel-host ux-item-display-panel\"\n #panel\n [class.box-shadow]=\"boxShadow\"\n [style.position]=\"position\"\n [style.width]=\"hostWidth\"\n [style.top]=\"cssTop\"\n [@panelState]=\"animationPanelState\"\n [tabindex]=\"open ? 0 : -1\"\n [focusIf]=\"open && focusOnShow\"\n >\n <div class=\"ux-side-panel-header\" [class.item-display-panel-shadow]=\"shadow\">\n <h3>{{ header }}</h3>\n @if (closeVisible) {\n <button\n uxFocusIndicator\n [attr.aria-label]=\"closeAriaLabel\"\n type=\"button\"\n class=\"btn btn-lg btn-link btn-icon button-secondary\"\n (click)=\"visible = false\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n </div>\n <div class=\"ux-side-panel-content\">\n <ng-content select=\"[uxItemDisplayPanelContent]\"></ng-content>\n </div>\n @if (footer) {\n <div class=\"ux-side-panel-footer\">\n <ng-content select=\"[uxItemDisplayPanelFooter]\"></ng-content>\n </div>\n }\n </div>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { header: [{
type: Input
}], boxShadow: [{
type: Input
}], closeVisible: [{
type: Input
}], preventClose: [{
type: Input
}], closeAriaLabel: [{
type: Input
}], shadow: [{
type: Input
}], visibleChange: [{
type: Output
}], footer: [{
type: ContentChild,
args: [ItemDisplayPanelFooterDirective, { static: false }]
}], panel: [{
type: ViewChild,
args: ['panel', { static: true }]
}], visible: [{
type: Input
}] } });
const DECLARATIONS$6 = [
ItemDisplayPanelComponent,
ItemDisplayPanelContentDirective,
ItemDisplayPanelFooterDirective,
];
class ItemDisplayPanelModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelModule, declarations: [ItemDisplayPanelComponent,
ItemDisplayPanelContentDirective,
ItemDisplayPanelFooterDirective], imports: [AccessibilityModule, CommonModule, FocusIfModule, IconModule], exports: [ItemDisplayPanelComponent,
ItemDisplayPanelContentDirective,
ItemDisplayPanelFooterDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelModule, imports: [AccessibilityModule, CommonModule, FocusIfModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ItemDisplayPanelModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, FocusIfModule, IconModule],
exports: DECLARATIONS$6,
declarations: DECLARATIONS$6,
}]
}] });
class MarqueeWizardStepIconDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardStepIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MarqueeWizardStepIconDirective, isStandalone: false, selector: "[uxMarqueeWizardStepIcon]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardStepIconDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMarqueeWizardStepIcon]',
standalone: false,
}]
}] });
/**
* This service is required to provide a form of communication
* between the marquee wizard steps and the containing marquee wizard.
* We cannot inject the Host due to the steps being content children
* rather than view children.
*/
class WizardService {
constructor() {
this.validChange$ = new Subject();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardService, decorators: [{
type: Injectable
}] });
class WizardStepComponent {
constructor() {
this._wizardService = inject(WizardService);
this._changeDetector = inject(ChangeDetectorRef);
/**
* Defines whether a step is valid. The user will not be able to proceed to the next step if this property has a value of false.
* If the new value is false is will also set the visited value to false.
*/
this._valid = true;
/**
* Defines whether or not this step has previously been visited.
* A visited step can be clicked on and jumped to at any time.
* By default, steps will become 'visited' when the user navigates to a step for the first time.
*/
this.visited = false;
/** Emits when visited changes. */
this.visitedChange = new EventEmitter();
/**
* Defines the currently visible step.
*/
this._active = false;
}
set valid(value) {
this.setValid(value);
}
get valid() {
return this._valid;
}
set active(value) {
const active = coerceBooleanProperty(value);
// store the active state of the step
this._active = active;
// if the value is true then the step should also be marked as visited
if (active && !this.visited) {
this.setVisitedAndEmitChangeEvent(true);
}
// mark for change detection
this._changeDetector.markForCheck();
}
get active() {
return this._active;
}
setVisitedAndEmitChangeEvent(value) {
if (value === this.visited) {
return;
}
this.visited = value;
this.visitedChange.emit(value);
}
setValid(value) {
if (this._valid === value) {
return;
}
this._valid = value;
this._wizardService.validChange$.next({ step: this, valid: value });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardStepComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: WizardStepComponent, isStandalone: false, selector: "ux-wizard-step", inputs: { header: "header", disableNextWhenInvalid: "disableNextWhenInvalid", valid: "valid", validator: "validator", visited: "visited" }, outputs: { visitedChange: "visitedChange" }, host: { attributes: { "role": "tabpanel" } }, ngImport: i0, template: "@if (active) {\n <ng-content></ng-content>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardStepComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-wizard-step', changeDetection: ChangeDetectionStrategy.OnPush, host: {
role: 'tabpanel',
}, standalone: false, template: "@if (active) {\n <ng-content></ng-content>\n}\n" }]
}], propDecorators: { header: [{
type: Input
}], disableNextWhenInvalid: [{
type: Input
}], valid: [{
type: Input
}], validator: [{
type: Input
}], visited: [{
type: Input
}], visitedChange: [{
type: Output
}] } });
let uniqueId$5 = 0;
class WizardComponent {
constructor() {
this._wizardService = inject(WizardService);
/** Defines whether or not the wizard should be displayed in a `horizontal` or `vertical` layout. */
this.orientation = 'horizontal';
/** Defines the text displayed in the 'Next' button. */
this.nextText = 'Next';
/** Defines the text displayed in the 'Previous' button. */
this.previousText = 'Previous';
/** Defines the text displayed in the 'Cancel' button. */
this.cancelText = 'Cancel';
/** Defines the text displayed in the 'Finish' button. */
this.finishText = 'Finish';
/** Defines the text displayed in the tooltip when the 'Next' button is hovered. */
this.nextTooltip = 'Go to the next step';
/** Defines the text displayed in the tooltip when the 'Previous' button is hovered. */
this.previousTooltip = 'Go to the previous step';
/** Defines the text displayed in the tooltip when the 'Cancel' button is hovered. */
this.cancelTooltip = 'Cancel the wizard';
/** Defines the text displayed in the tooltip when the 'Finish' button is hovered. */
this.finishTooltip = 'Finish the wizard';
/** Defines the text for the aria label on the 'Next' button. */
this.nextAriaLabel = 'Go to the next step';
/** Defines the text for the aria label on the 'Previous' button. */
this.previousAriaLabel = 'Go to the previous step';
/** Defines the text for the aria label on the 'Cancel' button. */
this.cancelAriaLabel = 'Cancel the wizard';
/** Defines the text for the aria label on the 'Finish' button. */
this.finishAriaLabel = 'Finish the wizard';
/** If set to `true` the 'Next' button will appear disabled and will not respond to clicks. */
this.nextDisabled = false;
/** If set to `true` the 'Previous' button will appear disabled and will not respond to clicks. */
this.previousDisabled = false;
/** If set to `true` the 'Cancel' button will appear disabled and will not respond to clicks. */
this.cancelDisabled = false;
/** If set to `true` the 'Finish' button will appear disabled and will not respond to clicks. */
this.finishDisabled = false;
/** If set to `false` the 'Next' button will be hidden. */
this.nextVisible = true;
/** If set to `false` the 'Previous' button will be hidden. */
this.previousVisible = true;
/** If set to `false` the 'Cancel' button will be hidden. */
this.cancelVisible = true;
/** If set to false the 'Finish' button will be hidden. */
this.finishVisible = true;
/** If set to `true` the 'Cancel' button will be visible even on the last step. By default it will be hidden on the final step. */
this.cancelAlwaysVisible = false;
/** If set to `true` the 'Finish' button will be visible on all steps of the wizard. By default this button will only be visible on the final step of the wizard. */
this.finishAlwaysVisible = false;
/** If set to `true` the 'Next' or 'Finish' button will become disabled when the current step is invalid. */
this.disableNextWhenInvalid = false;
/** Whether to set `visited` to false on subsequent steps after a validation fault. */
this.resetVisitedOnValidationError = false;
/** If set to false it will allow users to navigate to every step */
this.sequential = true;
/** Emits when the wizard has moved to the next step. It will receive the current step index as a parameter. */
this.onNext = new EventEmitter();
/** Emits when the wizard has moved to the previous step. It will receive the current step index as a parameter. */
this.onPrevious = new EventEmitter();
/** Emits when the 'Cancel' button has been pressed. */
this.onCancel = new EventEmitter();
/** Emits when the 'Finish' button is clicked, but before the finish event fires. This fires regardless of the validity of the final step. */
this.onFinishing = new EventEmitter();
/** Emits when the 'Finish' button has been pressed and the final step is valid. */
this.onFinish = new EventEmitter();
/** Emits before the current step changes. The event contains the current step index in the `from` property, and the requested step index in the `to` property. */
this.stepChanging = new EventEmitter();
/** Emits when the current step has changed. */
this.stepChange = new EventEmitter();
/** Emits when the user tries to continue but the current step is invalid. */
this.stepError = new EventEmitter();
this.steps = new QueryList();
this.id = `ux-wizard-${uniqueId$5++}`;
this.invalidIndicator = false;
this._step = 0;
this._onDestroy = new Subject();
}
/**
* The current active step. When the step changes an event will be emitted containing the index of the newly active step.
* If this is not specified the wizard will start on the first step.
*/
get step() {
return this._step;
}
set step(value) {
// only accept numbers as valid options
if (typeof value === 'number') {
// store the active step
this._step = value;
// update which steps should be active
this.update();
// reset the invalid state
this.invalidIndicator = false;
}
}
ngOnInit() {
// initially set the correct visibility of the steps
setTimeout(this.update.bind(this));
// watch for changes to valid subject
this._wizardService.validChange$
.pipe(filter((event) => !event.valid), takeUntil(this._onDestroy))
.subscribe((event) => this.setFutureStepsUnvisited(event.step));
}
ngAfterContentInit() {
this.steps.changes.pipe(tick(), takeUntil(this._onDestroy)).subscribe(this.update.bind(this));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Navigate to the next step
*/
async next() {
this.stepChanging.next(new StepChangingEvent(this.step, this.step + 1));
const step = this.getCurrentStep();
// Disable the button while waiting on validation
this.nextDisabled = true;
try {
// Fetch validation status
const validationResult = this.isStepValid();
step.valid = validationResult instanceof Promise ? await validationResult : validationResult;
}
finally {
// Re-enable button
this.nextDisabled = false;
}
// check if current step is invalid
if (!step.valid) {
this.invalidIndicator = true;
this.stepError.next(this.step);
return;
}
// check if we are currently on the last step
if (this.step + 1 < this.steps.length) {
this.step++;
this.stepChange.emit(this.step);
// emit the current step
this.onNext.next(this.step);
}
}
/**
* Whether the Next or Finish button should be disabled.
*/
isNextDisabled() {
const step = this.getCurrentStep();
// ensure the step is not null before we try to access its properties. It may be null if an ngFor is being
// used and the steps haven't rendered yet
if (!step) {
return false;
}
// Use the `disableNextWhenInvalid` setting to determine whether to disable the Next/Finish button
// based on validation.
// If not defined on the WizardStepComponent, use the value from WizardComponent.
return ((step.disableNextWhenInvalid === undefined
? this.disableNextWhenInvalid
: step.disableNextWhenInvalid) && !step.valid);
}
/**
* Navigate to the previous step
*/
previous() {
this.stepChanging.next(new StepChangingEvent(this.step, this.step - 1));
// check if we are currently on the last step
if (this.step > 0) {
this.step--;
this.stepChange.emit(this.step);
// emit the current step
this.onPrevious.next(this.step);
}
}
/**
* Perform actions when the finish button is clicked
*/
async finish() {
// fires when the finish button is clicked always
this.onFinishing.next();
// Disable the button while waiting on validation
this.finishDisabled = true;
try {
// Fetch validation status
const validationResult = this.isStepValid();
this.getCurrentStep().valid =
validationResult instanceof Promise ? await validationResult : validationResult;
}
finally {
// Re-enable button
this.finishDisabled = false;
}
/**
* This is required because we need to ensure change detection has run
* to determine whether or not we have the latest value for the 'valid' input
* on the current step. Unfortunately we can't use ChangeDetectorRef as we are looking to run
* on content children, and we cant use ApplicationRef.tick() as this does not work in a hybrid app, eg. our docs
*/
return new Promise(resolve => {
setTimeout(() => {
// only fires when the finish button is clicked and the step is valid
if (this.getCurrentStep().valid) {
this.onFinish.emit();
}
else {
this.stepError.next(this.step);
}
resolve();
});
});
}
/**
* Perform actions when the cancel button is clicked
*/
cancel() {
this.onCancel.next();
}
/**
* Update the active state of each step
*/
update() {
// update which steps should be active
this.steps.forEach((step, idx) => (step.active = idx === this.step));
}
/**
* Jump to a specific step only if the step has previously been visited
*/
gotoStep(step) {
if (step.visited || !this.sequential) {
const stepIndex = this.steps.toArray().findIndex(stp => stp === step);
this.stepChanging.next(new StepChangingEvent(this.step, stepIndex));
this.step = stepIndex;
this.stepChange.emit(this.step);
}
}
/**
* Determine if the current step is the last step
*/
isLastStep() {
return this.step === this.steps.length - 1;
}
/**
* Reset the wizard - goes to first step and resets visited state
*/
reset() {
// mark all steps as not visited
this.steps.forEach(step => step.setVisitedAndEmitChangeEvent(false));
// go to the first step
this.step = 0;
this.stepChange.emit(this.step);
}
/**
* Get the step at the current index
*/
getCurrentStep() {
return this.getStepAtIndex(this.step);
}
/**
* Return a step at a specific index
*/
getStepAtIndex(index) {
return this.steps.toArray()[index];
}
/**
* If a step in the wizard becomes invalid, all steps sequentially after
* it should become unvisited
*/
setFutureStepsUnvisited(currentStep) {
if (!this.resetVisitedOnValidationError) {
return;
}
this.getFutureSteps(currentStep).forEach(step => {
step.setVisitedAndEmitChangeEvent(false);
});
}
/**
* Get the currently active step and all steps beyond it
*/
getFutureSteps(currentStep) {
const currentIndex = this.steps.toArray().indexOf(currentStep);
return this.steps.toArray().slice(currentIndex + 1);
}
/**
* Returns the valid status of the current step, including the `validation` function (if provided).
*/
isStepValid() {
// get the current active step
const currentStep = this.getCurrentStep();
// if there is no validator then return the valid state
if (!currentStep.validator) {
return currentStep.valid;
}
// get the validator result
return currentStep.validator();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: WizardComponent, isStandalone: false, selector: "ux-wizard", inputs: { orientation: "orientation", nextText: "nextText", previousText: "previousText", cancelText: "cancelText", finishText: "finishText", nextTooltip: "nextTooltip", previousTooltip: "previousTooltip", cancelTooltip: "cancelTooltip", finishTooltip: "finishTooltip", nextAriaLabel: "nextAriaLabel", previousAriaLabel: "previousAriaLabel", cancelAriaLabel: "cancelAriaLabel", finishAriaLabel: "finishAriaLabel", nextDisabled: "nextDisabled", previousDisabled: "previousDisabled", cancelDisabled: "cancelDisabled", finishDisabled: "finishDisabled", nextVisible: "nextVisible", previousVisible: "previousVisible", cancelVisible: "cancelVisible", finishVisible: "finishVisible", cancelAlwaysVisible: "cancelAlwaysVisible", finishAlwaysVisible: "finishAlwaysVisible", disableNextWhenInvalid: "disableNextWhenInvalid", resetVisitedOnValidationError: "resetVisitedOnValidationError", sequential: "sequential", step: "step" }, outputs: { onNext: "onNext", onPrevious: "onPrevious", onCancel: "onCancel", onFinishing: "onFinishing", onFinish: "onFinish", stepChanging: "stepChanging", stepChange: "stepChange", stepError: "stepError" }, host: { properties: { "class": "orientation" } }, providers: [WizardService], queries: [{ propertyName: "footerTemplate", first: true, predicate: ["footerTemplate"], descendants: true }, { propertyName: "steps", predicate: WizardStepComponent }], ngImport: i0, template: "<div class=\"wizard-body\">\n <div\n class=\"wizard-steps\"\n uxTabbableList\n [direction]=\"orientation\"\n role=\"tablist\"\n [attr.aria-orientation]=\"orientation\"\n >\n @for (stp of steps; track stp; let index = $index) {\n <div\n role=\"tab\"\n class=\"wizard-step\"\n [class.active]=\"stp._active\"\n [class.visited]=\"stp.visited\"\n [class.invalid]=\"!stp._valid && stp.visited\"\n [attr.aria-posinset]=\"index + 1\"\n [attr.aria-setsize]=\"steps.length\"\n [attr.aria-selected]=\"stp._active\"\n [attr.aria-controls]=\"id + '-step-' + index\"\n [attr.aria-labelledby]=\"id + '-step-' + index + '-label'\"\n [attr.aria-expanded]=\"stp._active\"\n [id]=\"id + '-step-' + index + '-label'\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n uxTabbableListItem\n [disabled]=\"index !== 0 && (!stp.visited || !sequential)\"\n (click)=\"gotoStep(stp)\"\n (keydown.enter)=\"gotoStep(stp)\"\n >\n <span class=\"wizard-step-text\">{{ stp.header }}</span>\n @if (stp.visited && !stp._active) {\n <ux-icon class=\"wizard-step-icon\" name=\"checkmark\"></ux-icon>\n }\n </div>\n }\n </div>\n\n <div class=\"wizard-content\">\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"wizard-footer\">\n @if (footerTemplate) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate\" [ngTemplateOutletContext]=\"{ step: step }\">\n </ng-container>\n }\n\n @if (previousVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary\"\n [uxTooltip]=\"previousTooltip\"\n [disabled]=\"previousDisabled || step === 0\"\n [attr.aria-label]=\"previousAriaLabel\"\n (click)=\"previous(); tip.hide()\"\n >\n {{ previousText }}\n </button>\n }\n\n @if (nextVisible && !isLastStep()) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary\"\n [uxTooltip]=\"nextTooltip\"\n [disabled]=\"nextDisabled || isNextDisabled()\"\n [attr.aria-label]=\"nextAriaLabel\"\n (click)=\"next(); tip.hide()\"\n >\n {{ nextText }}\n </button>\n }\n\n @if ((finishVisible && isLastStep()) || finishAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary\"\n [uxTooltip]=\"finishTooltip\"\n [disabled]=\"finishDisabled || isNextDisabled()\"\n [attr.aria-label]=\"finishAriaLabel\"\n (click)=\"finish(); tip.hide()\"\n >\n {{ finishText }}\n </button>\n }\n\n @if ((cancelVisible && !isLastStep()) || cancelAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary\"\n [uxTooltip]=\"cancelTooltip\"\n [disabled]=\"cancelDisabled\"\n [attr.aria-label]=\"cancelAriaLabel\"\n (click)=\"cancel(); tip.hide()\"\n >\n {{ cancelText }}\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-wizard', providers: [WizardService], host: {
'[class]': 'orientation',
}, standalone: false, template: "<div class=\"wizard-body\">\n <div\n class=\"wizard-steps\"\n uxTabbableList\n [direction]=\"orientation\"\n role=\"tablist\"\n [attr.aria-orientation]=\"orientation\"\n >\n @for (stp of steps; track stp; let index = $index) {\n <div\n role=\"tab\"\n class=\"wizard-step\"\n [class.active]=\"stp._active\"\n [class.visited]=\"stp.visited\"\n [class.invalid]=\"!stp._valid && stp.visited\"\n [attr.aria-posinset]=\"index + 1\"\n [attr.aria-setsize]=\"steps.length\"\n [attr.aria-selected]=\"stp._active\"\n [attr.aria-controls]=\"id + '-step-' + index\"\n [attr.aria-labelledby]=\"id + '-step-' + index + '-label'\"\n [attr.aria-expanded]=\"stp._active\"\n [id]=\"id + '-step-' + index + '-label'\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n uxTabbableListItem\n [disabled]=\"index !== 0 && (!stp.visited || !sequential)\"\n (click)=\"gotoStep(stp)\"\n (keydown.enter)=\"gotoStep(stp)\"\n >\n <span class=\"wizard-step-text\">{{ stp.header }}</span>\n @if (stp.visited && !stp._active) {\n <ux-icon class=\"wizard-step-icon\" name=\"checkmark\"></ux-icon>\n }\n </div>\n }\n </div>\n\n <div class=\"wizard-content\">\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"wizard-footer\">\n @if (footerTemplate) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate\" [ngTemplateOutletContext]=\"{ step: step }\">\n </ng-container>\n }\n\n @if (previousVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary\"\n [uxTooltip]=\"previousTooltip\"\n [disabled]=\"previousDisabled || step === 0\"\n [attr.aria-label]=\"previousAriaLabel\"\n (click)=\"previous(); tip.hide()\"\n >\n {{ previousText }}\n </button>\n }\n\n @if (nextVisible && !isLastStep()) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary\"\n [uxTooltip]=\"nextTooltip\"\n [disabled]=\"nextDisabled || isNextDisabled()\"\n [attr.aria-label]=\"nextAriaLabel\"\n (click)=\"next(); tip.hide()\"\n >\n {{ nextText }}\n </button>\n }\n\n @if ((finishVisible && isLastStep()) || finishAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary\"\n [uxTooltip]=\"finishTooltip\"\n [disabled]=\"finishDisabled || isNextDisabled()\"\n [attr.aria-label]=\"finishAriaLabel\"\n (click)=\"finish(); tip.hide()\"\n >\n {{ finishText }}\n </button>\n }\n\n @if ((cancelVisible && !isLastStep()) || cancelAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary\"\n [uxTooltip]=\"cancelTooltip\"\n [disabled]=\"cancelDisabled\"\n [attr.aria-label]=\"cancelAriaLabel\"\n (click)=\"cancel(); tip.hide()\"\n >\n {{ cancelText }}\n </button>\n }\n</div>\n" }]
}], propDecorators: { orientation: [{
type: Input
}], nextText: [{
type: Input
}], previousText: [{
type: Input
}], cancelText: [{
type: Input
}], finishText: [{
type: Input
}], nextTooltip: [{
type: Input
}], previousTooltip: [{
type: Input
}], cancelTooltip: [{
type: Input
}], finishTooltip: [{
type: Input
}], nextAriaLabel: [{
type: Input
}], previousAriaLabel: [{
type: Input
}], cancelAriaLabel: [{
type: Input
}], finishAriaLabel: [{
type: Input
}], nextDisabled: [{
type: Input
}], previousDisabled: [{
type: Input
}], cancelDisabled: [{
type: Input
}], finishDisabled: [{
type: Input
}], nextVisible: [{
type: Input
}], previousVisible: [{
type: Input
}], cancelVisible: [{
type: Input
}], finishVisible: [{
type: Input
}], cancelAlwaysVisible: [{
type: Input
}], finishAlwaysVisible: [{
type: Input
}], disableNextWhenInvalid: [{
type: Input
}], resetVisitedOnValidationError: [{
type: Input
}], sequential: [{
type: Input
}], onNext: [{
type: Output
}], onPrevious: [{
type: Output
}], onCancel: [{
type: Output
}], onFinishing: [{
type: Output
}], onFinish: [{
type: Output
}], stepChanging: [{
type: Output
}], stepChange: [{
type: Output
}], stepError: [{
type: Output
}], steps: [{
type: ContentChildren,
args: [WizardStepComponent]
}], footerTemplate: [{
type: ContentChild,
args: ['footerTemplate', { static: false }]
}], step: [{
type: Input
}] } });
class StepChangingEvent {
constructor(from, to) {
this.from = from;
this.to = to;
}
}
const DECLARATIONS$5 = [WizardComponent, WizardStepComponent];
class WizardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: WizardModule, declarations: [WizardComponent, WizardStepComponent], imports: [AccessibilityModule, CommonModule, IconModule, TooltipModule], exports: [WizardComponent, WizardStepComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardModule, imports: [AccessibilityModule, CommonModule, IconModule, TooltipModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WizardModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, IconModule, TooltipModule],
exports: DECLARATIONS$5,
declarations: DECLARATIONS$5,
}]
}] });
class MarqueeWizardStepComponent extends WizardStepComponent {
constructor() {
super(...arguments);
/** Determine the completed state of this step */
this.completed = false;
/** Emit when the completed step changes */
this.completedChange = new EventEmitter();
}
/**
* Update the completed state and emit the latest value
* @param completed whether or not the step is completed
*/
setCompleted(completed) {
this.completed = completed;
this.completedChange.emit(completed);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardStepComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MarqueeWizardStepComponent, isStandalone: false, selector: "ux-marquee-wizard-step", inputs: { context: "context", completed: "completed" }, outputs: { completedChange: "completedChange" }, queries: [{ propertyName: "_iconTemplate", first: true, predicate: MarqueeWizardStepIconDirective, descendants: true, read: TemplateRef }], usesInheritance: true, ngImport: i0, template: "@if (active) {\n <ng-content></ng-content>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardStepComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-marquee-wizard-step', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "@if (active) {\n <ng-content></ng-content>\n}\n" }]
}], propDecorators: { context: [{
type: Input
}], completed: [{
type: Input
}], completedChange: [{
type: Output
}], _iconTemplate: [{
type: ContentChild,
args: [MarqueeWizardStepIconDirective, { read: TemplateRef, static: false }]
}] } });
class MarqueeWizardComponent extends WizardComponent {
get isTemplate() {
return this.description && this.description instanceof TemplateRef;
}
constructor() {
super();
this.wizardService = inject(WizardService);
this._resizeService = inject(ResizeService);
this._elementRef = inject(ElementRef);
/** Initial set to default width to match 240px on left but can be changed with a percentage value */
this.sidePanelWidth = 25;
/** Width of the splitter - default is 10 */
this.gutterSize = 10;
/** If set to true the resizable splitter will be enabled and set to the default width **/
this.resizable = false;
/** Emit the current width of the splitter*/
this.sidePanelWidthChange = new EventEmitter();
/** Access each step content component */
this.steps = new QueryList();
/**
* If the wizard is in a modal it may initially have a size of 0 until the modal displays
* in which case if we are using the splitter it will not render correctly. We use this
* variable to only initialise the splitter when the content has a width.
*/
this._isInitialised = false;
// set to true as default for Marquee Wizard only
this.resetVisitedOnValidationError = true;
// watch for changes to the size
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(this.onResize.bind(this));
}
ngAfterViewChecked() {
this.tabbableList?.setFirstItemTabbable();
}
ngOnDestroy() {
super.ngOnDestroy();
this._resizeService.removeResizeListener(this._elementRef.nativeElement);
}
/**
* If the current step is valid, mark it as
* complete and go to the next step
*/
async next() {
// get the current step
const step = this.getCurrentStep();
await super.next();
if (step && step.valid) {
// mark this step as completed
step.setCompleted(true);
}
else {
this.stepError.next(this.step);
}
}
/**
* Emit the onFinishing event and if valid the onFinish event.
* Also mark the final step as completed if it is valid
*/
async finish() {
// get the current step
const step = this.getCurrentStep();
await super.finish();
// if the step is valid indicate that it is now complete
if (step.valid) {
step.setCompleted(true);
}
else {
this.stepError.next(this.step);
}
}
onResize(event) {
if (event.width !== 0 && event.height !== 0) {
this._isInitialised = true;
}
}
/** Whenever the drag event ends, update the internal value and emit the new size */
onDragEnd({ sizes }) {
// we need to only get the size of the first panel which will be the side panel
this.sidePanelWidth = sizes[0];
this.sidePanelWidthChange.emit(this.sidePanelWidth);
}
gotoStep(step) {
const currentStep = this.getCurrentStep();
if (currentStep !== step) {
if (!this.sequential) {
currentStep.setCompleted(true);
}
super.gotoStep(step);
}
}
setFutureStepsUnvisited(currentStep) {
super.setFutureStepsUnvisited(currentStep);
// Marquee wizard steps have an additional completed property which must also be changed.
// The base class implementation only changes the visited state
this.getFutureSteps(currentStep).forEach((step) => (step.completed = false));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MarqueeWizardComponent, isStandalone: false, selector: "ux-marquee-wizard", inputs: { description: "description", stepTemplate: "stepTemplate", sidePanelWidth: "sidePanelWidth", gutterSize: "gutterSize", resizable: "resizable" }, outputs: { sidePanelWidthChange: "sidePanelWidthChange" }, providers: [WizardService], queries: [{ propertyName: "steps", predicate: MarqueeWizardStepComponent }], viewQueries: [{ propertyName: "tabbableList", first: true, predicate: TabbableListDirective, descendants: true }], usesInheritance: true, ngImport: i0, template: "@if (resizable && _isInitialised) {\n <as-split direction=\"horizontal\" [gutterSize]=\"gutterSize\" (dragEnd)=\"onDragEnd($event)\">\n <as-split-area [size]=\"sidePanelWidth\">\n <ng-container [ngTemplateOutlet]=\"sidePanel\"></ng-container>\n </as-split-area>\n <as-split-area [size]=\"100 - sidePanelWidth\">\n <ng-container [ngTemplateOutlet]=\"mainContentPanel\"></ng-container>\n </as-split-area>\n </as-split>\n}\n\n@if (!resizable) {\n <ng-container [ngTemplateOutlet]=\"sidePanel\"></ng-container>\n <ng-container [ngTemplateOutlet]=\"mainContentPanel\"></ng-container>\n}\n\n<ng-template #sidePanel>\n <div class=\"marquee-wizard-side-panel\" [class.marquee-wizard-side-panel-resize]=\"resizable\">\n @if (description) {\n <div class=\"marquee-wizard-description-container\">\n <!-- If a template was provided display it -->\n @if (isTemplate) {\n <ng-container [ngTemplateOutlet]=\"$any(description)\"></ng-container>\n }\n <!-- Otherwise simply display the string -->\n @if (!isTemplate) {\n <p>{{ description }}</p>\n }\n </div>\n }\n\n <ul\n class=\"marquee-wizard-steps\"\n uxTabbableList\n direction=\"vertical\"\n role=\"tablist\"\n aria-orientation=\"vertical\"\n >\n @for (step of steps; track step; let index = $index) {\n <li\n role=\"tab\"\n class=\"marquee-wizard-step\"\n [class.active]=\"step.active\"\n [class.visited]=\"step.visited\"\n [class.invalid]=\"!step.valid\"\n [attr.aria-posinset]=\"index + 1\"\n [attr.aria-setsize]=\"steps.length\"\n [attr.aria-selected]=\"step.active\"\n [attr.aria-controls]=\"id + '-step-' + index\"\n [attr.aria-labelledby]=\"id + '-step-' + index + '-label'\"\n [attr.aria-expanded]=\"step._active\"\n [id]=\"id + '-step-' + index + '-label'\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n uxTabbableListItem\n [disabled]=\"!step.visited\"\n (click)=\"gotoStep(step)\"\n (keydown.enter)=\"gotoStep(step)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"stepTemplate || defaultStepTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: step, index: index, context: step.context }\"\n ></ng-container>\n </li>\n }\n </ul>\n </div>\n</ng-template>\n\n<ng-template #mainContentPanel>\n <div class=\"marquee-wizard-content-panel\" [class.marquee-wizard-content-panel-resize]=\"resizable\">\n <div class=\"marquee-wizard-content\">\n <ng-content></ng-content>\n </div>\n\n <div class=\"modal-footer\">\n @if (footerTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"footerTemplate\"\n [ngTemplateOutletContext]=\"{ step: step }\"\n >\n </ng-container>\n }\n\n @if (previousVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary marquee-wizard-previous-button\"\n [uxTooltip]=\"previousTooltip\"\n [attr.aria-label]=\"previousAriaLabel\"\n container=\"body\"\n [disabled]=\"previousDisabled || step === 0\"\n (click)=\"previous(); tip.hide()\"\n >\n {{ previousText }}\n </button>\n }\n\n @if (nextVisible && !isLastStep()) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary marquee-wizard-next-button\"\n [uxTooltip]=\"nextTooltip\"\n [attr.aria-label]=\"nextAriaLabel\"\n container=\"body\"\n [disabled]=\"nextDisabled || isNextDisabled()\"\n (click)=\"next(); tip.hide()\"\n >\n {{ nextText }}\n </button>\n }\n\n @if ((finishVisible && isLastStep()) || finishAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary marquee-wizard-finish-button\"\n [uxTooltip]=\"finishTooltip\"\n [attr.aria-label]=\"finishAriaLabel\"\n container=\"body\"\n [disabled]=\"finishDisabled || isNextDisabled()\"\n (click)=\"finish(); tip.hide()\"\n >\n {{ finishText }}\n </button>\n }\n\n @if ((cancelVisible && !isLastStep()) || cancelAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary marquee-wizard-cancel-button\"\n [uxTooltip]=\"cancelTooltip\"\n [attr.aria-label]=\"cancelAriaLabel\"\n container=\"body\"\n [disabled]=\"cancelDisabled\"\n (click)=\"cancel(); tip.hide()\"\n >\n {{ cancelText }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n\n<ng-template #defaultStepTemplate let-step>\n <!-- Insert the icon -->\n @if (step._iconTemplate) {\n <div class=\"marquee-wizard-step-icon\">\n <ng-container [ngTemplateOutlet]=\"step._iconTemplate\"></ng-container>\n </div>\n }\n\n <span class=\"marquee-wizard-step-title\">{{ step.header }}</span>\n @if (step.completed && step.valid) {\n <ux-icon class=\"marquee-wizard-step-status\" name=\"checkmark\"></ux-icon>\n }\n @if (!step.valid) {\n <ux-icon class=\"marquee-wizard-step-status\" name=\"close\"></ux-icon>\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: SplitterAccessibilityDirective, selector: "as-split", outputs: ["gutterKeydown"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "component", type: i9.SplitComponent, selector: "as-split", inputs: ["direction", "unit", "gutterSize", "gutterStep", "restrictMove", "useTransition", "disabled", "dir", "gutterDblClickDuration", "gutterClickDeltaPx", "gutterAriaLabel"], outputs: ["transitionEnd", "dragStart", "dragEnd", "gutterClick", "gutterDblClick"], exportAs: ["asSplit"] }, { kind: "directive", type: i9.SplitAreaDirective, selector: "as-split-area, [as-split-area]", inputs: ["order", "size", "minSize", "maxSize", "lockSize", "visible"], exportAs: ["asSplitArea"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-marquee-wizard', providers: [WizardService], preserveWhitespaces: false, standalone: false, template: "@if (resizable && _isInitialised) {\n <as-split direction=\"horizontal\" [gutterSize]=\"gutterSize\" (dragEnd)=\"onDragEnd($event)\">\n <as-split-area [size]=\"sidePanelWidth\">\n <ng-container [ngTemplateOutlet]=\"sidePanel\"></ng-container>\n </as-split-area>\n <as-split-area [size]=\"100 - sidePanelWidth\">\n <ng-container [ngTemplateOutlet]=\"mainContentPanel\"></ng-container>\n </as-split-area>\n </as-split>\n}\n\n@if (!resizable) {\n <ng-container [ngTemplateOutlet]=\"sidePanel\"></ng-container>\n <ng-container [ngTemplateOutlet]=\"mainContentPanel\"></ng-container>\n}\n\n<ng-template #sidePanel>\n <div class=\"marquee-wizard-side-panel\" [class.marquee-wizard-side-panel-resize]=\"resizable\">\n @if (description) {\n <div class=\"marquee-wizard-description-container\">\n <!-- If a template was provided display it -->\n @if (isTemplate) {\n <ng-container [ngTemplateOutlet]=\"$any(description)\"></ng-container>\n }\n <!-- Otherwise simply display the string -->\n @if (!isTemplate) {\n <p>{{ description }}</p>\n }\n </div>\n }\n\n <ul\n class=\"marquee-wizard-steps\"\n uxTabbableList\n direction=\"vertical\"\n role=\"tablist\"\n aria-orientation=\"vertical\"\n >\n @for (step of steps; track step; let index = $index) {\n <li\n role=\"tab\"\n class=\"marquee-wizard-step\"\n [class.active]=\"step.active\"\n [class.visited]=\"step.visited\"\n [class.invalid]=\"!step.valid\"\n [attr.aria-posinset]=\"index + 1\"\n [attr.aria-setsize]=\"steps.length\"\n [attr.aria-selected]=\"step.active\"\n [attr.aria-controls]=\"id + '-step-' + index\"\n [attr.aria-labelledby]=\"id + '-step-' + index + '-label'\"\n [attr.aria-expanded]=\"step._active\"\n [id]=\"id + '-step-' + index + '-label'\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n uxTabbableListItem\n [disabled]=\"!step.visited\"\n (click)=\"gotoStep(step)\"\n (keydown.enter)=\"gotoStep(step)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"stepTemplate || defaultStepTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: step, index: index, context: step.context }\"\n ></ng-container>\n </li>\n }\n </ul>\n </div>\n</ng-template>\n\n<ng-template #mainContentPanel>\n <div class=\"marquee-wizard-content-panel\" [class.marquee-wizard-content-panel-resize]=\"resizable\">\n <div class=\"marquee-wizard-content\">\n <ng-content></ng-content>\n </div>\n\n <div class=\"modal-footer\">\n @if (footerTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"footerTemplate\"\n [ngTemplateOutletContext]=\"{ step: step }\"\n >\n </ng-container>\n }\n\n @if (previousVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary marquee-wizard-previous-button\"\n [uxTooltip]=\"previousTooltip\"\n [attr.aria-label]=\"previousAriaLabel\"\n container=\"body\"\n [disabled]=\"previousDisabled || step === 0\"\n (click)=\"previous(); tip.hide()\"\n >\n {{ previousText }}\n </button>\n }\n\n @if (nextVisible && !isLastStep()) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary marquee-wizard-next-button\"\n [uxTooltip]=\"nextTooltip\"\n [attr.aria-label]=\"nextAriaLabel\"\n container=\"body\"\n [disabled]=\"nextDisabled || isNextDisabled()\"\n (click)=\"next(); tip.hide()\"\n >\n {{ nextText }}\n </button>\n }\n\n @if ((finishVisible && isLastStep()) || finishAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-primary marquee-wizard-finish-button\"\n [uxTooltip]=\"finishTooltip\"\n [attr.aria-label]=\"finishAriaLabel\"\n container=\"body\"\n [disabled]=\"finishDisabled || isNextDisabled()\"\n (click)=\"finish(); tip.hide()\"\n >\n {{ finishText }}\n </button>\n }\n\n @if ((cancelVisible && !isLastStep()) || cancelAlwaysVisible) {\n <button\n #tip=\"ux-tooltip\"\n type=\"button\"\n class=\"btn button-secondary marquee-wizard-cancel-button\"\n [uxTooltip]=\"cancelTooltip\"\n [attr.aria-label]=\"cancelAriaLabel\"\n container=\"body\"\n [disabled]=\"cancelDisabled\"\n (click)=\"cancel(); tip.hide()\"\n >\n {{ cancelText }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n\n<ng-template #defaultStepTemplate let-step>\n <!-- Insert the icon -->\n @if (step._iconTemplate) {\n <div class=\"marquee-wizard-step-icon\">\n <ng-container [ngTemplateOutlet]=\"step._iconTemplate\"></ng-container>\n </div>\n }\n\n <span class=\"marquee-wizard-step-title\">{{ step.header }}</span>\n @if (step.completed && step.valid) {\n <ux-icon class=\"marquee-wizard-step-status\" name=\"checkmark\"></ux-icon>\n }\n @if (!step.valid) {\n <ux-icon class=\"marquee-wizard-step-status\" name=\"close\"></ux-icon>\n }\n</ng-template>\n" }]
}], ctorParameters: () => [], propDecorators: { tabbableList: [{
type: ViewChild,
args: [TabbableListDirective]
}], description: [{
type: Input
}], stepTemplate: [{
type: Input
}], sidePanelWidth: [{
type: Input
}], gutterSize: [{
type: Input
}], resizable: [{
type: Input
}], sidePanelWidthChange: [{
type: Output
}], steps: [{
type: ContentChildren,
args: [MarqueeWizardStepComponent]
}] } });
class MarqueeWizardModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardModule, declarations: [MarqueeWizardComponent,
MarqueeWizardStepComponent,
MarqueeWizardStepIconDirective], imports: [AccessibilityModule,
CommonModule,
IconModule,
TooltipModule,
WizardModule,
AngularSplitModule,
ResizeModule], exports: [MarqueeWizardComponent, MarqueeWizardStepComponent, MarqueeWizardStepIconDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardModule, imports: [AccessibilityModule,
CommonModule,
IconModule,
TooltipModule,
WizardModule,
AngularSplitModule,
ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MarqueeWizardModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
IconModule,
TooltipModule,
WizardModule,
AngularSplitModule,
ResizeModule,
],
exports: [MarqueeWizardComponent, MarqueeWizardStepComponent, MarqueeWizardStepIconDirective],
declarations: [
MarqueeWizardComponent,
MarqueeWizardStepComponent,
MarqueeWizardStepIconDirective,
],
}]
}] });
class FrameExtractionService {
createVideoPlayer(source) {
const videoPlayer = document.createElement('video');
videoPlayer.preload = 'auto';
videoPlayer.src = source;
return videoPlayer;
}
createCanvas(width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
goToFrame(videoPlayer, time) {
videoPlayer.currentTime = time;
return fromEvent(videoPlayer, time === 0 ? 'loadeddata' : 'seeked');
}
getThumbnail(videoPlayer, canvas, time, width = 160, height = 90) {
return Observable.create((observer) => {
// go to specified frame
const subscription = this.goToFrame(videoPlayer, time).subscribe(() => {
// create image from current frame
canvas.getContext('2d').drawImage(videoPlayer, 0, 0, width, height);
observer.next({ image: canvas.toDataURL(), width, height, time });
observer.complete();
subscription.unsubscribe();
});
});
}
getFrameThumbnail(source, width, height, time) {
// create required elements
let videoPlayer = this.createVideoPlayer(source);
let canvas = this.createCanvas(width, height);
const frameSubscription = this.getThumbnail(videoPlayer, canvas, time, width, height);
// ensure we release memory after we are finished
frameSubscription.subscribe(null, null, () => {
videoPlayer = null;
canvas = null;
});
return frameSubscription;
}
getFrameThumbnails(source, width, height, start, end, skip = 5) {
// create required elements
let videoPlayer = this.createVideoPlayer(source);
let canvas = this.createCanvas(width, height);
return Observable.create((observer) => {
fromEvent(videoPlayer, 'loadedmetadata').subscribe(() => {
// calculate the frames required
const frames = [];
for (let idx = start; idx < end; idx += skip) {
frames.push(this.getThumbnail(videoPlayer, canvas, idx, width, height));
}
concat(...frames).subscribe((frame) => observer.next(frame), null, () => {
videoPlayer = null;
canvas = null;
observer.complete();
});
});
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FrameExtractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FrameExtractionService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FrameExtractionService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class MediaPlayerService {
constructor() {
this._frameExtractionService = inject(FrameExtractionService);
this.type = 'video';
this.loaded = false;
/** Aria Labels */
this.muteAriaLabel = this.getMuteAriaLabel;
this.playAriaLabel = this.getPlayAriaLabel;
this.fullscreenAriaLabel = this.getFullscreenAriaLabel;
this.selectSubtitlesAriaLabel = this.getSubtitlesAriaLabel;
this.goToStartAriaLabel = 'Go to start';
this.goToEndAriaLabel = 'Go to end';
this.subtitlesTitleAriaLabel = 'Subtitles';
this.subtitlesOffAriaLabel = 'Subtitles Off';
this.noSubtitlesAriaLabel = 'No subtitles';
this.mediaPlayerAriaLabel = 'Media Player';
this.seekAriaLabel = 'Seek Slider';
/*
Create observables for media player events
*/
this.playing = new BehaviorSubject(false);
this.initEvent = new ReplaySubject();
this.abortEvent = new Subject();
this.canPlayEvent = new BehaviorSubject(false);
this.canPlayThroughEvent = new BehaviorSubject(false);
this.durationChangeEvent = new Subject();
this.endedEvent = new Subject();
this.errorEvent = new Subject();
this.loadedDataEvent = new Subject();
this.loadedMetadataEvent = new Subject();
this.loadStartEvent = new Subject();
this.pauseEvent = new Subject();
this.playEvent = new Subject();
this.playingEvent = new Subject();
this.rateChangeEvent = new Subject();
this.seekedEvent = new Subject();
this.seekingEvent = new Subject();
this.stalledEvent = new Subject();
this.suspendEvent = new Subject();
this.timeUpdateEvent = new Subject();
this.volumeChangeEvent = new Subject();
this.waitingEvent = new Subject();
this.mediaClickEvent = new Subject();
this.fullscreenEvent = new BehaviorSubject(false);
this.quietModeEvent = new BehaviorSubject(false);
this.progressEvent = Observable.create((observer) => {
// repeat until the whole video has fully loaded
const interval = setInterval(() => {
const buffered = this._mediaPlayer.buffered;
observer.next(buffered);
if (buffered.length === 1 && buffered.start(0) === 0 && buffered.end(0) === this.duration) {
observer.complete();
clearInterval(interval);
}
}, 1000);
});
this._fullscreen = false;
}
/*
Create all the getters and setters the can be used by media player extensions
*/
get mediaPlayer() {
return this._mediaPlayer;
}
get quietMode() {
return this._quietMode;
}
set quietMode(value) {
// quiet mode cannot be enabled on audio player
if (this.type === 'audio') {
value = false;
}
this._quietMode = value;
this.quietModeEvent.next(value);
}
get mediaPlayerWidth() {
return this._mediaPlayer ? this._mediaPlayer.offsetWidth : 0;
}
get mediaPlayerHeight() {
return this._mediaPlayer ? this._mediaPlayer.offsetHeight : 0;
}
get autoplay() {
return this._mediaPlayer ? this._mediaPlayer.autoplay : false;
}
set autoplay(value) {
this._mediaPlayer.autoplay = value;
}
get buffered() {
return this._mediaPlayer ? this._mediaPlayer.buffered : new TimeRanges();
}
get crossOrigin() {
return this._mediaPlayer ? this._mediaPlayer.crossOrigin : null;
}
set crossOrigin(value) {
this._mediaPlayer.crossOrigin = value;
}
get currentSrc() {
return this._mediaPlayer ? this._mediaPlayer.currentSrc : null;
}
get currentTime() {
return this._mediaPlayer ? this._mediaPlayer.currentTime : 0;
}
set currentTime(value) {
this._mediaPlayer.currentTime = value;
}
get defaultMuted() {
return this._mediaPlayer ? this._mediaPlayer.defaultMuted : false;
}
set defaultMuted(value) {
this._mediaPlayer.defaultMuted = value;
}
get defaultPlaybackRate() {
return this._mediaPlayer ? this._mediaPlayer.defaultPlaybackRate : 1;
}
set defaultPlaybackRate(value) {
this._mediaPlayer.defaultPlaybackRate = value;
}
get duration() {
return this._mediaPlayer && !isNaN(this.mediaPlayer.duration) ? this._mediaPlayer.duration : 0;
}
get ended() {
return this._mediaPlayer ? this._mediaPlayer.ended : false;
}
get loop() {
return this._mediaPlayer ? this._mediaPlayer.loop : false;
}
set loop(value) {
this._mediaPlayer.loop = value;
}
get muted() {
return this._mediaPlayer ? this._mediaPlayer.muted : false;
}
set muted(value) {
this._mediaPlayer.muted = value;
}
get networkState() {
return this._mediaPlayer.networkState;
}
get paused() {
return this._mediaPlayer ? this._mediaPlayer.paused : true;
}
get playbackRate() {
return this._mediaPlayer ? this._mediaPlayer.playbackRate : 1;
}
set playbackRate(value) {
this._mediaPlayer.playbackRate = value;
}
get played() {
return this._mediaPlayer ? this._mediaPlayer.played : new TimeRanges();
}
get preload() {
return this._mediaPlayer ? this._mediaPlayer.preload : 'auto';
}
set preload(value) {
this._mediaPlayer.preload = value;
}
get readyState() {
return this._mediaPlayer ? this._mediaPlayer.readyState : 0;
}
get seekable() {
return this._mediaPlayer ? this._mediaPlayer.seekable : new TimeRanges();
}
get seeking() {
return this._mediaPlayer ? this._mediaPlayer.seeking : false;
}
get src() {
return this._mediaPlayer ? this._mediaPlayer.src : '';
}
set src(value) {
this._mediaPlayer.src = value;
}
get textTracks() {
return this._mediaPlayer ? Array.from(this._mediaPlayer.textTracks) : [];
}
get volume() {
return this._mediaPlayer ? this._mediaPlayer.volume : 1;
}
set volume(value) {
if (this._mediaPlayer) {
this._mediaPlayer.volume = value;
}
}
get fullscreen() {
return this._mediaPlayer ? this._fullscreen : false;
}
set fullscreen(value) {
this._fullscreen = value;
this.fullscreenEvent.next(value);
}
setMediaPlayer(hostElement, mediaPlayer) {
this._hostElement = hostElement;
this._mediaPlayer = mediaPlayer;
this.initEvent.next(true);
}
/**
* Toggle playing state
*/
togglePlay() {
// prevent any action is not loaded
if (this.loaded === false) {
return;
}
if (this.paused) {
this.play();
}
else {
this.pause();
}
}
/**
* Starts playing the audio/video
*/
play() {
this._mediaPlayer.play();
}
/**
* Pauses the currently playing audio/video
*/
pause() {
this._mediaPlayer.pause();
}
/**
* Re-loads the audio/video element
*/
load() {
this._mediaPlayer.load();
}
/**
* Checks if the browser can play the specified audio/video type
*/
canPlayType(type) {
return this._mediaPlayer.canPlayType(type);
}
/**
* Adds a new text track to the audio/video
*/
addTextTrack(kind, label, language) {
return this._mediaPlayer.addTextTrack(kind, label, language);
}
/**
* Attempt to display media in fullscreen mode
*/
requestFullscreen() {
// get the host element (we need to do some browser specific checks and typescript complains)
const host = this._hostElement;
const requestFullscreen = host.requestFullscreen ||
host.webkitRequestFullscreen ||
host.msRequestFullscreen ||
host.mozRequestFullScreen;
// if we can perform the action then perform it and update the state
if (requestFullscreen) {
requestFullscreen.call(host);
// update the internal state
this.fullscreen = true;
}
}
/**
* Exit full screen mode
*/
exitFullscreen() {
// get the document element (we need to do some browser specific checks and typescript complains)
const host = document;
const exitFullscreen = host.exitFullscreen ||
host.webkitExitFullscreen ||
host.msExitFullscreen ||
host.mozCancelFullScreen;
// if we can perform the action then perform it and update the state
if (exitFullscreen) {
exitFullscreen.call(host);
// update the internal state
this.fullscreen = false;
}
}
/**
* Toggle Fullscreen State
*/
toggleFullscreen() {
if (this.fullscreen) {
this.exitFullscreen();
}
else {
this.requestFullscreen();
}
}
fullscreenChange() {
// get the document element (we need to do some browser specific checks and typescript complains)
const host = document;
// set the fullscreen state (this also emits the event)
this.fullscreen =
host.fullscreen ||
host.webkitIsFullScreen ||
host.mozFullScreen ||
(host.msFullscreenElement !== null && host.msFullscreenElement !== undefined);
}
/**
* Extract the frames from the video
*/
getFrames(width, height) {
if (this.type === 'video') {
return this._frameExtractionService.getFrameThumbnails(this.source, width, height, 0, this.duration, 10);
}
return from([]);
}
hideSubtitleTracks() {
for (let index = 0; index < this.textTracks.length; index++) {
this.textTracks[index].mode = 'hidden';
}
}
getMuteAriaLabel(volume) {
return volume === 0 ? 'Unmute' : 'Mute';
}
getPlayAriaLabel(isPlaying) {
return isPlaying ? 'Pause' : 'Play';
}
getFullscreenAriaLabel(isFullscreen) {
return isFullscreen ? 'Exit full screen' : 'Full screen';
}
getSubtitlesAriaLabel(track) {
return `Select subtitles, ${track} currently selected.`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerService, decorators: [{
type: Injectable
}] });
class MediaPlayerBaseExtensionDirective {
constructor() {
this.mediaPlayerService = inject(MediaPlayerService);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerBaseExtensionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MediaPlayerBaseExtensionDirective, isStandalone: false, selector: "[mediaPlayerBaseExtension]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerBaseExtensionDirective, decorators: [{
type: Directive,
args: [{
selector: '[mediaPlayerBaseExtension]',
standalone: false,
}]
}] });
class SliderComponent {
/** A set of options to customize the appearance and behavior of the slider. */
set options(options) {
this._options = options;
this.updateOptions();
}
/** Whether the slider is disabled. */
set disabled(disabled) {
this._disabled = coerceBooleanProperty(disabled);
}
get disabled() {
return this._disabled;
}
get options() {
return this._options;
}
constructor() {
this.colorService = inject(ColorService);
this._changeDetectorRef = inject(ChangeDetectorRef);
/** A single number or a SliderValue object, depending on the slider type specified. */
this.value = 0;
/** Emits when the `value` changes. */
this.valueChange = new EventEmitter();
this._disabled = false;
// expose enums to Angular view
this.sliderType = SliderType;
this.sliderStyle = SliderStyle;
this.sliderSize = SliderSize;
this.sliderSnap = SliderSnap;
this.sliderThumb = SliderThumb;
this.sliderTickType = SliderTickType;
this.sliderThumbEvent = SliderThumbEvent;
this.sliderCalloutTrigger = SliderCalloutTrigger;
this.tracks = {
lower: {
size: 0,
color: '',
},
middle: {
size: 0,
color: '',
},
upper: {
size: 0,
color: '',
},
};
this.tooltips = {
lower: {
visible: false,
position: 0,
label: '',
},
upper: {
visible: false,
position: 0,
label: '',
},
};
this.thumbs = {
lower: {
hover: false,
drag: false,
position: 0,
order: 100,
value: null,
},
upper: {
hover: false,
drag: false,
position: 0,
order: 101,
value: null,
},
};
// store all the ticks to display
this.ticks = [];
// setup default options
this.defaultOptions = {
type: SliderType.Value,
handles: {
style: SliderStyle.Button,
callout: {
trigger: SliderCalloutTrigger.None,
background: this.colorService.getColor('grey2').toHex(),
color: '#fff',
formatter: (value) => value,
},
keyboard: {
major: 5,
minor: 1,
},
aria: {
thumb: 'Slider value',
lowerThumb: 'Slider lower value',
upperThumb: 'Slider upper value',
},
},
track: {
height: SliderSize.Wide,
min: 0,
max: 100,
ticks: {
snap: SliderSnap.None,
major: {
show: true,
steps: 10,
labels: true,
formatter: (value) => value,
},
minor: {
show: true,
steps: 5,
labels: false,
formatter: (value) => value,
},
},
colors: {},
},
};
}
ngOnInit() {
this.updateValues();
this.setThumbState(SliderThumb.Lower, false, false);
this.setThumbState(SliderThumb.Upper, false, false);
// emit the initial value
this.valueChange.next(this.clone(this.value));
}
ngDoCheck() {
if (this.detectValueChange(this.value, this._value)) {
this.updateValues();
this._value = this.clone(this.value);
}
}
ngAfterViewInit() {
// persistent tooltips will need positioned correctly at this stage
setTimeout(() => {
this.updateTooltipPosition(SliderThumb.Lower);
this.updateTooltipPosition(SliderThumb.Upper);
// mark as dirty
this._changeDetectorRef.markForCheck();
});
this.updateOrder();
}
snapToNearestTick(thumb, snapTarget, forwards) {
if (this.disabled) {
return;
}
// get the value for the thumb
const { value } = this.getThumbState(thumb);
// get the closest ticks - remove any tick if we are currently on it
const closest = this.getTickDistances(value, thumb, snapTarget)
.filter(tick => tick.value !== value)
.find(tick => (forwards ? tick.value > value : tick.value < value));
// If we have no ticks then move by a predefined amount
if (closest) {
return this.setThumbValue(thumb, this.validateValue(thumb, closest.value));
}
const step = snapTarget === SliderSnap.Major
? this.options.handles.keyboard.major
: this.options.handles.keyboard.minor;
this.setThumbValue(thumb, this.validateValue(thumb, value + (forwards ? step : -step)));
}
snapToEnd(thumb, forwards) {
this.setThumbValue(thumb, this.validateValue(thumb, forwards ? this.options.track.max : this.options.track.min));
}
getThumbValue(thumb) {
return this.getThumbState(thumb).value;
}
getFormattedValue(thumb) {
return this.options.handles.callout.formatter(this.getThumbState(thumb).value);
}
getThumbState(thumb) {
return thumb === SliderThumb.Lower ? this.thumbs.lower : this.thumbs.upper;
}
setThumbState(thumb, hover, drag) {
if (thumb === SliderThumb.Lower) {
this.thumbs.lower.hover = hover;
this.thumbs.lower.drag = drag;
}
else {
this.thumbs.upper.hover = hover;
this.thumbs.upper.drag = drag;
}
// update the visibility of the tooltips
this.updateTooltips(thumb);
}
thumbEvent(thumb, event) {
if (this.disabled) {
return;
}
// get the current thumb state
const state = this.getThumbState(thumb);
// update based upon event
switch (event) {
case SliderThumbEvent.DragStart:
state.drag = true;
break;
case SliderThumbEvent.DragEnd:
state.drag = false;
break;
case SliderThumbEvent.MouseOver:
state.hover = true;
break;
case SliderThumbEvent.MouseLeave:
state.hover = false;
break;
case SliderThumbEvent.None:
state.drag = false;
state.hover = false;
break;
}
// update the thumb state
this.setThumbState(thumb, state.hover, state.drag);
this.updateOrder();
}
getAriaValueText(thumb) {
// get the current thumb value
const value = this.getThumbValue(thumb);
// get all the ticks
const tick = this.ticks.find(_tick => _tick.value === value);
if (tick && tick.label) {
return tick.label;
}
// otherwise simply display the formatted value
return this.getFormattedValue(thumb);
}
updateTooltips(thumb) {
let visible = false;
const state = this.getThumbState(thumb);
switch (this.options.handles.callout.trigger) {
case SliderCalloutTrigger.Persistent:
visible = true;
break;
case SliderCalloutTrigger.Drag:
visible = state.drag;
break;
case SliderCalloutTrigger.Hover:
visible = state.hover || state.drag;
break;
case SliderCalloutTrigger.Dynamic:
visible = true;
break;
}
// update the state for the corresponding thumb
this.getTooltip(thumb).visible = visible;
// update the tooltip text
this.updateTooltipText(thumb);
// update the tooltip positions
this.updateTooltipPosition(thumb);
}
updateTooltipText(thumb) {
// get the thumb value
const tooltip = this.getTooltip(thumb);
// store the formatted label
tooltip.label = this.getFormattedValue(thumb).toString();
}
getTooltipElement(thumb) {
return thumb === SliderThumb.Lower ? this.lowerTooltip : this.upperTooltip;
}
getTooltip(thumb) {
return thumb === SliderThumb.Lower ? this.tooltips.lower : this.tooltips.upper;
}
updateTooltipPosition(thumb) {
const tooltip = this.getTooltip(thumb);
// if tooltip is not visible then stop here
if (tooltip.visible === false) {
return;
}
const tooltipElement = this.getTooltipElement(thumb);
// get the element widths
let thumbWidth;
if (this._options.handles.style === SliderStyle.Button) {
thumbWidth = this._options.track.height === SliderSize.Narrow ? 16 : 24;
}
else {
thumbWidth = 2;
}
const tooltipWidth = tooltipElement.nativeElement.offsetWidth;
// calculate the tooltips new position
const tooltipPosition = Math.ceil((tooltipWidth - thumbWidth) / 2);
// update tooltip position
tooltip.position = -tooltipPosition;
if (this._options.type === SliderType.Range &&
this._options.handles.callout.trigger === SliderCalloutTrigger.Dynamic) {
this.preventTooltipOverlap(tooltip);
}
}
preventTooltipOverlap(tooltip) {
const trackWidth = this.track.nativeElement.offsetWidth;
const lower = (trackWidth / 100) * this.thumbs.lower.position;
const upper = (trackWidth / 100) * this.thumbs.upper.position;
const lowerWidth = this.lowerTooltip.nativeElement.offsetWidth / 2;
const upperWidth = this.upperTooltip.nativeElement.offsetWidth / 2;
const diff = lower + lowerWidth - (upper - upperWidth);
// if the tooltips are closer than 16px then adjust so the dont move any close
if (diff > 0) {
if (tooltip === this.tooltips.lower && this.thumbs.lower.drag === false) {
tooltip.position -= diff / 2;
}
else if (tooltip === this.tooltips.upper && this.thumbs.upper.drag === false) {
tooltip.position += diff / 2;
}
}
}
clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
updateThumbPosition(event, thumb) {
if (this.disabled) {
return;
}
// get event position - either mouse or touch
const eventPosition = event instanceof MouseEvent
? event.clientX
: event.touches && event.touches.length > 0
? event.touches[0].clientX
: null;
// if event position is null do nothing
if (eventPosition === null) {
return;
}
// get mouse position
const mouseX = window.pageXOffset + eventPosition;
// get track size and position
const trackBounds = this.track.nativeElement.getBoundingClientRect();
// restrict the value within the range size
const position = this.clamp(mouseX - trackBounds.left, 0, trackBounds.width);
// get fraction representation of location within the track
const fraction = position / trackBounds.width;
// convert to value within the range
let value = (this._options.track.max - this._options.track.min) * fraction + this._options.track.min;
// ensure value is valid
value = this.validateValue(thumb, value);
// snap to a tick if required
value = this.snapToTick(value, thumb);
// update the value accordingly
this.setThumbValue(thumb, value);
this.updateOrderOnDrag(thumb);
this.updateValues();
// update tooltip text & position
this.updateTooltipText(thumb);
// update the position of all visible tooltips
this.updateTooltipPosition(SliderThumb.Lower);
this.updateTooltipPosition(SliderThumb.Upper);
// mark as dirty for change detection
this._changeDetectorRef.markForCheck();
}
updateOrderOnDrag(thumb) {
const lower = thumb === SliderThumb.Lower ? 101 : 100;
const upper = thumb === SliderThumb.Lower ? 100 : 101;
// Ensure currently dragged thumb is on top
this.thumbs.lower.order = lower;
this.thumbs.upper.order = upper;
}
updateOrder() {
const isDragged = this.thumbs.lower.drag || this.thumbs.upper.drag;
if (this._options && !isDragged) {
const lowerValue = this.getThumbValue(this.sliderThumb.Lower);
const upperValue = this.getThumbValue(this.sliderThumb.Upper);
const max = this._options.track.max;
const min = this._options.track.min;
const range = max - min;
const median = (range / 100) * 50 + min;
if (upperValue <= median) {
this.thumbs.lower.order = 100;
this.thumbs.upper.order = 101;
}
if (lowerValue > median) {
this.thumbs.lower.order = 101;
this.thumbs.upper.order = 100;
}
}
}
getTickDistances(value, thumb, snapTarget) {
// if snap target is none then return original value
if (snapTarget === SliderSnap.None) {
return [];
}
// get filtered ticks
let ticks;
switch (snapTarget) {
case SliderSnap.Minor:
ticks = this.ticks.filter(tick => tick.type === SliderTickType.Minor);
break;
case SliderSnap.Major:
ticks = this.ticks.filter(tick => tick.type === SliderTickType.Major);
break;
default:
ticks = this.ticks.slice(0);
}
// get the track limit
let lowerLimit = this._options.track.min;
let upperLimit = this._options.track.max;
if (this._options.type === SliderType.Range && thumb === SliderThumb.Lower) {
upperLimit = this.thumbs.upper.value;
}
if (this._options.type === SliderType.Range && thumb === SliderThumb.Upper) {
lowerLimit = this.thumbs.lower.value;
}
// Find the closest tick to the current position
const range = ticks.filter(tick => tick.value >= lowerLimit && tick.value <= upperLimit);
// If there are no close ticks in the valid range then dont snap
if (range.length === 0) {
return [];
}
return range.sort((tickOne, tickTwo) => {
const tickOneDelta = Math.max(tickOne.value, value) - Math.min(tickOne.value, value);
const tickTwoDelta = Math.max(tickTwo.value, value) - Math.min(tickTwo.value, value);
return tickOneDelta - tickTwoDelta;
});
}
snapToTick(value, thumb) {
const tickDistances = this.getTickDistances(value, thumb, this._options.track.ticks.snap);
// if there are no ticks return the current value
if (tickDistances.length === 0) {
return value;
}
// get the closest tick
return tickDistances[0].value;
}
validateValue(thumb, value) {
// if slider is not a range value is always valid providing it is within the chart min and max values
if (this._options.type === SliderType.Value) {
return Math.max(Math.min(value, this._options.track.max), this._options.track.min);
}
// check if value is with chart ranges
if (value > this._options.track.max) {
return thumb === SliderThumb.Lower
? Math.min(this._options.track.max, this.thumbs.upper.value)
: this._options.track.max;
}
if (value < this._options.track.min) {
return thumb === SliderThumb.Upper
? Math.max(this._options.track.min, this.thumbs.lower.value)
: this._options.track.min;
}
// otherwise we need to check to make sure lower thumb cannot go above higher and vice versa
if (thumb === SliderThumb.Lower) {
if (this.thumbs.upper.value === null) {
return value;
}
return value <= this.thumbs.upper.value ? value : this.thumbs.upper.value;
}
if (thumb === SliderThumb.Upper) {
if (this.thumbs.lower.value === null) {
return value;
}
return value >= this.thumbs.lower.value ? value : this.thumbs.lower.value;
}
}
updateOptions() {
// add in the default options that user hasn't specified
this._options = this.deepMerge(this._options || {}, this.defaultOptions);
this.updateTrackColors();
this.updateTicks();
this.updateValues();
}
updateValues() {
if (this.value === undefined || this.value === null) {
this.value = 0;
}
let lowerValue = typeof this.value === 'number' ? this.value : this.value.low;
let upperValue = typeof this.value === 'number' ? this.value : this.value.high;
// validate values
lowerValue = this.validateValue(SliderThumb.Lower, Number(lowerValue.toFixed(4)));
upperValue = this.validateValue(SliderThumb.Upper, Number(upperValue.toFixed(4)));
// calculate the positions as percentages
const lowerPosition = ((lowerValue - this._options.track.min) /
(this._options.track.max - this._options.track.min)) *
100;
const upperPosition = ((upperValue - this._options.track.min) /
(this._options.track.max - this._options.track.min)) *
100;
// update thumb positions
this.thumbs.lower.position = lowerPosition;
this.thumbs.upper.position = upperPosition;
// calculate the track sizes
this.tracks.lower.size = lowerPosition;
this.tracks.middle.size = upperPosition - lowerPosition;
this.tracks.upper.size =
this._options.type === SliderType.Value ? 100 - lowerPosition : 100 - upperPosition;
// update the value input
this.setValue(lowerValue, upperValue);
}
setValue(low, high) {
this.thumbs.lower.value = low;
this.thumbs.upper.value = high;
const previousValue = this.clone(this._value);
this.value = this._options.type === SliderType.Value ? low : { low, high };
// call the event emitter if changes occured
if (this.detectValueChange(this.value, previousValue)) {
this.valueChange.emit(this.clone(this.value));
this.updateTooltipText(SliderThumb.Lower);
this.updateTooltipText(SliderThumb.Upper);
}
else {
this.valueChange.emit(this.clone(this.value));
}
}
setThumbValue(thumb, value) {
// update the thumb value
this.getThumbState(thumb).value = value;
// forward these changes to the value
this.setValue(this.thumbs.lower.value, this.thumbs.upper.value);
}
updateTicks() {
// get tick options
const majorOptions = this._options.track.ticks.major;
const minorOptions = this._options.track.ticks.minor;
// check if we should show ticks
if (majorOptions.show === false && minorOptions.show === false) {
this.ticks = [];
}
// create ticks for both major and minor - only get the ones to be shown
const majorTicks = this.getTicks(majorOptions, SliderTickType.Major).filter(tick => tick.showTicks);
const minorTicks = this.getTicks(minorOptions, SliderTickType.Minor).filter(tick => tick.showTicks);
// remove any minor ticks that are on a major interval
this.ticks = this.unionTicks(majorTicks, minorTicks);
}
updateTrackColors() {
// get colors for each part of the track
const { lower, range, higher } = this._options.track.colors;
// update the controller value
this.tracks.lower.color = this.getTrackColorStyle(lower);
this.tracks.middle.color = this.getTrackColorStyle(range);
this.tracks.upper.color = this.getTrackColorStyle(higher);
}
/** Map the color value to the correct CSS color value */
getTrackColorStyle(color) {
return Array.isArray(color) ? `linear-gradient(to right, ${color.join(', ')})` : color;
}
getSteps(steps) {
// if they are already an array just return it
if (steps instanceof Array) {
return steps;
}
const output = [];
// otherwise calculate the steps
for (let idx = this._options.track.min; idx <= this._options.track.max; idx += steps) {
output.push(idx);
}
return output;
}
getTicks(options, type) {
// create an array to store the ticks and step points
const steps = this.getSteps(options.steps);
// get some chart options
const min = this._options.track.min;
const max = this._options.track.max;
// convert each step to a slider tick and remove invalid ticks
return steps
.map(step => {
return {
showTicks: options.show,
showLabels: options.labels,
type,
position: ((step - min) / (max - min)) * 100,
value: step,
label: options.formatter(step),
};
})
.filter(tick => tick.position >= 0 && tick.position <= 100);
}
unionTicks(majorTicks, minorTicks) {
// get all ticks combined removing any minor ticks with the same value as major ticks
return majorTicks
.concat(minorTicks)
.filter((tick, index, array) => tick.type === SliderTickType.Major ||
!array.find(tk => tk.type === SliderTickType.Major && tk.position === tick.position))
.sort((t1, t2) => t1.value - t2.value);
}
deepMerge(destination, source) {
// loop though all of the properties in the source object
for (const prop in source) {
// check if the destination object has the property
// eslint-disable-next-line no-prototype-builtins
if (!destination.hasOwnProperty(prop)) {
// copy the property across
destination[prop] = source[prop];
continue;
}
// if the property exists and is not an object then skip
if (typeof destination[prop] !== 'object') {
continue;
}
// check if property is an array
if (destination[prop] instanceof Array) {
continue;
}
// if it is an object then perform a recursive check
destination[prop] = this.deepMerge(destination[prop], source[prop]);
}
return destination;
}
detectValueChange(value1, value2) {
// compare two slider values
if (this.isSliderValue(value1) && this.isSliderValue(value2)) {
// references to the objects in the correct types
const obj1 = value1;
const obj2 = value2;
return obj1.low !== obj2.low || obj1.high !== obj2.high;
}
// if not a slider value - should be number of nullable type - compare normally
return value1 !== value2;
}
/**
* Determines whether or not an object conforms to the
* SliderValue interface.
* @param value - The object to check - this must be type any
*/
isSliderValue(value) {
// check if is an object
if (typeof value !== 'object') {
return false;
}
// next check if it contains the necessary properties
return 'low' in value && 'high' in value;
}
clone(value) {
// if it is not an object simply return the value
if (typeof value !== 'object') {
return value;
}
// create a new object from the existing one
const instance = { ...value };
// delete remove the value from the old object
value = undefined;
// return the new instance of the object
return instance;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SliderComponent, isStandalone: false, selector: "ux-slider", inputs: { value: "value", options: "options", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.disabled": "disabled" } }, viewQueries: [{ propertyName: "lowerTooltip", first: true, predicate: ["lowerTooltip"], descendants: true, static: true }, { propertyName: "upperTooltip", first: true, predicate: ["upperTooltip"], descendants: true, static: true }, { propertyName: "track", first: true, predicate: ["track"], descendants: true, static: true }], ngImport: i0, template: "<div\n class=\"track\"\n #track\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n [class.range]=\"_options.type === sliderType.Range\"\n>\n <!-- Section Beneath Lower Thumb -->\n <div\n class=\"track-section track-lower\"\n [style.flex-grow]=\"tracks.lower.size\"\n [style.background]=\"this.disabled ? null : tracks.lower.color\"\n ></div>\n\n <!-- Lower Thumb Button / Line -->\n <div\n class=\"thumb lower\"\n uxDrag\n uxFocusIndicator\n role=\"slider\"\n [tabindex]=\"disabled ? -1 : 0\"\n #lowerthumb\n [attr.aria-label]=\"\n _options.type === sliderType.Range\n ? _options.handles.aria.lowerThumb\n : _options.handles.aria.thumb\n \"\n [attr.aria-valuemin]=\"_options?.track?.min\"\n [attr.aria-valuemax]=\"\n _options.type === sliderType.Range ? getThumbValue(sliderThumb.Upper) : _options?.track?.max\n \"\n [attr.aria-valuenow]=\"getThumbValue(sliderThumb.Lower)\"\n [attr.aria-valuetext]=\"getAriaValueText(sliderThumb.Lower)\"\n [style.left.%]=\"thumbs.lower.position\"\n [class.active]=\"thumbs.lower.drag\"\n [style.z-index]=\"thumbs.lower.order\"\n [class.button]=\"_options.handles.style === sliderStyle.Button\"\n [class.line]=\"_options.handles.style === sliderStyle.Line\"\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n (onDragStart)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.DragStart); lowerthumb.focus()\"\n (onDrag)=\"updateThumbPosition($event, sliderThumb.Lower)\"\n (onDragEnd)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.DragEnd)\"\n (mouseenter)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseOver)\"\n (mouseleave)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseLeave)\"\n (focus)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseOver)\"\n (blur)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseLeave)\"\n (keydown.ArrowLeft)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowRight)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.ArrowUp)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowDown)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.PageDown)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.Major, false); $event.preventDefault()\n \"\n (keydown.PageUp)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.Major, true); $event.preventDefault()\n \"\n (keydown.Home)=\"snapToEnd(sliderThumb.Lower, false); $event.preventDefault()\"\n (keydown.End)=\"snapToEnd(sliderThumb.Lower, true); $event.preventDefault()\"\n >\n <!-- Lower Thumb Callout -->\n <div\n class=\"tooltip top tooltip-lower\"\n #lowerTooltip\n [class.tooltip-dynamic]=\"\n _options.handles.callout.trigger === sliderCalloutTrigger.Dynamic &&\n thumbs.lower.drag === false\n \"\n [style.opacity]=\"tooltips.lower.visible ? 1 : 0\"\n [style.left.px]=\"tooltips.lower.position\"\n >\n <div\n class=\"tooltip-arrow\"\n [style.border-top-color]=\"_options.handles.callout.background\"\n ></div>\n\n <div\n class=\"tooltip-inner\"\n [style.background-color]=\"_options.handles.callout.background\"\n [style.color]=\"_options.handles.callout.color\"\n >\n {{ tooltips.lower.label }}\n </div>\n </div>\n </div>\n\n <!-- Section of Track Between Lower and Upper Thumbs -->\n @if (_options.type === sliderType.Range) {\n <div\n class=\"track-section track-range\"\n [style.flex-grow]=\"tracks.middle.size\"\n [style.background]=\"this.disabled ? null : tracks.middle.color\"\n ></div>\n }\n\n <!-- Upper Thumb Button / Line -->\n <div\n class=\"thumb upper\"\n uxDrag\n uxFocusIndicator\n role=\"slider\"\n [tabindex]=\"disabled ? -1 : 0\"\n #upperthumb\n [attr.aria-label]=\"_options.handles.aria.upperThumb\"\n [attr.aria-valuemin]=\"getThumbValue(sliderThumb.Lower) || _options?.track?.min\"\n [attr.aria-valuemax]=\"_options?.track?.max\"\n [attr.aria-valuenow]=\"getThumbValue(sliderThumb.Upper)\"\n [attr.aria-valuetext]=\"getAriaValueText(sliderThumb.Upper)\"\n [hidden]=\"_options.type !== sliderType.Range\"\n [class.active]=\"thumbs.upper.drag\"\n [style.left.%]=\"thumbs.upper.position\"\n [style.z-index]=\"thumbs.upper.order\"\n [class.button]=\"_options.handles.style === sliderStyle.Button\"\n [class.line]=\"_options.handles.style === sliderStyle.Line\"\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n (onDragStart)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.DragStart); upperthumb.focus()\"\n (onDrag)=\"updateThumbPosition($event, sliderThumb.Upper)\"\n (onDragEnd)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.DragEnd)\"\n (mouseenter)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseOver)\"\n (mouseleave)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseLeave)\"\n (focus)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseOver)\"\n (blur)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseLeave)\"\n (keydown.ArrowLeft)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowRight)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.ArrowUp)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowDown)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.PageDown)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.Major, false); $event.preventDefault()\n \"\n (keydown.PageUp)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.Major, true); $event.preventDefault()\n \"\n (keydown.Home)=\"snapToEnd(sliderThumb.Upper, false); $event.preventDefault()\"\n (keydown.End)=\"snapToEnd(sliderThumb.Upper, true); $event.preventDefault()\"\n >\n <!-- Upper Thumb Callout -->\n <div\n class=\"tooltip top tooltip-upper\"\n #upperTooltip\n [class.tooltip-dynamic]=\"\n _options.handles.callout.trigger === sliderCalloutTrigger.Dynamic &&\n thumbs.upper.drag === false\n \"\n [style.opacity]=\"tooltips.upper.visible ? 1 : 0\"\n [style.left.px]=\"tooltips.upper.position\"\n >\n <div\n class=\"tooltip-arrow\"\n [style.border-top-color]=\"_options.handles.callout.background\"\n ></div>\n\n @if (_options.type === sliderType.Range) {\n <div\n class=\"tooltip-inner\"\n [style.background-color]=\"_options.handles.callout.background\"\n [style.color]=\"_options.handles.callout.color\"\n >\n {{ tooltips.upper.label }}\n </div>\n }\n </div>\n </div>\n\n <!-- Section of Track Abover Upper Thumb -->\n <div\n class=\"track-section track-higher\"\n [style.flex-grow]=\"tracks.upper.size\"\n [style.background]=\"this.disabled ? null : tracks.upper.color\"\n ></div>\n</div>\n\n<!-- Chart Ticks and Tick Labels -->\n@if (\n (_options.track.ticks.major.show || _options.track.ticks.minor.show) &&\n _options.handles.callout.trigger !== sliderCalloutTrigger.Dynamic\n) {\n <div\n class=\"tick-container\"\n role=\"presentation\"\n [class.show-labels]=\"_options.track.ticks.major.labels || _options.track.ticks.minor.labels\"\n >\n @for (tick of ticks; track tick) {\n <div\n class=\"tick\"\n [class.major]=\"tick.type === sliderTickType.Major\"\n [class.minor]=\"tick.type === sliderTickType.Minor\"\n [style.left.%]=\"tick.position\"\n [hidden]=\"!tick.showTicks\"\n >\n <div class=\"tick-indicator\"></div>\n <div class=\"tick-label\" aria-hidden=\"true\" [hidden]=\"!tick.showLabels\">\n {{ tick.label }}\n </div>\n </div>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: DragDirective, selector: "[uxDrag]", inputs: ["clone", "group", "model", "draggable"], outputs: ["onDragStart", "onDrag", "onDragScroll", "onDragEnd", "onDrop", "onDropEnter", "onDropLeave"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SliderComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-slider', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[class.disabled]': 'disabled',
}, standalone: false, template: "<div\n class=\"track\"\n #track\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n [class.range]=\"_options.type === sliderType.Range\"\n>\n <!-- Section Beneath Lower Thumb -->\n <div\n class=\"track-section track-lower\"\n [style.flex-grow]=\"tracks.lower.size\"\n [style.background]=\"this.disabled ? null : tracks.lower.color\"\n ></div>\n\n <!-- Lower Thumb Button / Line -->\n <div\n class=\"thumb lower\"\n uxDrag\n uxFocusIndicator\n role=\"slider\"\n [tabindex]=\"disabled ? -1 : 0\"\n #lowerthumb\n [attr.aria-label]=\"\n _options.type === sliderType.Range\n ? _options.handles.aria.lowerThumb\n : _options.handles.aria.thumb\n \"\n [attr.aria-valuemin]=\"_options?.track?.min\"\n [attr.aria-valuemax]=\"\n _options.type === sliderType.Range ? getThumbValue(sliderThumb.Upper) : _options?.track?.max\n \"\n [attr.aria-valuenow]=\"getThumbValue(sliderThumb.Lower)\"\n [attr.aria-valuetext]=\"getAriaValueText(sliderThumb.Lower)\"\n [style.left.%]=\"thumbs.lower.position\"\n [class.active]=\"thumbs.lower.drag\"\n [style.z-index]=\"thumbs.lower.order\"\n [class.button]=\"_options.handles.style === sliderStyle.Button\"\n [class.line]=\"_options.handles.style === sliderStyle.Line\"\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n (onDragStart)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.DragStart); lowerthumb.focus()\"\n (onDrag)=\"updateThumbPosition($event, sliderThumb.Lower)\"\n (onDragEnd)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.DragEnd)\"\n (mouseenter)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseOver)\"\n (mouseleave)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseLeave)\"\n (focus)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseOver)\"\n (blur)=\"thumbEvent(sliderThumb.Lower, sliderThumbEvent.MouseLeave)\"\n (keydown.ArrowLeft)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowRight)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.ArrowUp)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowDown)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.PageDown)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.Major, false); $event.preventDefault()\n \"\n (keydown.PageUp)=\"\n snapToNearestTick(sliderThumb.Lower, sliderSnap.Major, true); $event.preventDefault()\n \"\n (keydown.Home)=\"snapToEnd(sliderThumb.Lower, false); $event.preventDefault()\"\n (keydown.End)=\"snapToEnd(sliderThumb.Lower, true); $event.preventDefault()\"\n >\n <!-- Lower Thumb Callout -->\n <div\n class=\"tooltip top tooltip-lower\"\n #lowerTooltip\n [class.tooltip-dynamic]=\"\n _options.handles.callout.trigger === sliderCalloutTrigger.Dynamic &&\n thumbs.lower.drag === false\n \"\n [style.opacity]=\"tooltips.lower.visible ? 1 : 0\"\n [style.left.px]=\"tooltips.lower.position\"\n >\n <div\n class=\"tooltip-arrow\"\n [style.border-top-color]=\"_options.handles.callout.background\"\n ></div>\n\n <div\n class=\"tooltip-inner\"\n [style.background-color]=\"_options.handles.callout.background\"\n [style.color]=\"_options.handles.callout.color\"\n >\n {{ tooltips.lower.label }}\n </div>\n </div>\n </div>\n\n <!-- Section of Track Between Lower and Upper Thumbs -->\n @if (_options.type === sliderType.Range) {\n <div\n class=\"track-section track-range\"\n [style.flex-grow]=\"tracks.middle.size\"\n [style.background]=\"this.disabled ? null : tracks.middle.color\"\n ></div>\n }\n\n <!-- Upper Thumb Button / Line -->\n <div\n class=\"thumb upper\"\n uxDrag\n uxFocusIndicator\n role=\"slider\"\n [tabindex]=\"disabled ? -1 : 0\"\n #upperthumb\n [attr.aria-label]=\"_options.handles.aria.upperThumb\"\n [attr.aria-valuemin]=\"getThumbValue(sliderThumb.Lower) || _options?.track?.min\"\n [attr.aria-valuemax]=\"_options?.track?.max\"\n [attr.aria-valuenow]=\"getThumbValue(sliderThumb.Upper)\"\n [attr.aria-valuetext]=\"getAriaValueText(sliderThumb.Upper)\"\n [hidden]=\"_options.type !== sliderType.Range\"\n [class.active]=\"thumbs.upper.drag\"\n [style.left.%]=\"thumbs.upper.position\"\n [style.z-index]=\"thumbs.upper.order\"\n [class.button]=\"_options.handles.style === sliderStyle.Button\"\n [class.line]=\"_options.handles.style === sliderStyle.Line\"\n [class.narrow]=\"_options.track.height === sliderSize.Narrow\"\n [class.wide]=\"_options.track.height === sliderSize.Wide\"\n (onDragStart)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.DragStart); upperthumb.focus()\"\n (onDrag)=\"updateThumbPosition($event, sliderThumb.Upper)\"\n (onDragEnd)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.DragEnd)\"\n (mouseenter)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseOver)\"\n (mouseleave)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseLeave)\"\n (focus)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseOver)\"\n (blur)=\"thumbEvent(sliderThumb.Upper, sliderThumbEvent.MouseLeave)\"\n (keydown.ArrowLeft)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowRight)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.ArrowUp)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, false); $event.preventDefault()\n \"\n (keydown.ArrowDown)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.All, true); $event.preventDefault()\n \"\n (keydown.PageDown)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.Major, false); $event.preventDefault()\n \"\n (keydown.PageUp)=\"\n snapToNearestTick(sliderThumb.Upper, sliderSnap.Major, true); $event.preventDefault()\n \"\n (keydown.Home)=\"snapToEnd(sliderThumb.Upper, false); $event.preventDefault()\"\n (keydown.End)=\"snapToEnd(sliderThumb.Upper, true); $event.preventDefault()\"\n >\n <!-- Upper Thumb Callout -->\n <div\n class=\"tooltip top tooltip-upper\"\n #upperTooltip\n [class.tooltip-dynamic]=\"\n _options.handles.callout.trigger === sliderCalloutTrigger.Dynamic &&\n thumbs.upper.drag === false\n \"\n [style.opacity]=\"tooltips.upper.visible ? 1 : 0\"\n [style.left.px]=\"tooltips.upper.position\"\n >\n <div\n class=\"tooltip-arrow\"\n [style.border-top-color]=\"_options.handles.callout.background\"\n ></div>\n\n @if (_options.type === sliderType.Range) {\n <div\n class=\"tooltip-inner\"\n [style.background-color]=\"_options.handles.callout.background\"\n [style.color]=\"_options.handles.callout.color\"\n >\n {{ tooltips.upper.label }}\n </div>\n }\n </div>\n </div>\n\n <!-- Section of Track Abover Upper Thumb -->\n <div\n class=\"track-section track-higher\"\n [style.flex-grow]=\"tracks.upper.size\"\n [style.background]=\"this.disabled ? null : tracks.upper.color\"\n ></div>\n</div>\n\n<!-- Chart Ticks and Tick Labels -->\n@if (\n (_options.track.ticks.major.show || _options.track.ticks.minor.show) &&\n _options.handles.callout.trigger !== sliderCalloutTrigger.Dynamic\n) {\n <div\n class=\"tick-container\"\n role=\"presentation\"\n [class.show-labels]=\"_options.track.ticks.major.labels || _options.track.ticks.minor.labels\"\n >\n @for (tick of ticks; track tick) {\n <div\n class=\"tick\"\n [class.major]=\"tick.type === sliderTickType.Major\"\n [class.minor]=\"tick.type === sliderTickType.Minor\"\n [style.left.%]=\"tick.position\"\n [hidden]=\"!tick.showTicks\"\n >\n <div class=\"tick-indicator\"></div>\n <div class=\"tick-label\" aria-hidden=\"true\" [hidden]=\"!tick.showLabels\">\n {{ tick.label }}\n </div>\n </div>\n }\n </div>\n}\n" }]
}], ctorParameters: () => [], propDecorators: { value: [{
type: Input
}], options: [{
type: Input
}], disabled: [{
type: Input
}], valueChange: [{
type: Output
}], lowerTooltip: [{
type: ViewChild,
args: ['lowerTooltip', { static: true }]
}], upperTooltip: [{
type: ViewChild,
args: ['upperTooltip', { static: true }]
}], track: [{
type: ViewChild,
args: ['track', { static: true }]
}] } });
var SliderType;
(function (SliderType) {
SliderType[SliderType["Value"] = 0] = "Value";
SliderType[SliderType["Range"] = 1] = "Range";
})(SliderType || (SliderType = {}));
var SliderStyle;
(function (SliderStyle) {
SliderStyle[SliderStyle["Button"] = 0] = "Button";
SliderStyle[SliderStyle["Line"] = 1] = "Line";
})(SliderStyle || (SliderStyle = {}));
var SliderSize;
(function (SliderSize) {
SliderSize[SliderSize["Narrow"] = 0] = "Narrow";
SliderSize[SliderSize["Wide"] = 1] = "Wide";
})(SliderSize || (SliderSize = {}));
var SliderCalloutTrigger;
(function (SliderCalloutTrigger) {
SliderCalloutTrigger[SliderCalloutTrigger["None"] = 0] = "None";
SliderCalloutTrigger[SliderCalloutTrigger["Hover"] = 1] = "Hover";
SliderCalloutTrigger[SliderCalloutTrigger["Drag"] = 2] = "Drag";
SliderCalloutTrigger[SliderCalloutTrigger["Persistent"] = 3] = "Persistent";
SliderCalloutTrigger[SliderCalloutTrigger["Dynamic"] = 4] = "Dynamic";
})(SliderCalloutTrigger || (SliderCalloutTrigger = {}));
var SliderSnap;
(function (SliderSnap) {
SliderSnap[SliderSnap["None"] = 0] = "None";
SliderSnap[SliderSnap["Minor"] = 1] = "Minor";
SliderSnap[SliderSnap["Major"] = 2] = "Major";
SliderSnap[SliderSnap["All"] = 3] = "All";
})(SliderSnap || (SliderSnap = {}));
var SliderTickType;
(function (SliderTickType) {
SliderTickType[SliderTickType["Minor"] = 0] = "Minor";
SliderTickType[SliderTickType["Major"] = 1] = "Major";
})(SliderTickType || (SliderTickType = {}));
var SliderThumbEvent;
(function (SliderThumbEvent) {
SliderThumbEvent[SliderThumbEvent["None"] = 0] = "None";
SliderThumbEvent[SliderThumbEvent["MouseOver"] = 1] = "MouseOver";
SliderThumbEvent[SliderThumbEvent["MouseLeave"] = 2] = "MouseLeave";
SliderThumbEvent[SliderThumbEvent["DragStart"] = 3] = "DragStart";
SliderThumbEvent[SliderThumbEvent["DragEnd"] = 4] = "DragEnd";
})(SliderThumbEvent || (SliderThumbEvent = {}));
var SliderThumb;
(function (SliderThumb) {
SliderThumb[SliderThumb["Lower"] = 0] = "Lower";
SliderThumb[SliderThumb["Upper"] = 1] = "Upper";
})(SliderThumb || (SliderThumb = {}));
class SliderModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SliderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SliderModule, declarations: [SliderComponent], imports: [AccessibilityModule, CommonModule, ColorServiceModule, DragModule], exports: [SliderComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SliderModule, imports: [AccessibilityModule, CommonModule, ColorServiceModule, DragModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SliderModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, ColorServiceModule, DragModule],
exports: [SliderComponent],
declarations: [SliderComponent],
}]
}] });
let uniqueId$4 = 1;
class MediaPlayerControlsExtensionComponent {
constructor() {
this.mediaPlayerService = inject(MediaPlayerService);
this.volumeActive = false;
this.volumeFocus = false;
this.returnFocus = true;
this.subtitlesId = `ux-media-player-subtitle-popover-${uniqueId$4++}`;
this.subtitlesOpen = false;
this.mouseEnterVolume = new Subject();
this.mouseLeaveVolume = new Subject();
this.options = {
handles: {
aria: {
thumb: 'Volume',
},
},
track: {
colors: {
lower: '#666',
},
height: SliderSize.Narrow,
ticks: {
major: {
show: false,
},
minor: {
show: false,
},
},
},
};
this._volume = 100;
this._previousVolume = 100;
this._onDestroy = new Subject();
}
get volume() {
return this._volume;
}
set volume(value) {
if (value === 0 && this._volume !== 0) {
this._previousVolume = this._volume;
}
this._volume = Math.min(Math.max(value, 0), 100);
this.mediaPlayerService.volume = this._volume / 100;
}
ngOnInit() {
this.mediaPlayerService.volumeChangeEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(volume => (this.volume = volume * 100));
this.mediaPlayerService.initEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this.volume = this.mediaPlayerService.volume * 100));
this.mouseEnterVolume
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this.volumeActive = true));
this.mouseLeaveVolume
.pipe(switchMap(() => timer(1500).pipe(takeUntil(this.mouseEnterVolume))), takeUntil(this._onDestroy))
.subscribe(() => (this.volumeActive = false));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
toggleMute() {
this.volume = this.volume === 0 ? this._previousVolume : 0;
}
goToStart() {
this.mediaPlayerService.currentTime = 0;
}
goToEnd() {
this.mediaPlayerService.currentTime = this.mediaPlayerService.duration;
}
isSubtitleActive() {
for (let idx = 0; idx < this.mediaPlayerService.textTracks.length; idx++) {
if (this.mediaPlayerService.textTracks[idx].mode === 'showing') {
return true;
}
}
return false;
}
setSubtitleTrack(track) {
// hide all tracks
this.mediaPlayerService.hideSubtitleTracks();
// set the position of the subtitle track
for (let idx = 0; idx < track.cues.length; idx++) {
const cue = track.cues[idx];
cue.line = -3;
}
// activate the selected one
track.mode = 'showing';
}
getSubtitleTrack() {
for (let idx = 0; idx < this.mediaPlayerService.textTracks.length; idx++) {
if (this.mediaPlayerService.textTracks[idx].mode === 'showing') {
return this.mediaPlayerService.textTracks[idx].label;
}
}
return this.mediaPlayerService.noSubtitlesAriaLabel;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerControlsExtensionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MediaPlayerControlsExtensionComponent, isStandalone: false, selector: "ux-media-player-controls", host: { properties: { "class.quiet": "mediaPlayerService.quietMode || mediaPlayerService.fullscreen" } }, ngImport: i0, template: "<div class=\"volume-container\">\n <div\n class=\"volume-slider-container\"\n #volumeContainer\n [class.active]=\"volumeActive || volumeFocus\"\n (mouseenter)=\"mouseEnterVolume.next()\"\n (mouseleave)=\"mouseLeaveVolume.next()\"\n (uxFocusWithin)=\"volumeFocus = true\"\n (uxBlurWithin)=\"volumeFocus = false\"\n >\n <button\n #volumeIcon\n uxFocusIndicator\n type=\"button\"\n class=\"volume-slider-icon\"\n [attr.aria-label]=\"mediaPlayerService.muteAriaLabel(volume)\"\n [uxTooltip]=\"muteTooltip\"\n [showTriggers]=\"['mouseenter']\"\n [hideTriggers]=\"['mouseleave']\"\n (click)=\"toggleMute()\"\n (mouseup)=\"volumeIcon.blur()\"\n >\n @if (volume === 0) {\n <ux-icon name=\"volume-mute\"></ux-icon>\n }\n @if (volume > 0 && volume <= 70) {\n <ux-icon name=\"volume-low\"></ux-icon>\n }\n @if (volume > 70) {\n <ux-icon name=\"volume\"></ux-icon>\n }\n </button>\n\n <div class=\"volume-slider-node\">\n <ux-slider\n [value]=\"volume\"\n (valueChange)=\"volume = $any($event)\"\n [options]=\"options\"\n ></ux-slider>\n </div>\n </div>\n</div>\n\n<button\n #startButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n (click)=\"goToStart()\"\n (mouseup)=\"startButton.blur()\"\n [attr.aria-label]=\"mediaPlayerService.goToStartAriaLabel\"\n>\n <svg viewBox=\"0 0 51.5 64\" width=\"14\" height=\"17\" focusable=\"false\">\n <rect x=\"0\" y=\"0\" width=\"7.5\" height=\"64\" />\n <polygon points=\"51.5,64 51.5,0 7.4,32 \" />\n </svg>\n</button>\n\n<button\n #playButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n [attr.aria-label]=\"mediaPlayerService.playAriaLabel(mediaPlayerService.playing | async)\"\n (click)=\"mediaPlayerService.togglePlay()\"\n (mouseup)=\"playButton.blur()\"\n>\n @if ((mediaPlayerService.playing | async) === false) {\n <svg viewBox=\"0 0 45 64\" width=\"20\" height=\"29\" focusable=\"false\">\n <polygon points=\"0.4,0 0.4,64 44.6,32\" />\n </svg>\n }\n @if (mediaPlayerService.playing | async) {\n <svg viewBox=\"0 0 43 56.9\" width=\"20\" height=\"29\" focusable=\"false\">\n <rect y=\"0.1\" width=\"15.7\" height=\"56.9\" />\n <rect x=\"27.3\" y=\"0.1\" width=\"15.7\" height=\"56.9\" />\n </svg>\n }\n</button>\n\n<button\n #endButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n (click)=\"goToEnd()\"\n (mouseup)=\"endButton.blur()\"\n [attr.aria-label]=\"mediaPlayerService.goToEndAriaLabel\"\n>\n <svg viewBox=\"0 0 51.5 64\" width=\"14\" height=\"17\" focusable=\"false\">\n <rect x=\"44.1\" y=\"0\" width=\"7.5\" height=\"64\" />\n <polygon points=\"0,64 0,0 44.1,32\" />\n </svg>\n</button>\n\n<div class=\"actions-list\">\n <ng-content></ng-content>\n\n @if (mediaPlayerService.textTracks.length > 0 && mediaPlayerService.type === 'video') {\n <div class=\"action-button-container\">\n <button\n #subtitlesButton\n uxFocusIndicator\n type=\"button\"\n class=\"action-button\"\n (keydown)=\"returnFocus = true\"\n (click)=\"subtitlesOpen = !subtitlesOpen\"\n (mouseup)=\"subtitlesButton.blur(); returnFocus = false\"\n [attr.aria-label]=\"mediaPlayerService.selectSubtitlesAriaLabel(getSubtitleTrack())\"\n [attr.aria-expanded]=\"subtitlesOpen\"\n [attr.aria-describedby]=\"subtitlesId\"\n aria-haspopup=\"true\"\n >\n <ux-icon name=\"subtitles\"></ux-icon>\n </button>\n @if (subtitlesOpen) {\n <div\n #subtitles\n [style.top.px]=\"-subtitles.offsetHeight\"\n class=\"popover top media-player-subtitles-popover show\"\n [id]=\"subtitlesId\"\n (keydown.escape)=\"subtitlesOpen = false\"\n (uxClickOutside)=\"subtitlesOpen = false\"\n >\n <div class=\"arrow\"></div>\n <h3 class=\"popover-title\">{{ mediaPlayerService.subtitlesTitleAriaLabel }}</h3>\n <div class=\"popover-content\">\n <ul\n class=\"subtitles-list\"\n uxTabbableList\n [focusOnShow]=\"returnFocus\"\n [returnFocus]=\"returnFocus\"\n >\n <li\n uxTabbableListItem\n tabindex=\"0\"\n class=\"subtitles-list-item\"\n [class.active]=\"!isSubtitleActive()\"\n [attr.aria-selected]=\"isSubtitleActive()\"\n (click)=\"mediaPlayerService.hideSubtitleTracks(); subtitlesOpen = false\"\n (keydown.enter)=\"\n mediaPlayerService.hideSubtitleTracks(); subtitlesOpen = false; returnFocus = true\n \"\n >\n <ux-icon name=\"checkmark\" class=\"subtitles-list-item-checkmark\"></ux-icon>\n <span>{{ mediaPlayerService.subtitlesOffAriaLabel }}</span>\n </li>\n @for (track of mediaPlayerService.textTracks; track track) {\n <li\n uxTabbableListItem\n class=\"subtitles-list-item\"\n [class.active]=\"track.mode === 'showing'\"\n [attr.aria-selected]=\"isSubtitleActive()\"\n (click)=\"setSubtitleTrack(track); subtitlesOpen = false\"\n (keydown.enter)=\"\n setSubtitleTrack(track); subtitlesOpen = false; returnFocus = true\n \"\n >\n <ux-icon name=\"checkmark\" class=\"subtitles-list-item-checkmark\"></ux-icon>\n <span>{{ track.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n </div>\n }\n\n <div class=\"action-button-container\">\n @if (mediaPlayerService.type !== 'audio') {\n <button\n #fullscreenButton\n uxFocusIndicator\n type=\"button\"\n class=\"action-button\"\n [attr.aria-label]=\"mediaPlayerService.fullscreenAriaLabel(mediaPlayerService.fullscreen)\"\n (click)=\"mediaPlayerService.toggleFullscreen()\"\n (mouseup)=\"fullscreenButton.blur()\"\n >\n <ux-icon [name]=\"mediaPlayerService.fullscreen ? 'contract' : 'expand'\"></ux-icon>\n </button>\n }\n </div>\n</div>\n\n<ng-template #muteTooltip>\n <span aria-hidden=\"true\">{{ volume === 0 ? 'Unmute' : 'Mute' }}</span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusWithinDirective, selector: "[uxFocusWithin],[uxBlurWithin]", outputs: ["uxFocusWithin", "uxBlurWithin"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: ClickOutsideDirective, selector: "[uxClickOutside]", outputs: ["uxClickOutside"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: SliderComponent, selector: "ux-slider", inputs: ["value", "options", "disabled"], outputs: ["valueChange"] }, { kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerControlsExtensionComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-media-player-controls', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[class.quiet]': 'mediaPlayerService.quietMode || mediaPlayerService.fullscreen',
}, standalone: false, template: "<div class=\"volume-container\">\n <div\n class=\"volume-slider-container\"\n #volumeContainer\n [class.active]=\"volumeActive || volumeFocus\"\n (mouseenter)=\"mouseEnterVolume.next()\"\n (mouseleave)=\"mouseLeaveVolume.next()\"\n (uxFocusWithin)=\"volumeFocus = true\"\n (uxBlurWithin)=\"volumeFocus = false\"\n >\n <button\n #volumeIcon\n uxFocusIndicator\n type=\"button\"\n class=\"volume-slider-icon\"\n [attr.aria-label]=\"mediaPlayerService.muteAriaLabel(volume)\"\n [uxTooltip]=\"muteTooltip\"\n [showTriggers]=\"['mouseenter']\"\n [hideTriggers]=\"['mouseleave']\"\n (click)=\"toggleMute()\"\n (mouseup)=\"volumeIcon.blur()\"\n >\n @if (volume === 0) {\n <ux-icon name=\"volume-mute\"></ux-icon>\n }\n @if (volume > 0 && volume <= 70) {\n <ux-icon name=\"volume-low\"></ux-icon>\n }\n @if (volume > 70) {\n <ux-icon name=\"volume\"></ux-icon>\n }\n </button>\n\n <div class=\"volume-slider-node\">\n <ux-slider\n [value]=\"volume\"\n (valueChange)=\"volume = $any($event)\"\n [options]=\"options\"\n ></ux-slider>\n </div>\n </div>\n</div>\n\n<button\n #startButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n (click)=\"goToStart()\"\n (mouseup)=\"startButton.blur()\"\n [attr.aria-label]=\"mediaPlayerService.goToStartAriaLabel\"\n>\n <svg viewBox=\"0 0 51.5 64\" width=\"14\" height=\"17\" focusable=\"false\">\n <rect x=\"0\" y=\"0\" width=\"7.5\" height=\"64\" />\n <polygon points=\"51.5,64 51.5,0 7.4,32 \" />\n </svg>\n</button>\n\n<button\n #playButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n [attr.aria-label]=\"mediaPlayerService.playAriaLabel(mediaPlayerService.playing | async)\"\n (click)=\"mediaPlayerService.togglePlay()\"\n (mouseup)=\"playButton.blur()\"\n>\n @if ((mediaPlayerService.playing | async) === false) {\n <svg viewBox=\"0 0 45 64\" width=\"20\" height=\"29\" focusable=\"false\">\n <polygon points=\"0.4,0 0.4,64 44.6,32\" />\n </svg>\n }\n @if (mediaPlayerService.playing | async) {\n <svg viewBox=\"0 0 43 56.9\" width=\"20\" height=\"29\" focusable=\"false\">\n <rect y=\"0.1\" width=\"15.7\" height=\"56.9\" />\n <rect x=\"27.3\" y=\"0.1\" width=\"15.7\" height=\"56.9\" />\n </svg>\n }\n</button>\n\n<button\n #endButton\n uxFocusIndicator\n type=\"button\"\n class=\"control-button\"\n (click)=\"goToEnd()\"\n (mouseup)=\"endButton.blur()\"\n [attr.aria-label]=\"mediaPlayerService.goToEndAriaLabel\"\n>\n <svg viewBox=\"0 0 51.5 64\" width=\"14\" height=\"17\" focusable=\"false\">\n <rect x=\"44.1\" y=\"0\" width=\"7.5\" height=\"64\" />\n <polygon points=\"0,64 0,0 44.1,32\" />\n </svg>\n</button>\n\n<div class=\"actions-list\">\n <ng-content></ng-content>\n\n @if (mediaPlayerService.textTracks.length > 0 && mediaPlayerService.type === 'video') {\n <div class=\"action-button-container\">\n <button\n #subtitlesButton\n uxFocusIndicator\n type=\"button\"\n class=\"action-button\"\n (keydown)=\"returnFocus = true\"\n (click)=\"subtitlesOpen = !subtitlesOpen\"\n (mouseup)=\"subtitlesButton.blur(); returnFocus = false\"\n [attr.aria-label]=\"mediaPlayerService.selectSubtitlesAriaLabel(getSubtitleTrack())\"\n [attr.aria-expanded]=\"subtitlesOpen\"\n [attr.aria-describedby]=\"subtitlesId\"\n aria-haspopup=\"true\"\n >\n <ux-icon name=\"subtitles\"></ux-icon>\n </button>\n @if (subtitlesOpen) {\n <div\n #subtitles\n [style.top.px]=\"-subtitles.offsetHeight\"\n class=\"popover top media-player-subtitles-popover show\"\n [id]=\"subtitlesId\"\n (keydown.escape)=\"subtitlesOpen = false\"\n (uxClickOutside)=\"subtitlesOpen = false\"\n >\n <div class=\"arrow\"></div>\n <h3 class=\"popover-title\">{{ mediaPlayerService.subtitlesTitleAriaLabel }}</h3>\n <div class=\"popover-content\">\n <ul\n class=\"subtitles-list\"\n uxTabbableList\n [focusOnShow]=\"returnFocus\"\n [returnFocus]=\"returnFocus\"\n >\n <li\n uxTabbableListItem\n tabindex=\"0\"\n class=\"subtitles-list-item\"\n [class.active]=\"!isSubtitleActive()\"\n [attr.aria-selected]=\"isSubtitleActive()\"\n (click)=\"mediaPlayerService.hideSubtitleTracks(); subtitlesOpen = false\"\n (keydown.enter)=\"\n mediaPlayerService.hideSubtitleTracks(); subtitlesOpen = false; returnFocus = true\n \"\n >\n <ux-icon name=\"checkmark\" class=\"subtitles-list-item-checkmark\"></ux-icon>\n <span>{{ mediaPlayerService.subtitlesOffAriaLabel }}</span>\n </li>\n @for (track of mediaPlayerService.textTracks; track track) {\n <li\n uxTabbableListItem\n class=\"subtitles-list-item\"\n [class.active]=\"track.mode === 'showing'\"\n [attr.aria-selected]=\"isSubtitleActive()\"\n (click)=\"setSubtitleTrack(track); subtitlesOpen = false\"\n (keydown.enter)=\"\n setSubtitleTrack(track); subtitlesOpen = false; returnFocus = true\n \"\n >\n <ux-icon name=\"checkmark\" class=\"subtitles-list-item-checkmark\"></ux-icon>\n <span>{{ track.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n </div>\n }\n\n <div class=\"action-button-container\">\n @if (mediaPlayerService.type !== 'audio') {\n <button\n #fullscreenButton\n uxFocusIndicator\n type=\"button\"\n class=\"action-button\"\n [attr.aria-label]=\"mediaPlayerService.fullscreenAriaLabel(mediaPlayerService.fullscreen)\"\n (click)=\"mediaPlayerService.toggleFullscreen()\"\n (mouseup)=\"fullscreenButton.blur()\"\n >\n <ux-icon [name]=\"mediaPlayerService.fullscreen ? 'contract' : 'expand'\"></ux-icon>\n </button>\n }\n </div>\n</div>\n\n<ng-template #muteTooltip>\n <span aria-hidden=\"true\">{{ volume === 0 ? 'Unmute' : 'Mute' }}</span>\n</ng-template>\n" }]
}] });
class MediaPlayerCustomControlDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerCustomControlDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MediaPlayerCustomControlDirective, isStandalone: false, selector: "[uxMediaPlayerCustomControl]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerCustomControlDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMediaPlayerCustomControl]',
standalone: false,
}]
}] });
class DurationPipe {
transform(seconds) {
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
hours = hours - days * 24;
minutes = minutes - days * 24 * 60 - hours * 60;
seconds = Math.floor(seconds - days * 24 * 60 * 60 - hours * 60 * 60 - minutes * 60);
if (hours > 0) {
return `${this.pad(hours)}:${this.pad(minutes)}:${this.pad(seconds)}`;
}
else {
return `${this.pad(minutes)}:${this.pad(seconds)}`;
}
}
pad(value) {
if (value < 10) {
return `0${value}`;
}
return value.toString();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DurationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DurationPipe, isStandalone: false, name: "duration" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DurationPipe, decorators: [{
type: Pipe,
args: [{
name: 'duration',
standalone: false,
}]
}] });
class MediaPlayerTimelineExtensionComponent {
constructor() {
this.mediaPlayerService = inject(MediaPlayerService);
this.current = 0;
this.position = 0;
this.buffered = [];
this.mouseDown = false;
this.scrub = { visible: false, position: 0, time: 0 };
this._onDestroy = new Subject();
}
ngOnInit() {
// watch for changes to the current time
this.mediaPlayerService.fullscreenEvent.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.scrub.position = 0;
});
this.mediaPlayerService.timeUpdateEvent.pipe(takeUntil(this._onDestroy)).subscribe(current => {
this.current = current;
this.position = (this.current / this.mediaPlayerService.duration) * 100;
});
this.mediaPlayerService.progressEvent
.pipe(takeUntil(this._onDestroy))
.subscribe((buffered) => {
this.buffered = [];
for (let idx = 0; idx < buffered.length; idx++) {
this.buffered.push({
start: (buffered.start(idx) / this.mediaPlayerService.duration) * 100,
end: (buffered.end(idx) / this.mediaPlayerService.duration) * 100,
});
}
});
}
ngAfterViewInit() {
const mousedown$ = fromEvent(this.thumb.nativeElement, 'mousedown');
const mousemove$ = fromEvent(document, 'mousemove');
const mouseup$ = fromEvent(document, 'mouseup');
mousedown$
.pipe(switchMap(() => mousemove$.pipe(takeUntil(mouseup$))), takeUntil(this._onDestroy))
.subscribe(() => (this.scrub.visible = false));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
updateScrub(event) {
const target = event.target;
if (target.classList.contains('media-progress-bar-thumb')) {
return;
}
const timeline = this.timelineRef.nativeElement;
const bounds = timeline.getBoundingClientRect();
this.scrub.position = event.offsetX;
this.scrub.time = (event.offsetX / bounds.width) * this.mediaPlayerService.duration;
if (this.mouseDown) {
this.mediaPlayerService.pause();
this.mediaPlayerService.currentTime = this.scrub.time;
}
}
/** Skip a number of seconds in any direction */
skip(seconds) {
let target = this.current + seconds;
// ensure that the target position is within the bounds of the clip
if (target < 0) {
target = 0;
}
if (target > this.mediaPlayerService.duration) {
target = this.mediaPlayerService.duration;
}
this.mediaPlayerService.currentTime = target;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerTimelineExtensionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MediaPlayerTimelineExtensionComponent, isStandalone: false, selector: "ux-media-player-timeline", host: { listeners: { "document:mouseup": "mouseDown = false" }, properties: { "class.quiet": "mediaPlayerService.quietMode || mediaPlayerService.fullscreen" } }, viewQueries: [{ propertyName: "thumb", first: true, predicate: ["progressThumb"], descendants: true, static: true }, { propertyName: "timelineRef", first: true, predicate: ["timeline"], descendants: true, static: true }], ngImport: i0, template: "<p class=\"current-time\">{{ current | duration }}</p>\n\n<div\n #timeline\n class=\"timeline-bar\"\n tabindex=\"0\"\n role=\"slider\"\n [attr.aria-label]=\"mediaPlayerService.seekAriaLabel\"\n aria-valuemin=\"0\"\n [attr.aria-valuemax]=\"mediaPlayerService.duration | number: '0.0-0'\"\n [attr.aria-valuenow]=\"mediaPlayerService.currentTime | number: '0.0-0'\"\n attr.aria-valuetext=\"{{ mediaPlayerService.currentTime | duration }} of {{\n mediaPlayerService.duration | duration\n }}\"\n (keydown.ArrowLeft)=\"skip(-5)\"\n (keydown.ArrowRight)=\"skip(5)\"\n (mouseenter)=\"scrub.visible = true; tooltip.show()\"\n (mouseleave)=\"scrub.visible = false; tooltip.hide()\"\n (mousemove)=\"updateScrub($event); tooltip.reposition()\"\n (mouseup)=\"updateScrub($event)\"\n (mousedown)=\"mouseDown = true; $event.preventDefault()\"\n>\n @for (buffer of buffered; track buffer) {\n <div\n class=\"buffered-bar\"\n [style.left.%]=\"buffer.start\"\n [style.width.%]=\"buffer.end - buffer.start\"\n ></div>\n }\n\n <div class=\"media-progress-bar\" [style.width.%]=\"position\">\n <div\n #progressThumb\n class=\"media-progress-bar-thumb\"\n (mouseenter)=\"scrub.visible = false; tooltip.hide(); $event.stopPropagation()\"\n (mouseleave)=\"scrub.visible = true; tooltip.show(); $event.stopPropagation()\"\n ></div>\n </div>\n\n <div\n #tooltip=\"ux-tooltip\"\n class=\"scrub-handle\"\n [class.scrub-handle-hidden]=\"!scrub.visible\"\n [style.left.px]=\"scrub.position\"\n [uxTooltip]=\"popTemplate\"\n tooltipClass=\"ux-media-player-timeline-tooltip\"\n placement=\"top\"\n [showTriggers]=\"[]\"\n [hideTriggers]=\"[]\"\n [tooltipDelay]=\"100\"\n [tooltipDisabled]=\"mediaPlayerService.duration === 0\"\n ></div>\n</div>\n\n<p class=\"duration-time\">{{ mediaPlayerService.duration | duration }}</p>\n\n<ng-template #popTemplate>\n <span>{{ scrub.time | duration }}</span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }, { kind: "pipe", type: DurationPipe, name: "duration" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerTimelineExtensionComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-media-player-timeline', host: {
'(document:mouseup)': 'mouseDown = false',
'[class.quiet]': 'mediaPlayerService.quietMode || mediaPlayerService.fullscreen',
}, standalone: false, template: "<p class=\"current-time\">{{ current | duration }}</p>\n\n<div\n #timeline\n class=\"timeline-bar\"\n tabindex=\"0\"\n role=\"slider\"\n [attr.aria-label]=\"mediaPlayerService.seekAriaLabel\"\n aria-valuemin=\"0\"\n [attr.aria-valuemax]=\"mediaPlayerService.duration | number: '0.0-0'\"\n [attr.aria-valuenow]=\"mediaPlayerService.currentTime | number: '0.0-0'\"\n attr.aria-valuetext=\"{{ mediaPlayerService.currentTime | duration }} of {{\n mediaPlayerService.duration | duration\n }}\"\n (keydown.ArrowLeft)=\"skip(-5)\"\n (keydown.ArrowRight)=\"skip(5)\"\n (mouseenter)=\"scrub.visible = true; tooltip.show()\"\n (mouseleave)=\"scrub.visible = false; tooltip.hide()\"\n (mousemove)=\"updateScrub($event); tooltip.reposition()\"\n (mouseup)=\"updateScrub($event)\"\n (mousedown)=\"mouseDown = true; $event.preventDefault()\"\n>\n @for (buffer of buffered; track buffer) {\n <div\n class=\"buffered-bar\"\n [style.left.%]=\"buffer.start\"\n [style.width.%]=\"buffer.end - buffer.start\"\n ></div>\n }\n\n <div class=\"media-progress-bar\" [style.width.%]=\"position\">\n <div\n #progressThumb\n class=\"media-progress-bar-thumb\"\n (mouseenter)=\"scrub.visible = false; tooltip.hide(); $event.stopPropagation()\"\n (mouseleave)=\"scrub.visible = true; tooltip.show(); $event.stopPropagation()\"\n ></div>\n </div>\n\n <div\n #tooltip=\"ux-tooltip\"\n class=\"scrub-handle\"\n [class.scrub-handle-hidden]=\"!scrub.visible\"\n [style.left.px]=\"scrub.position\"\n [uxTooltip]=\"popTemplate\"\n tooltipClass=\"ux-media-player-timeline-tooltip\"\n placement=\"top\"\n [showTriggers]=\"[]\"\n [hideTriggers]=\"[]\"\n [tooltipDelay]=\"100\"\n [tooltipDisabled]=\"mediaPlayerService.duration === 0\"\n ></div>\n</div>\n\n<p class=\"duration-time\">{{ mediaPlayerService.duration | duration }}</p>\n\n<ng-template #popTemplate>\n <span>{{ scrub.time | duration }}</span>\n</ng-template>\n" }]
}], propDecorators: { thumb: [{
type: ViewChild,
args: ['progressThumb', { static: true }]
}], timelineRef: [{
type: ViewChild,
args: ['timeline', { static: true }]
}] } });
class AudioService {
constructor() {
this._http = inject(HttpClient);
}
getAudioFileMetadata(mediaElement) {
return Observable.create((observer) => {
this._http.get(mediaElement.src, { responseType: 'blob' }).subscribe(response => {
let description;
const extension = mediaElement.src
.substring(mediaElement.src.lastIndexOf('.') + 1)
.toLowerCase();
const filename = mediaElement.src.indexOf('base64') !== -1
? ''
: mediaElement.src.substring(mediaElement.src.lastIndexOf('/') + 1);
switch (extension) {
case 'mp3':
description = 'MPEG audio layer 3 file';
break;
case 'wma':
description = 'Windows media audio file';
break;
case 'wav':
description = 'WAVE audio file';
break;
case 'ogg':
description = 'Ogg Vorbis file';
break;
case 'aac':
description = 'Advanced audio coding file';
break;
case 'midi':
description = 'Musical instrument digital interface file';
break;
default:
description = 'Audio file';
break;
}
observer.next({
filename,
extension,
description,
size: response.size,
});
});
});
}
getWaveformFromUrl(url) {
// if audio context is not support return a stream of empty data
if (!window.AudioContext) {
return of([new Float32Array(0)]);
}
this._audioContext = new AudioContext();
this.createVolumeNode();
this.createAnalyserNode();
return Observable.create((observer) => {
// load the media from the URL provided
this._http.get(url, { responseType: 'arraybuffer' }).subscribe(response => {
this.getAudioBuffer(response).subscribe(audioBuffer => {
// create the buffer source
this.createBufferSource(audioBuffer);
let dataPoints = [];
const channels = this._audioBuffer.numberOfChannels;
// extract the data from each channel
for (let channelIdx = 0; channelIdx < channels; channelIdx++) {
dataPoints[channelIdx] = this._audioBuffer.getChannelData(channelIdx);
}
observer.next(dataPoints);
observer.complete();
// cleanup after ourselves
dataPoints = null;
}, error => observer.error(error));
}, error => observer.error(error));
});
}
getWaveformPoints(channels = [], skip = 1000) {
const waveform = [];
const duration = channels.length > 0 ? channels[0].length : 0;
// convert each channel data to a series of waveform points
for (let idx = 0; idx < duration; idx += skip) {
// get all the channel data for a specific point
const points = channels.map(channel => channel[idx]);
// find the minimum point and maximum points at each position across all channels
waveform.push({
min: points.reduce((previous, current) => (current < previous ? current : previous)),
max: points.reduce((previous, current) => (current > previous ? current : previous)),
});
}
return waveform;
}
getAudioBuffer(arrayBuffer) {
return Observable.create((observer) => {
this.getOfflineAudioContext().decodeAudioData(arrayBuffer, (audioBuffer) => {
observer.next(audioBuffer);
observer.complete();
}, error => observer.error(error));
});
}
getOfflineAudioContext() {
return new OfflineAudioContext(1, 2, this._audioContext.sampleRate || 44100);
}
createBufferSource(audioBuffer) {
this.disconnectSource();
this._audioBuffer = audioBuffer;
this._audioBufferSource = this._audioContext.createBufferSource();
this._audioBufferSource.buffer = this._audioBuffer;
this._audioBufferSource.connect(this._analyserNode);
}
createVolumeNode() {
this._gainNode = this._audioContext.createGain();
this._gainNode.connect(this._audioContext.destination);
}
createAnalyserNode() {
this._analyserNode = this._audioContext.createAnalyser();
this._analyserNode.connect(this._gainNode);
}
disconnectSource() {
if (this._audioBufferSource) {
this._audioBufferSource.disconnect();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioService, decorators: [{
type: Injectable
}] });
class AudioServiceModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioServiceModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: AudioServiceModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioServiceModule, providers: [AudioService, provideHttpClient(withInterceptorsFromDi())] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AudioServiceModule, decorators: [{
type: NgModule,
args: [{ imports: [], providers: [AudioService, provideHttpClient(withInterceptorsFromDi())] }]
}] });
class FileSizePipe {
transform(value) {
// allow for async values
if (!value) {
return value;
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
// calculate the which unit bracket the values should be a part of
const idx = Math.floor(Math.log(value) / Math.log(1024));
const formattedValue = value / Math.pow(1024, idx);
return `${formattedValue.toFixed(2)} ${units[idx]}`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipe, isStandalone: false, name: "fileSize" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipe, decorators: [{
type: Pipe,
args: [{
name: 'fileSize',
standalone: false,
}]
}] });
class MediaPlayerComponent {
get source() {
return this.mediaPlayerService.source;
}
/** The url to the media file to be loaded by the media player. */
set source(value) {
this.mediaPlayerService.source = value;
}
get type() {
return this.mediaPlayerService.type;
}
/**
* Defines the appearance of the media player. The two possible values are `video` and `audio`.
* The media player will adapt it's appearance to best suit the type specified.
*/
set type(value) {
this.mediaPlayerService.type = value;
}
get quietMode() {
return this.mediaPlayerService.quietMode;
}
/**
* If enabled, the controls in the media player will be hidden unless the mouse is over the player and will appear in a darker style.
* Dark mode is automatically enabled in full screen mode. Quiet mode is only available for videos.
*/
set quietMode(value) {
this.mediaPlayerService.quietMode = value;
}
/**
* If specified the function will be called passing the current volume as an argument.
* It should return an appropriate aria-label for the mute/unmute button.
*/
set muteAriaLabel(fn) {
this.mediaPlayerService.muteAriaLabel = fn;
}
/**
* If specified the function will be called passing the current playing state as an argument.
* It should return an appropriate aria-label for the play/pause button.
*/
set playAriaLabel(fn) {
this.mediaPlayerService.playAriaLabel = fn;
}
/**
* If specified the function will be called passing the current fullscreen state as an argument.
* It should return an appropriate aria-label for the fullscreen toggle button.
*/
set fullscreenAriaLabel(fn) {
this.mediaPlayerService.fullscreenAriaLabel = fn;
}
/**
* If specified the function will be called passing the current track as an argument.
* It should return an appropriate aria-label for the subtitle selection button.
*/
set selectSubtitlesAriaLabel(fn) {
this.mediaPlayerService.selectSubtitlesAriaLabel = fn;
}
/** Defines an aria-label for the go to start button. */
set goToStartAriaLabel(ariaLabel) {
this.mediaPlayerService.goToStartAriaLabel = ariaLabel;
}
/** Defines an aria-label for the go to end button. */
set goToEndAriaLabel(ariaLabel) {
this.mediaPlayerService.goToEndAriaLabel = ariaLabel;
}
/** Defines an aria-label for the title displayed in the subtitle selection popover. */
set subtitlesTitleAriaLabel(ariaLabel) {
this.mediaPlayerService.subtitlesTitleAriaLabel = ariaLabel;
}
/** Defines an aria-label to indicate subtitle are not currently enabled. */
set subtitlesOffAriaLabel(ariaLabel) {
this.mediaPlayerService.subtitlesOffAriaLabel = ariaLabel;
}
/** Define an aria-label to indicate there are no subtitles available. */
set noSubtitlesAriaLabel(ariaLabel) {
this.mediaPlayerService.noSubtitlesAriaLabel = ariaLabel;
}
/** Define an aria-label for the media player. */
set mediaPlayerAriaLabel(ariaLabel) {
this.mediaPlayerService.mediaPlayerAriaLabel = ariaLabel;
}
/** Define an aria-label for the the seek element. */
set seekAriaLabel(ariaLabel) {
this.mediaPlayerService.seekAriaLabel = ariaLabel;
}
constructor() {
this.mediaPlayerService = inject(MediaPlayerService);
this._audioService = inject(AudioService);
this._elementRef = inject(ElementRef);
this.hovering = false;
this.focused = false;
/** The `anonymous` keyword means that there will be no exchange of user credentials when the media source is fetched. */
this.crossorigin = 'use-credentials';
this._onDestroy = new Subject();
// show controls when hovering and in quiet mode
fromEvent(this._elementRef.nativeElement, 'mousemove')
.pipe(tap(() => (this.hovering = true)), debounceTime(2000), takeUntil(this._onDestroy))
.subscribe(() => (this.hovering = false));
}
ngAfterViewInit() {
this.mediaPlayerService.setMediaPlayer(this._elementRef.nativeElement, this._playerRef.nativeElement);
this.audioMetadata = this._audioService.getAudioFileMetadata(this._playerRef.nativeElement);
this.mediaPlayerService.playingEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.mediaPlayerService.playing.next(true));
this.mediaPlayerService.pauseEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.mediaPlayerService.playing.next(false));
this.mediaPlayerService.mediaClickEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.mediaPlayerService.togglePlay());
this.mediaPlayerService.loadedMetadataEvent
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this.mediaPlayerService.loaded = true));
// initially hide all text tracks
this.mediaPlayerService.hideSubtitleTracks();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: MediaPlayerComponent, isStandalone: false, selector: "ux-media-player", inputs: { crossorigin: "crossorigin", filename: "filename", source: "source", type: "type", quietMode: "quietMode", muteAriaLabel: "muteAriaLabel", playAriaLabel: "playAriaLabel", fullscreenAriaLabel: "fullscreenAriaLabel", selectSubtitlesAriaLabel: "selectSubtitlesAriaLabel", goToStartAriaLabel: "goToStartAriaLabel", goToEndAriaLabel: "goToEndAriaLabel", subtitlesTitleAriaLabel: "subtitlesTitleAriaLabel", subtitlesOffAriaLabel: "subtitlesOffAriaLabel", noSubtitlesAriaLabel: "noSubtitlesAriaLabel", mediaPlayerAriaLabel: "mediaPlayerAriaLabel", seekAriaLabel: "seekAriaLabel" }, host: { listeners: { "keydown.Space": "mediaPlayerService.togglePlay(); $event.preventDefault()", "mouseenter": "hovering = true", "mouseleave": "hovering = false", "document:fullscreenchange": "mediaPlayerService.fullscreenChange()", "document:webkitfullscreenchange": "mediaPlayerService.fullscreenChange()", "document:mozfullscreenchange": "mediaPlayerService.fullscreenChange()", "document:MSFullscreenChange": "mediaPlayerService.fullscreenChange()" }, properties: { "class.standard": "!mediaPlayerService.fullscreen", "class.fullscreen": "mediaPlayerService.fullscreen", "class.quiet": "quietMode && type === \"video\" || mediaPlayerService.fullscreen", "class.hover": "hovering || focused", "class.video": "type === \"video\"", "class.audio": "type === \"audio\"" } }, providers: [MediaPlayerService], viewQueries: [{ propertyName: "_playerRef", first: true, predicate: ["player"], descendants: true }], ngImport: i0, template: "<div\n class=\"player-container\"\n uxFocusIndicator\n tabindex=\"0\"\n [attr.aria-label]=\"mediaPlayerService.mediaPlayerAriaLabel\"\n [cdkTrapFocus]=\"mediaPlayerService.fullscreen\"\n>\n @if (type === 'video') {\n <div class=\"video-player-container\">\n <video\n class=\"video-player\"\n #player\n tabindex=\"-1\"\n [src]=\"source\"\n [crossOrigin]=\"crossorigin\"\n (abort)=\"mediaPlayerService.abortEvent.next()\"\n (canplay)=\"mediaPlayerService.canPlayEvent.next(true)\"\n (canplaythrough)=\"mediaPlayerService.canPlayThroughEvent.next(true)\"\n (durationchange)=\"mediaPlayerService.durationChangeEvent.next(player.duration)\"\n (ended)=\"mediaPlayerService.endedEvent.next()\"\n (error)=\"mediaPlayerService.errorEvent.next($event)\"\n (loadeddata)=\"mediaPlayerService.loadedDataEvent.next($event)\"\n (loadedmetadata)=\"mediaPlayerService.loadedMetadataEvent.next($event)\"\n (loadstart)=\"mediaPlayerService.loadStartEvent.next()\"\n (pause)=\"mediaPlayerService.pauseEvent.next()\"\n (play)=\"mediaPlayerService.playEvent.next()\"\n (playing)=\"mediaPlayerService.playingEvent.next(!player.paused)\"\n (ratechange)=\"mediaPlayerService.rateChangeEvent.next(player.playbackRate)\"\n (seeked)=\"mediaPlayerService.seekedEvent.next(player.currentTime)\"\n (seeking)=\"mediaPlayerService.seekingEvent.next(player.currentTime)\"\n (stalled)=\"mediaPlayerService.stalledEvent.next()\"\n (suspend)=\"mediaPlayerService.suspendEvent.next()\"\n (timeupdate)=\"mediaPlayerService.timeUpdateEvent.next(player.currentTime)\"\n (volumechange)=\"mediaPlayerService.volumeChangeEvent.next(player.volume)\"\n (waiting)=\"mediaPlayerService.waitingEvent.next()\"\n (click)=\"mediaPlayerService.mediaClickEvent.next($event)\"\n >\n <ng-content select=\"track\"></ng-content>\n </video>\n <div class=\"video-overlay\" [class.playing]=\"mediaPlayerService.playing | async\">\n <svg class=\"play-graphic\" x=\"0px\" y=\"0px\" viewBox=\"0 0 64 64\">\n <circle class=\"play-circle\" cx=\"32.2\" cy=\"31.8\" r=\"31.8\" />\n <polygon class=\"play-triangle\" points=\"23,14.1 23,50.8 48.3,32.5\" />\n </svg>\n </div>\n </div>\n }\n\n @if (type === 'audio') {\n <div class=\"audio-player\">\n <svg width=\"24px\" height=\"24px\" viewBox=\"0 0 24 24\">\n <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\n <g transform=\"translate(-98.000000, -458.000000)\">\n <g transform=\"translate(98.000000, 458.000000)\">\n <path\n d=\"M4.5,0.5 L18.0435308,0.5 L23.5,6.22251502 L23.5,23.5 L4.5,23.5 L4.5,0.5 Z\"\n fill=\"#CCEAE2\"\n ></path>\n <path\n d=\"M4.5,8 L4.5,0.5 L18,0.5 L23.5,6 L23.5,23.5 L18,23.5\"\n stroke=\"#60798D\"\n fill=\"#CCEAE2\"\n ></path>\n <path\n d=\"M4,13.5 L0.5,13.5 L0.5,18.5 L4,18.5 L9.5,22.5 L9.5,9.5 L4,13.5 Z\"\n stroke=\"#60798D\"\n fill=\"#85D2BE\"\n ></path>\n <path\n d=\"M11.5,12.5137939 C13.7576225,12.5137939 14.5,14.3709236 14.5,16 C14.5,17.6849236 13.7089152,19.5420532 11.5,19.5420532\"\n stroke=\"#60798D\"\n ></path>\n <path\n d=\"M11.5,9 C15.8037643,9.04168701 18.5,11.6604805 18.5,16 C18.5,20.3395195 15.8804302,23.0079956 11.5,23\"\n stroke=\"#60798D\"\n ></path>\n <path\n d=\"M17.5219116,0.761413574 L17.5219116,6 L23,6\"\n stroke=\"#60798D\"\n fill=\"#85D2BE\"\n ></path>\n </g>\n </g>\n </g>\n </svg>\n <p class=\"audio-file-name\">\n {{ this.filename ? this.filename : (audioMetadata | async)?.filename }}\n </p>\n <p class=\"audio-file-format\">{{ (audioMetadata | async)?.description }}</p>\n <p class=\"audio-file-size\">{{ (audioMetadata | async)?.size | fileSize }}</p>\n <audio\n #player\n [src]=\"source\"\n (abort)=\"mediaPlayerService.abortEvent.next()\"\n (canplay)=\"mediaPlayerService.canPlayEvent.next(true)\"\n (canplaythrough)=\"mediaPlayerService.canPlayThroughEvent.next(true)\"\n (durationchange)=\"mediaPlayerService.durationChangeEvent.next(player.duration)\"\n (ended)=\"mediaPlayerService.endedEvent.next()\"\n (error)=\"mediaPlayerService.errorEvent.next($event)\"\n (loadeddata)=\"mediaPlayerService.loadedDataEvent.next($event)\"\n (loadedmetadata)=\"mediaPlayerService.loadedMetadataEvent.next($event)\"\n (loadstart)=\"mediaPlayerService.loadStartEvent.next()\"\n (pause)=\"mediaPlayerService.pauseEvent.next()\"\n (play)=\"mediaPlayerService.playEvent.next()\"\n (playing)=\"mediaPlayerService.playingEvent.next(!player.paused)\"\n (ratechange)=\"mediaPlayerService.rateChangeEvent.next(player.playbackRate)\"\n (seeked)=\"mediaPlayerService.seekedEvent.next(player.currentTime)\"\n (seeking)=\"mediaPlayerService.seekingEvent.next(player.currentTime)\"\n (stalled)=\"mediaPlayerService.stalledEvent.next()\"\n (suspend)=\"mediaPlayerService.suspendEvent.next()\"\n (timeupdate)=\"mediaPlayerService.timeUpdateEvent.next(player.currentTime)\"\n (volumechange)=\"mediaPlayerService.volumeChangeEvent.next(player.volume)\"\n (waiting)=\"mediaPlayerService.waitingEvent.next()\"\n (click)=\"mediaPlayerService.mediaClickEvent.next($event)\"\n ></audio>\n </div>\n }\n\n <div class=\"control-bar\" (uxFocusWithin)=\"focused = true\" (uxBlurWithin)=\"focused = false\">\n <ux-media-player-timeline></ux-media-player-timeline>\n <ux-media-player-controls>\n <ng-content select=\"[uxMediaPlayerCustomControl]\"></ng-content>\n </ux-media-player-controls>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1$1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusWithinDirective, selector: "[uxFocusWithin],[uxBlurWithin]", outputs: ["uxFocusWithin", "uxBlurWithin"] }, { kind: "component", type: MediaPlayerTimelineExtensionComponent, selector: "ux-media-player-timeline" }, { kind: "component", type: MediaPlayerControlsExtensionComponent, selector: "ux-media-player-controls" }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-media-player', providers: [MediaPlayerService], host: {
'(keydown.Space)': 'mediaPlayerService.togglePlay(); $event.preventDefault()',
'[class.standard]': '!mediaPlayerService.fullscreen',
'[class.fullscreen]': 'mediaPlayerService.fullscreen',
'[class.quiet]': 'quietMode && type === "video" || mediaPlayerService.fullscreen',
'[class.hover]': 'hovering || focused',
'[class.video]': 'type === "video"',
'[class.audio]': 'type === "audio"',
'(mouseenter)': 'hovering = true',
'(mouseleave)': 'hovering = false',
'(document:fullscreenchange)': 'mediaPlayerService.fullscreenChange()',
'(document:webkitfullscreenchange)': 'mediaPlayerService.fullscreenChange()',
'(document:mozfullscreenchange)': 'mediaPlayerService.fullscreenChange()',
'(document:MSFullscreenChange)': 'mediaPlayerService.fullscreenChange()',
}, standalone: false, template: "<div\n class=\"player-container\"\n uxFocusIndicator\n tabindex=\"0\"\n [attr.aria-label]=\"mediaPlayerService.mediaPlayerAriaLabel\"\n [cdkTrapFocus]=\"mediaPlayerService.fullscreen\"\n>\n @if (type === 'video') {\n <div class=\"video-player-container\">\n <video\n class=\"video-player\"\n #player\n tabindex=\"-1\"\n [src]=\"source\"\n [crossOrigin]=\"crossorigin\"\n (abort)=\"mediaPlayerService.abortEvent.next()\"\n (canplay)=\"mediaPlayerService.canPlayEvent.next(true)\"\n (canplaythrough)=\"mediaPlayerService.canPlayThroughEvent.next(true)\"\n (durationchange)=\"mediaPlayerService.durationChangeEvent.next(player.duration)\"\n (ended)=\"mediaPlayerService.endedEvent.next()\"\n (error)=\"mediaPlayerService.errorEvent.next($event)\"\n (loadeddata)=\"mediaPlayerService.loadedDataEvent.next($event)\"\n (loadedmetadata)=\"mediaPlayerService.loadedMetadataEvent.next($event)\"\n (loadstart)=\"mediaPlayerService.loadStartEvent.next()\"\n (pause)=\"mediaPlayerService.pauseEvent.next()\"\n (play)=\"mediaPlayerService.playEvent.next()\"\n (playing)=\"mediaPlayerService.playingEvent.next(!player.paused)\"\n (ratechange)=\"mediaPlayerService.rateChangeEvent.next(player.playbackRate)\"\n (seeked)=\"mediaPlayerService.seekedEvent.next(player.currentTime)\"\n (seeking)=\"mediaPlayerService.seekingEvent.next(player.currentTime)\"\n (stalled)=\"mediaPlayerService.stalledEvent.next()\"\n (suspend)=\"mediaPlayerService.suspendEvent.next()\"\n (timeupdate)=\"mediaPlayerService.timeUpdateEvent.next(player.currentTime)\"\n (volumechange)=\"mediaPlayerService.volumeChangeEvent.next(player.volume)\"\n (waiting)=\"mediaPlayerService.waitingEvent.next()\"\n (click)=\"mediaPlayerService.mediaClickEvent.next($event)\"\n >\n <ng-content select=\"track\"></ng-content>\n </video>\n <div class=\"video-overlay\" [class.playing]=\"mediaPlayerService.playing | async\">\n <svg class=\"play-graphic\" x=\"0px\" y=\"0px\" viewBox=\"0 0 64 64\">\n <circle class=\"play-circle\" cx=\"32.2\" cy=\"31.8\" r=\"31.8\" />\n <polygon class=\"play-triangle\" points=\"23,14.1 23,50.8 48.3,32.5\" />\n </svg>\n </div>\n </div>\n }\n\n @if (type === 'audio') {\n <div class=\"audio-player\">\n <svg width=\"24px\" height=\"24px\" viewBox=\"0 0 24 24\">\n <g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\n <g transform=\"translate(-98.000000, -458.000000)\">\n <g transform=\"translate(98.000000, 458.000000)\">\n <path\n d=\"M4.5,0.5 L18.0435308,0.5 L23.5,6.22251502 L23.5,23.5 L4.5,23.5 L4.5,0.5 Z\"\n fill=\"#CCEAE2\"\n ></path>\n <path\n d=\"M4.5,8 L4.5,0.5 L18,0.5 L23.5,6 L23.5,23.5 L18,23.5\"\n stroke=\"#60798D\"\n fill=\"#CCEAE2\"\n ></path>\n <path\n d=\"M4,13.5 L0.5,13.5 L0.5,18.5 L4,18.5 L9.5,22.5 L9.5,9.5 L4,13.5 Z\"\n stroke=\"#60798D\"\n fill=\"#85D2BE\"\n ></path>\n <path\n d=\"M11.5,12.5137939 C13.7576225,12.5137939 14.5,14.3709236 14.5,16 C14.5,17.6849236 13.7089152,19.5420532 11.5,19.5420532\"\n stroke=\"#60798D\"\n ></path>\n <path\n d=\"M11.5,9 C15.8037643,9.04168701 18.5,11.6604805 18.5,16 C18.5,20.3395195 15.8804302,23.0079956 11.5,23\"\n stroke=\"#60798D\"\n ></path>\n <path\n d=\"M17.5219116,0.761413574 L17.5219116,6 L23,6\"\n stroke=\"#60798D\"\n fill=\"#85D2BE\"\n ></path>\n </g>\n </g>\n </g>\n </svg>\n <p class=\"audio-file-name\">\n {{ this.filename ? this.filename : (audioMetadata | async)?.filename }}\n </p>\n <p class=\"audio-file-format\">{{ (audioMetadata | async)?.description }}</p>\n <p class=\"audio-file-size\">{{ (audioMetadata | async)?.size | fileSize }}</p>\n <audio\n #player\n [src]=\"source\"\n (abort)=\"mediaPlayerService.abortEvent.next()\"\n (canplay)=\"mediaPlayerService.canPlayEvent.next(true)\"\n (canplaythrough)=\"mediaPlayerService.canPlayThroughEvent.next(true)\"\n (durationchange)=\"mediaPlayerService.durationChangeEvent.next(player.duration)\"\n (ended)=\"mediaPlayerService.endedEvent.next()\"\n (error)=\"mediaPlayerService.errorEvent.next($event)\"\n (loadeddata)=\"mediaPlayerService.loadedDataEvent.next($event)\"\n (loadedmetadata)=\"mediaPlayerService.loadedMetadataEvent.next($event)\"\n (loadstart)=\"mediaPlayerService.loadStartEvent.next()\"\n (pause)=\"mediaPlayerService.pauseEvent.next()\"\n (play)=\"mediaPlayerService.playEvent.next()\"\n (playing)=\"mediaPlayerService.playingEvent.next(!player.paused)\"\n (ratechange)=\"mediaPlayerService.rateChangeEvent.next(player.playbackRate)\"\n (seeked)=\"mediaPlayerService.seekedEvent.next(player.currentTime)\"\n (seeking)=\"mediaPlayerService.seekingEvent.next(player.currentTime)\"\n (stalled)=\"mediaPlayerService.stalledEvent.next()\"\n (suspend)=\"mediaPlayerService.suspendEvent.next()\"\n (timeupdate)=\"mediaPlayerService.timeUpdateEvent.next(player.currentTime)\"\n (volumechange)=\"mediaPlayerService.volumeChangeEvent.next(player.volume)\"\n (waiting)=\"mediaPlayerService.waitingEvent.next()\"\n (click)=\"mediaPlayerService.mediaClickEvent.next($event)\"\n ></audio>\n </div>\n }\n\n <div class=\"control-bar\" (uxFocusWithin)=\"focused = true\" (uxBlurWithin)=\"focused = false\">\n <ux-media-player-timeline></ux-media-player-timeline>\n <ux-media-player-controls>\n <ng-content select=\"[uxMediaPlayerCustomControl]\"></ng-content>\n </ux-media-player-controls>\n </div>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { _playerRef: [{
type: ViewChild,
args: ['player', { static: false }]
}], crossorigin: [{
type: Input
}], filename: [{
type: Input
}], source: [{
type: Input
}], type: [{
type: Input
}], quietMode: [{
type: Input
}], muteAriaLabel: [{
type: Input
}], playAriaLabel: [{
type: Input
}], fullscreenAriaLabel: [{
type: Input
}], selectSubtitlesAriaLabel: [{
type: Input
}], goToStartAriaLabel: [{
type: Input
}], goToEndAriaLabel: [{
type: Input
}], subtitlesTitleAriaLabel: [{
type: Input
}], subtitlesOffAriaLabel: [{
type: Input
}], noSubtitlesAriaLabel: [{
type: Input
}], mediaPlayerAriaLabel: [{
type: Input
}], seekAriaLabel: [{
type: Input
}] } });
class DurationPipeModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DurationPipeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: DurationPipeModule, declarations: [DurationPipe], exports: [DurationPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DurationPipeModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DurationPipeModule, decorators: [{
type: NgModule,
args: [{
exports: [DurationPipe],
declarations: [DurationPipe],
}]
}] });
class FileSizePipeModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipeModule, declarations: [FileSizePipe], exports: [FileSizePipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipeModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FileSizePipeModule, decorators: [{
type: NgModule,
args: [{
exports: [FileSizePipe],
declarations: [FileSizePipe],
}]
}] });
const DECLARATIONS$4 = [
MediaPlayerComponent,
MediaPlayerTimelineExtensionComponent,
MediaPlayerBaseExtensionDirective,
MediaPlayerControlsExtensionComponent,
MediaPlayerCustomControlDirective,
];
class MediaPlayerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerModule, declarations: [MediaPlayerComponent,
MediaPlayerTimelineExtensionComponent,
MediaPlayerBaseExtensionDirective,
MediaPlayerControlsExtensionComponent,
MediaPlayerCustomControlDirective], imports: [A11yModule,
AccessibilityModule,
AudioServiceModule,
ClickOutsideModule,
CommonModule,
DurationPipeModule,
FileSizePipeModule,
IconModule,
SliderModule,
TooltipModule], exports: [MediaPlayerComponent,
MediaPlayerTimelineExtensionComponent,
MediaPlayerBaseExtensionDirective,
MediaPlayerControlsExtensionComponent,
MediaPlayerCustomControlDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerModule, imports: [A11yModule,
AccessibilityModule,
AudioServiceModule,
ClickOutsideModule,
CommonModule,
DurationPipeModule,
FileSizePipeModule,
IconModule,
SliderModule,
TooltipModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MediaPlayerModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
AudioServiceModule,
ClickOutsideModule,
CommonModule,
DurationPipeModule,
FileSizePipeModule,
IconModule,
SliderModule,
TooltipModule,
],
exports: DECLARATIONS$4,
declarations: DECLARATIONS$4,
}]
}] });
class NavigationItemComponent {
/** Get the active state of this item from the router */
get active() {
return this.link ? this._router.isActive(this.link, true) : false;
}
get children() {
return this._children.filter(item => item !== this);
}
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._router = inject(Router);
this._parent = inject(NavigationItemComponent, { optional: true, skipSelf: true });
/** Whether the navigation item is expanded, displaying the items from the `children` array. */
this.expanded = false;
/** Indicate whether the indentation should include the arrow */
this._indentWithoutArrow = true;
/** Automatically unsubscribe when the component is destroyed */
this._onDestroy = new Subject();
this._level = this._parent ? this._parent._level + 1 : 1;
// Expand this component if it or a descendant is active.
this._router.events
.pipe(filter(event => event instanceof NavigationEnd), takeUntil(this._onDestroy))
.subscribe(() => {
this.expanded = this.hasActiveLink(this.link);
});
}
ngAfterViewInit() {
// Add classes to parent for styling
const parentListElement = this._elementRef.nativeElement.parentElement;
if (parentListElement) {
const levelClass = this.getLevelClass();
if (levelClass.length > 0) {
this._renderer.addClass(parentListElement, 'nav');
this._renderer.addClass(parentListElement, levelClass);
}
}
}
ngAfterContentInit() {
// Set 'indentWithoutArrow'
this.setIndentWithoutArrow();
// Update 'indentWithoutArrow' in response to changes to children
this._children.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.setIndentWithoutArrow());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Check if this item or any children are active */
hasActiveLink(link) {
// If this component has a link, check if it is active.
if (link && this._router.isActive(link, true)) {
return true;
}
// If this component has children, check if any of them, or their descendants, are active.
return this.children.some(item => item.hasActiveLink(item.link));
}
getLevelClass() {
switch (this._level) {
case 2:
return 'nav-second-level';
case 3:
return 'nav-third-level';
case 4:
return 'nav-fourth-level';
case 5:
return 'nav-fifth-level';
}
return '';
}
setIndentWithoutArrow() {
if (this.children.length > 0) {
// If this element has children it will be indented and will have an arrow
this._indentWithoutArrow = false;
}
else if (this._parent) {
// If this element has a parent, indent it if any of its siblings have children
this._indentWithoutArrow = !this._parent.children.every(item => item.children.length === 0);
}
else {
// Top-level elements should be indented
this._indentWithoutArrow = true;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NavigationItemComponent, isStandalone: false, selector: "[ux-navigation-item]", inputs: { header: "header", icon: "icon", expanded: "expanded", link: "link" }, host: { properties: { "class.active": "active", "class.selected": "expanded" } }, queries: [{ propertyName: "_children", predicate: NavigationItemComponent, descendants: true }], ngImport: i0, template: "@if (link) {\n <a\n [class.has-arrow]=\"children.length > 0\"\n [class.no-arrow]=\"_indentWithoutArrow\"\n [routerLink]=\"link\"\n >\n <span>{{ header }}</span>\n </a>\n}\n\n@if (!link) {\n <a\n (click)=\"expanded = !expanded\"\n [class.has-arrow]=\"children.length > 0\"\n [class.no-arrow]=\"_indentWithoutArrow\"\n >\n <span>{{ header }}</span>\n </a>\n}\n\n<ng-content></ng-content>\n", dependencies: [{ kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationItemComponent, decorators: [{
type: Component,
args: [{ selector: '[ux-navigation-item]', host: {
'[class.active]': 'active',
'[class.selected]': 'expanded',
}, standalone: false, template: "@if (link) {\n <a\n [class.has-arrow]=\"children.length > 0\"\n [class.no-arrow]=\"_indentWithoutArrow\"\n [routerLink]=\"link\"\n >\n <span>{{ header }}</span>\n </a>\n}\n\n@if (!link) {\n <a\n (click)=\"expanded = !expanded\"\n [class.has-arrow]=\"children.length > 0\"\n [class.no-arrow]=\"_indentWithoutArrow\"\n >\n <span>{{ header }}</span>\n </a>\n}\n\n<ng-content></ng-content>\n" }]
}], ctorParameters: () => [], propDecorators: { header: [{
type: Input
}], icon: [{
type: Input
}], expanded: [{
type: Input
}], link: [{
type: Input
}], _children: [{
type: ContentChildren,
args: [NavigationItemComponent, { descendants: true }]
}] } });
const NAVIGATION_MODULE_OPTIONS = new InjectionToken('NAVIGATION_MODULE_OPTIONS');
class NavigationService {
constructor() {
/** Whether to collapse other menu items when expanding a menu item. */
this.autoCollapse = true;
/** Emit when the expanded state has changed */
this.expanded$ = new Subject();
}
ngOnDestroy() {
this.expanded$.complete();
}
/** Set the expanded state of an item */
setExpanded(source, expanded) {
if (expanded && this.autoCollapse) {
this.collapseSiblings(source);
this.expanded$.next();
}
}
/** Collapse all siblings nodes */
collapseSiblings(source) {
let siblings = this.items;
for (const item of this.items) {
const parent = this.getParent(source, item);
if (parent) {
siblings = parent.children;
break;
}
}
// collapse every sibling
siblings.filter(item => item !== source).forEach(item => this.collapseAll(item));
}
/** Collapse an item and all its children */
collapseAll(item) {
item.expanded = false;
if (item.children) {
item.children.forEach(child => this.collapseAll(child));
}
}
/** Get a nodes parent if it has one */
getParent(target, item) {
return (item.children || []).find(child => child === target) ? item : null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationService, decorators: [{
type: Injectable
}] });
class NavigationLinkDirective {
constructor() {
this._router = inject(Router);
this._locationStrategy = inject(LocationStrategy);
this._navigationService = inject(NavigationService);
this._changeDetector = inject(ChangeDetectorRef);
this._route = inject(ActivatedRoute);
this._options = inject(NAVIGATION_MODULE_OPTIONS, { optional: true });
/** Emit with the current expaned state */
this._expanded$ = new Subject();
/** Unsubscribe from all observables when this directive is destroyed */
this._onDestroy = new Subject();
}
/** The expaned state of this item */
set expanded(value) {
this._expanded$.next(value);
}
ngOnInit() {
// any time expanded state anywhere change we should run change detection in case we should collapse
this._navigationService.expanded$
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._changeDetector.markForCheck());
this._expanded$.pipe(tick(), takeUntil(this._onDestroy)).subscribe(expanded => {
if (this.navigationItem.children && this.navigationItem.children.length > 0) {
this.ariaExpanded = expanded;
this._navigationService.setExpanded(this.navigationItem, expanded);
}
});
this._router.events
.pipe(filter(event => event instanceof NavigationEnd), takeUntil(this._onDestroy))
.subscribe(this.updateNavigationState.bind(this));
this.updateNavigationState();
this.updateAttributes();
}
ngOnChanges() {
this.updateAttributes();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
activated(event) {
if (this.navigationItem.disabled) {
return false;
}
if (this.navigationItem.routerLink) {
const commands = Array.isArray(this.navigationItem.routerLink)
? this.navigationItem.routerLink
: [this.navigationItem.routerLink];
this._router.navigate(commands, this.navigationItem.routerExtras);
}
// Toggle expanded state (relevant only if it has children)
this.navigationItem.expanded = !this.navigationItem.expanded;
// Invoke the custom click handler if specified
if (this.navigationItem.click) {
this.navigationItem.click(event, this.navigationItem);
}
return false;
}
updateNavigationState() {
this.isActive = this.isActiveItem(this.navigationItem);
if (this.navigationItem.children) {
const activeChild = this.navigationItem.children.find(child => this.isActiveItem(child));
if (activeChild) {
this.navigationItem.expanded = true;
}
}
this._changeDetector.markForCheck();
}
updateAttributes() {
this.href = this.getHref();
this.role =
this.navigationItem.children && this.navigationItem.children.length > 0
? 'button'
: 'treeitem';
this.indentChildren =
this.navigationItem.children &&
this.navigationItem.children.some(item => item.children && item.children.length > 0);
}
getHref() {
if (this.navigationItem.disabled) {
return null;
}
if (this.navigationItem.routerLink) {
const commands = Array.isArray(this.navigationItem.routerLink)
? this.navigationItem.routerLink
: [this.navigationItem.routerLink];
const urlTree = this._router.createUrlTree(commands, this.navigationItem.routerExtras);
return this._locationStrategy.prepareExternalUrl(this._router.serializeUrl(urlTree));
}
return null;
}
isActiveItem(item) {
const { exact, ignoreQueryParams } = this.getRouterOptions(item);
if (item.routerLink) {
let routerExtras = item.routerExtras;
// if we are to ignore the query params we must remove them
if (ignoreQueryParams) {
// get the current actual query params
const { queryParams } = this._route.snapshot;
// override the provided query params with the actual query params so they will alway match
routerExtras = { ...routerExtras, queryParams };
}
const commands = Array.isArray(item.routerLink) ? item.routerLink : [item.routerLink];
const urlTree = this._router.createUrlTree(commands, routerExtras);
return this._router.isActive(urlTree, exact);
}
return false;
}
/** Get the router options with defaults for missing properties */
getRouterOptions(item) {
// get the default options based on the ones provided in `forRoot`
const defaultOptions = {
exact: true,
ignoreQueryParams: false,
...(this._options ? this._options.routerOptions : {}),
};
// if there are item specific router options they should take precendence
return { ...defaultOptions, ...item.routerOptions };
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationLinkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: NavigationLinkDirective, isStandalone: false, selector: "[uxNavigationLink]", inputs: { navigationItem: "navigationItem", expanded: "expanded", canExpand: "canExpand", indent: "indent" }, host: { listeners: { "click": "activated($event)", "keydown.enter": "activated($event)" }, properties: { "class.indent": "this.indent", "attr.href": "this.href", "attr.role": "this.role", "attr.aria-expanded": "this.ariaExpanded" } }, exportAs: ["uxNavigationLink"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationLinkDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxNavigationLink]',
exportAs: 'uxNavigationLink',
standalone: false,
}]
}], propDecorators: { navigationItem: [{
type: Input
}], expanded: [{
type: Input
}], canExpand: [{
type: Input
}], indent: [{
type: Input
}, {
type: HostBinding,
args: ['class.indent']
}], href: [{
type: HostBinding,
args: ['attr.href']
}], role: [{
type: HostBinding,
args: ['attr.role']
}], ariaExpanded: [{
type: HostBinding,
args: ['attr.aria-expanded']
}], activated: [{
type: HostListener,
args: ['click', ['$event']]
}, {
type: HostListener,
args: ['keydown.enter', ['$event']]
}] } });
class NavigationComponent {
constructor() {
this._navigationService = inject(NavigationService);
/** Whether to present the menu as a hierarchical tree. */
this.tree = true;
/** The classes to be added to each different level */
this._hierarchyClasses = [
'',
'nav-second-level',
'nav-third-level',
'nav-fourth-level',
'nav-fifth-level',
];
}
/** The navigation items to populate the menu with. */
set items(items) {
this._navigationService.items = items;
}
get items() {
return this._navigationService.items;
}
/** Whether to collapse other menu items when expanding a menu item. */
set autoCollapse(autoCollapse) {
this._navigationService.autoCollapse = autoCollapse;
}
get _depthLimit() {
return this.tree ? this._hierarchyClasses.length : 2;
}
/**
* Returns true if the sets of items needs to be indented to make room for one or more expander.
*/
_needsIndent(items) {
return items && items.some(item => item.children && item.children.length > 0);
}
/** Determine the type of icon to display. We support `ux-icon` or `component` */
_getIconType(item) {
return getIconType(item.icon);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NavigationComponent, isStandalone: false, selector: "ux-navigation", inputs: { items: "items", tree: "tree", autoCollapse: "autoCollapse" }, providers: [NavigationService], queries: [{ propertyName: "navigationItemTemplate", first: true, predicate: ["uxNavigationItem"], descendants: true }], ngImport: i0, template: "<nav class=\"ux-side-nav\" [class.tree]=\"tree\" role=\"navigation\">\n @if (items) {\n <ol role=\"tree\" class=\"nav\" uxTabbableList [hierarchy]=\"true\">\n @for (item of items; track item; let rank = $index) {\n <ng-container\n [ngTemplateOutlet]=\"navigationNode\"\n [ngTemplateOutletContext]=\"{\n item: item,\n level: 1,\n rank: rank,\n indent: _needsIndent(items)\n }\"\n >\n </ng-container>\n }\n <ng-template\n #navigationNode\n let-item=\"item\"\n let-parent=\"parent\"\n let-level=\"level\"\n let-rank=\"rank\"\n let-indent=\"indent\"\n >\n <li\n [attr.role]=\"item.children && item.children.length > 0 ? 'treeitem' : 'none'\"\n [attr.aria-expanded]=\"item.children && item.children.length > 0 ? item.expanded : null\"\n [class.selected]=\"item.expanded\"\n [class.disabled]=\"item.disabled\"\n [class.active]=\"navigationLink.isActive\"\n >\n <a\n uxNavigationLink\n #navigationLink=\"uxNavigationLink\"\n #tli=\"ux-tabbable-list-item\"\n [navigationItem]=\"item\"\n [expanded]=\"item.expanded\"\n [canExpand]=\"level < _depthLimit\"\n [indent]=\"indent\"\n uxTabbableListItem\n [disabled]=\"item.disabled\"\n [parent]=\"parent\"\n [rank]=\"rank\"\n [(expanded)]=\"item.expanded\"\n >\n @if (\n !navigationItemTemplate &&\n item.children &&\n item.children.length > 0 &&\n level < _depthLimit\n ) {\n <span\n aria-hidden=\"true\"\n class=\"nav-expander\"\n (click)=\"\n item.expanded = !item.expanded; $event.stopPropagation(); $event.preventDefault()\n \"\n >\n </span>\n }\n <!-- Support UX Icons and Icon Component -->\n @if (!navigationItemTemplate && item.icon && !tree) {\n @if (_getIconType(item) !== 'component') {\n <span class=\"nav-icon\" [ngClass]=\"[_getIconType(item), item.icon]\"> </span>\n }\n @if (_getIconType(item) === 'component') {\n <ux-icon class=\"nav-icon\" [name]=\"item.icon\"> </ux-icon>\n }\n }\n @if (!navigationItemTemplate && item.iconUrl && !tree) {\n <img class=\"nav-icon\" [src]=\"item.iconUrl\" alt=\"item.iconLabel\" />\n }\n @if (!navigationItemTemplate) {\n <span class=\"nav-title\">{{ item.title }}</span>\n }\n <ng-container\n [ngTemplateOutlet]=\"navigationItemTemplate\"\n [ngTemplateOutletContext]=\"{ item: item, level: level }\"\n >\n </ng-container>\n </a>\n @if (item.children && item.expanded && level < _depthLimit) {\n <ol role=\"group\" class=\"nav\" [ngClass]=\"_hierarchyClasses[level]\">\n @for (child of item.children; track child; let rank = $index) {\n <ng-container\n [ngTemplateOutlet]=\"navigationNode\"\n [ngTemplateOutletContext]=\"{\n item: child,\n parent: tli,\n level: level + 1,\n rank: rank,\n indent: navigationLink.indentChildren\n }\"\n >\n </ng-container>\n }\n </ol>\n }\n </li>\n </ng-template>\n </ol>\n }\n\n <!-- Backward compatibility with the original ux-navigation -->\n @if (!items) {\n <ol role=\"tree\" class=\"nav\">\n <ng-content></ng-content>\n </ol>\n }\n</nav>\n", dependencies: [{ kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: NavigationLinkDirective, selector: "[uxNavigationLink]", inputs: ["navigationItem", "expanded", "canExpand", "indent"], exportAs: ["uxNavigationLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-navigation', providers: [NavigationService], changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, standalone: false, template: "<nav class=\"ux-side-nav\" [class.tree]=\"tree\" role=\"navigation\">\n @if (items) {\n <ol role=\"tree\" class=\"nav\" uxTabbableList [hierarchy]=\"true\">\n @for (item of items; track item; let rank = $index) {\n <ng-container\n [ngTemplateOutlet]=\"navigationNode\"\n [ngTemplateOutletContext]=\"{\n item: item,\n level: 1,\n rank: rank,\n indent: _needsIndent(items)\n }\"\n >\n </ng-container>\n }\n <ng-template\n #navigationNode\n let-item=\"item\"\n let-parent=\"parent\"\n let-level=\"level\"\n let-rank=\"rank\"\n let-indent=\"indent\"\n >\n <li\n [attr.role]=\"item.children && item.children.length > 0 ? 'treeitem' : 'none'\"\n [attr.aria-expanded]=\"item.children && item.children.length > 0 ? item.expanded : null\"\n [class.selected]=\"item.expanded\"\n [class.disabled]=\"item.disabled\"\n [class.active]=\"navigationLink.isActive\"\n >\n <a\n uxNavigationLink\n #navigationLink=\"uxNavigationLink\"\n #tli=\"ux-tabbable-list-item\"\n [navigationItem]=\"item\"\n [expanded]=\"item.expanded\"\n [canExpand]=\"level < _depthLimit\"\n [indent]=\"indent\"\n uxTabbableListItem\n [disabled]=\"item.disabled\"\n [parent]=\"parent\"\n [rank]=\"rank\"\n [(expanded)]=\"item.expanded\"\n >\n @if (\n !navigationItemTemplate &&\n item.children &&\n item.children.length > 0 &&\n level < _depthLimit\n ) {\n <span\n aria-hidden=\"true\"\n class=\"nav-expander\"\n (click)=\"\n item.expanded = !item.expanded; $event.stopPropagation(); $event.preventDefault()\n \"\n >\n </span>\n }\n <!-- Support UX Icons and Icon Component -->\n @if (!navigationItemTemplate && item.icon && !tree) {\n @if (_getIconType(item) !== 'component') {\n <span class=\"nav-icon\" [ngClass]=\"[_getIconType(item), item.icon]\"> </span>\n }\n @if (_getIconType(item) === 'component') {\n <ux-icon class=\"nav-icon\" [name]=\"item.icon\"> </ux-icon>\n }\n }\n @if (!navigationItemTemplate && item.iconUrl && !tree) {\n <img class=\"nav-icon\" [src]=\"item.iconUrl\" alt=\"item.iconLabel\" />\n }\n @if (!navigationItemTemplate) {\n <span class=\"nav-title\">{{ item.title }}</span>\n }\n <ng-container\n [ngTemplateOutlet]=\"navigationItemTemplate\"\n [ngTemplateOutletContext]=\"{ item: item, level: level }\"\n >\n </ng-container>\n </a>\n @if (item.children && item.expanded && level < _depthLimit) {\n <ol role=\"group\" class=\"nav\" [ngClass]=\"_hierarchyClasses[level]\">\n @for (child of item.children; track child; let rank = $index) {\n <ng-container\n [ngTemplateOutlet]=\"navigationNode\"\n [ngTemplateOutletContext]=\"{\n item: child,\n parent: tli,\n level: level + 1,\n rank: rank,\n indent: navigationLink.indentChildren\n }\"\n >\n </ng-container>\n }\n </ol>\n }\n </li>\n </ng-template>\n </ol>\n }\n\n <!-- Backward compatibility with the original ux-navigation -->\n @if (!items) {\n <ol role=\"tree\" class=\"nav\">\n <ng-content></ng-content>\n </ol>\n }\n</nav>\n" }]
}], propDecorators: { items: [{
type: Input
}], tree: [{
type: Input
}], autoCollapse: [{
type: Input
}], navigationItemTemplate: [{
type: ContentChild,
args: ['uxNavigationItem', { static: false }]
}] } });
class NavigationModule {
// allow options to be specified globally
static forRoot(options) {
return {
ngModule: NavigationModule,
providers: [{ provide: NAVIGATION_MODULE_OPTIONS, useValue: options }],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: NavigationModule, declarations: [NavigationComponent, NavigationItemComponent, NavigationLinkDirective], imports: [AccessibilityModule, CommonModule, IconModule, RouterModule], exports: [NavigationComponent, NavigationItemComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationModule, imports: [AccessibilityModule, CommonModule, IconModule, RouterModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NavigationModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, IconModule, RouterModule],
exports: [NavigationComponent, NavigationItemComponent],
declarations: [NavigationComponent, NavigationItemComponent, NavigationLinkDirective],
}]
}] });
class NestedDonutChartComponent {
constructor() {
this._colorService = inject(ColorService);
this._changeDetector = inject(ChangeDetectorRef);
this._elementRef = inject(ElementRef);
this._resizeService = inject(ResizeService);
/** Define the maximum range of the arcs */
this.max = 100;
/** Define the thickness of each arc */
this.thickness = 8;
/** Define the spacing of each arc */
this.spacing = 8;
/** Determine if we should show the hover effect */
this.disableHover = false;
/** Determine if we should show a tooltip on arc hover */
this.disableTooltip = false;
/** Determine the position of the tooltip */
this.tooltipPlacement = 'top';
/** Set the duration of the animation */
this.animationDuration = 750;
/** Emit whenever an arc is clicked */
this.itemClick = new EventEmitter();
/** Indicate if the tooltip should be visible */
this._tooltipVisible = false;
/** Store the previously processed data */
this._arcData = [];
/** Determine if the intial render has taken place */
this._isInitialized = false;
/** Unsubscribe from all observables automatically */
this._onDestroy = new Subject();
}
/** Determine the radius of the chart based on the specified size */
get _radius() {
return this._size / 2;
}
/**
* Get the size of the chart. The chart will always be square to
* the size will be the smaller of the width/height properties
*/
get _size() {
return Math.min(this._elementRef.nativeElement.offsetWidth, this._elementRef.nativeElement.offsetHeight);
}
/** Perform the initial render */
ngOnInit() {
// create the selection where we will draw the tracks
this._trackLayer = select(this._chartElement.nativeElement).append('g');
// create the selection where we will draw the arcs
this._arcLayer = select(this._chartElement.nativeElement).append('g');
// create the arcs representing the data
this.render();
// mark the component as initialized
this._isInitialized = true;
// listen for any resizing - skip the first emission as it always emits on first subscribe
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.render();
this._changeDetector.markForCheck();
});
}
/** Any time an input changes we must re-render the chart */
ngOnChanges() {
if (this._isInitialized) {
this.render();
}
}
ngOnDestroy() {
this._resizeService.removeResizeListener(this._elementRef.nativeElement);
this._onDestroy.next();
this._onDestroy.complete();
}
/** Inset the content so it never overlaps the arcs */
_getContentInset() {
return this.dataset.length * (this.spacing + this.thickness);
}
/** Get the dimensions of the content area */
_getContentSize() {
return this._size - this._getContentInset() * 2;
}
/** Get the dataset formated in an accessible manner */
_getAriaLabel() {
return this.dataset.map(data => `${data.value} ${data.name}`).join('. ');
}
/**
* Display the tracks and arcs defined by the dataset.
* We also provide the transition configuration so anytime the dataset
* changes we will animate the update.
*/
render() {
// update the transform of the layers
this._trackLayer.attr('transform', `translate(${this._radius}, ${this._radius})`);
this._arcLayer.attr('transform', `translate(${this._radius}, ${this._radius})`);
// create the arcs based on the dataset
this._arcs = this._arcLayer.selectAll('path').data(this.getChartData());
// create the default transition based on the specified duration
const arcTransition = transition$1('nestedDonutArcTransition')
.ease(easeCubic)
.duration(this.animationDuration);
// create the tracks based on the dataset
this._tracks = this._trackLayer
.selectAll('path')
.data(this.getChartData())
.enter()
.append('path')
.attr('class', 'ux-nested-donut-chart-track');
// set the track color on each render in case the input has changed
this._trackLayer
.selectAll('path')
.attr('d', this.getTrackArc())
.style('fill', () => this.getTrackColor());
// if an arc is removed then also remove the track
this._tracks.exit().remove();
// When a new arc is added we should create the element
// size it and provide the background color and begin the
// animation until it reaches its final angle
this._arcs
.enter()
.append('path')
.attr('class', 'ux-nested-donut-chart-arc')
.style('fill', data => this.getColor(data.color))
.attr('opacity', 1)
.on('click', data => this.itemClick.emit(data))
.on('mouseenter', (event, node) => this.onArcMouseEnter(event.srcElement, node))
.on('mousemove', event => this.onArcMouseMove(pointer(event, this._chartElement.nativeElement)))
.on('mouseleave', event => this.onArcMouseLeave(event.srcElement))
.transition(arcTransition)
.attrTween('d', this.getArcTween.bind(this));
// any time an existing dataset value changes
// we should update the angle with an animation
// we also animate any color changes also.
this._arcs
.transition(arcTransition)
.style('fill', data => this.getColor(data.color))
.attrTween('d', this.getArcTween.bind(this));
// when a dataset it removed animate the arc out
// and then remove the associated DOM element
this._arcs
.exit()
.transition(arcTransition)
.attrTween('d', this.getArcTween.bind(this))
.remove();
}
/** Get the interpolation function based on the new and previous angle */
getArcTween(data) {
// create a new interpolation function with a new endAngle
const interpolation = interpolate({ ...data, endAngle: data.previousEndAngle }, data);
// return the function that will produce the interpolation
return (delta) => this.getArc()(interpolation(delta));
}
/** Get the arc layout for a specific item in the dataset */
getArc() {
return arc()
.innerRadius(data => this.getArcRadius(data.index))
.outerRadius(data => this.getArcRadius(data.index) + this.thickness)
.startAngle(data => data.startAngle)
.endAngle(data => data.endAngle);
}
/**
* Get the track arc layout for a specific item in the dataset.
* This will match the arc of that represents the actual data
* however the endAngle will always be a complete circle
*/
getTrackArc() {
return this.getArc().endAngle(() => Math.PI * 2);
}
/**
* Get the radius of an arc. This is calculated
* based on the chart radius that has been defined,
* minus the thickness defined, then taking into account
* the depth of the arc and the spacing between each arc.
*/
getArcRadius(index) {
return this._radius - this.thickness - index * (this.thickness + this.spacing);
}
/**
* Map the dataset to the NestedDonutChartArc interface
*/
getChartData() {
const dataset = this.dataset.map((data, index) => {
let previousEndAngle = 0;
// check if there was a previous dataset at this index
if (this._arcData && this._arcData[index]) {
previousEndAngle = this._arcData[index].endAngle;
}
return { ...data, index, startAngle: 0, endAngle: this.getAngle(data), previousEndAngle };
});
// store the latest processed arc data
this._arcData = dataset;
return dataset;
}
/** Convert the data value to radians */
getAngle(data) {
const fraction = data.value / this.max;
const degrees = fraction * 360.0;
return degrees * (Math.PI / 180);
}
/**
* Get the color of the arc, this may be a CSS color value, the name of a color
* from the color set or a ThemeColor object. We return this as a rgba color to
* support the alpha channel
*/
getColor(color) {
return ThemeColor.isInstanceOf(color)
? color.toRgba()
: this._colorService.resolve(color);
}
/** If no track color is specified then default to a specific color based on the active colorset */
getTrackColor() {
if (this.trackColor) {
return this.getColor(this.trackColor);
}
// otherwise default to a color based on the colorset (note we can't use the Color enum from MF package)
if (this._colorService.colorExists(Color.Grey6)) {
return this.getColor(Color.Grey6);
}
if (this._colorService.colorExists('bright-gray')) {
return this.getColor('bright-gray');
}
}
/** Define the on hover event */
onArcMouseEnter(target, data) {
// update the hover effect if it is enabled
if (this.disableHover === false) {
select(target).transition().duration(250).attr('opacity', 0.5);
}
// update the tooltip context
this._tooltipContext = { ...data, color: this.getColor(data.color) };
// update the tooltip visibility
this._tooltipVisible = true;
// run change detection to ensure the visibility is updated
this._changeDetector.detectChanges();
}
/** Update the tooltip position on mouse move */
onArcMouseMove([x, y]) {
this._tooltipX = x;
this._tooltipY = y - 2; // subtract 2 so that it appears slightly above the cursor
// run change detection to update the element position
this._changeDetector.detectChanges();
}
/** Define the on hover out event */
onArcMouseLeave(target) {
// update the hover effect if it is enabled
if (this.disableHover === false) {
select(target).transition().duration(250).attr('opacity', 1);
}
// clear the tooltip context
this._tooltipContext = null;
// update the tooltip visibility
this._tooltipVisible = false;
// run change detection to ensure the visibility is updated
this._changeDetector.detectChanges();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NestedDonutChartComponent, isStandalone: false, selector: "ux-nested-donut-chart", inputs: { dataset: "dataset", max: "max", thickness: "thickness", spacing: "spacing", trackColor: "trackColor", disableHover: "disableHover", disableTooltip: "disableTooltip", tooltipPlacement: "tooltipPlacement", animationDuration: "animationDuration" }, outputs: { itemClick: "itemClick" }, queries: [{ propertyName: "_customTooltip", first: true, predicate: ["tooltip"], descendants: true }], viewQueries: [{ propertyName: "_chartElement", first: true, predicate: ["chart"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<svg\n #chart\n class=\"ux-nested-donut-chart\"\n [attr.focusable]=\"false\"\n [attr.width]=\"_size\"\n [attr.height]=\"_size\"\n [attr.aria-label]=\"_getAriaLabel()\"\n></svg>\n\n<!-- Custom content in center of the chart -->\n<div\n class=\"ux-nested-donut-chart-content\"\n [style.top.px]=\"_getContentInset()\"\n [style.right.px]=\"_getContentInset()\"\n [style.bottom.px]=\"_getContentInset()\"\n [style.left.px]=\"_getContentInset()\"\n [style.width.px]=\"_getContentSize()\"\n [style.height.px]=\"_getContentSize()\"\n>\n <ng-content></ng-content>\n</div>\n\n<!-- Tooltip to appear on arc hover -->\n@if (_tooltipVisible && !disableTooltip) {\n <div class=\"ux-nested-donut-chart-tooltip\">\n <ux-tooltip\n [placement]=\"tooltipPlacement\"\n [content]=\"_customTooltip || tooltip\"\n [context]=\"_tooltipContext\"\n [style.top.px]=\"_tooltipY\"\n [style.left.px]=\"_tooltipX\"\n >\n </ux-tooltip>\n </div>\n}\n\n<!-- Default tooltip template -->\n<ng-template #tooltip let-name=\"name\" let-value=\"value\">\n <span class=\"ux-nested-donut-chart-tooltip-content\"> {{ name }}: {{ value }} </span>\n</ng-template>\n", dependencies: [{ kind: "component", type: TooltipComponent, selector: "ux-tooltip", inputs: ["content", "context", "placement", "alignment"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-nested-donut-chart', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<svg\n #chart\n class=\"ux-nested-donut-chart\"\n [attr.focusable]=\"false\"\n [attr.width]=\"_size\"\n [attr.height]=\"_size\"\n [attr.aria-label]=\"_getAriaLabel()\"\n></svg>\n\n<!-- Custom content in center of the chart -->\n<div\n class=\"ux-nested-donut-chart-content\"\n [style.top.px]=\"_getContentInset()\"\n [style.right.px]=\"_getContentInset()\"\n [style.bottom.px]=\"_getContentInset()\"\n [style.left.px]=\"_getContentInset()\"\n [style.width.px]=\"_getContentSize()\"\n [style.height.px]=\"_getContentSize()\"\n>\n <ng-content></ng-content>\n</div>\n\n<!-- Tooltip to appear on arc hover -->\n@if (_tooltipVisible && !disableTooltip) {\n <div class=\"ux-nested-donut-chart-tooltip\">\n <ux-tooltip\n [placement]=\"tooltipPlacement\"\n [content]=\"_customTooltip || tooltip\"\n [context]=\"_tooltipContext\"\n [style.top.px]=\"_tooltipY\"\n [style.left.px]=\"_tooltipX\"\n >\n </ux-tooltip>\n </div>\n}\n\n<!-- Default tooltip template -->\n<ng-template #tooltip let-name=\"name\" let-value=\"value\">\n <span class=\"ux-nested-donut-chart-tooltip-content\"> {{ name }}: {{ value }} </span>\n</ng-template>\n" }]
}], propDecorators: { dataset: [{
type: Input
}], max: [{
type: Input
}], thickness: [{
type: Input
}], spacing: [{
type: Input
}], trackColor: [{
type: Input
}], disableHover: [{
type: Input
}], disableTooltip: [{
type: Input
}], tooltipPlacement: [{
type: Input
}], animationDuration: [{
type: Input
}], itemClick: [{
type: Output
}], _chartElement: [{
type: ViewChild,
args: ['chart', { static: true }]
}], _customTooltip: [{
type: ContentChild,
args: ['tooltip', { static: false }]
}] } });
class NestedDonutChartModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartModule, declarations: [NestedDonutChartComponent], imports: [CommonModule, ColorServiceModule, TooltipModule, ResizeModule], exports: [NestedDonutChartComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartModule, imports: [CommonModule, ColorServiceModule, TooltipModule, ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NestedDonutChartModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, ColorServiceModule, TooltipModule, ResizeModule],
declarations: [NestedDonutChartComponent],
exports: [NestedDonutChartComponent],
}]
}] });
class NotificationService {
constructor() {
this._colorService = inject(ColorService);
/**
* Sets the order in which notifications are displayed:
`above` - newer notifications will appear above older ones.
`below` - newer notifications will appear below older ones.
*/
this.direction = 'above';
/**
* The list of notifications including notifications that have been dismissed
*/
this.notifications$ = new BehaviorSubject([]);
/**
* Define the default set of notification options
*/
this.options = {
duration: 4,
backgroundColor: this._colorService.getColor('accent').toHex(),
iconColor: this._colorService.getColor('accent').toHex(),
};
}
/**
* Access the list of notifications as an array
*/
get notifications() {
return this.notifications$.value;
}
/**
* This function should be called to show a notification.
* It should be given a TemplateRef containing the content to be displayed.
* @param templateRef - A TemplateRef containing the content to be displayed
* @param options - The properties to configure the notification.
* @param context - The context passed to the notification TemplateRef. This can be accessed by adding a let-data="data" to the ng-template element.
*/
show(templateRef, options = this.options, context = {}) {
// populate the specified options with the default values for any missing properties
options = { ...this.options, ...options };
// create the notificationRef based on the options and context specified
const notificationRef = {
templateRef,
duration: options.duration,
date: new Date(),
visible: true,
height: options.height,
spacing: options.spacing,
backgroundColor: options.backgroundColor,
iconColor: options.iconColor,
data: context,
};
// add the new notification to the list (either above or below based on direction)
this.direction === 'above'
? this.notifications.unshift(notificationRef)
: this.notifications.push(notificationRef);
// update the notifications list
this.notifications$.next(this.notifications);
// remove notification after delay
if (options.duration !== 0) {
setTimeout(() => this.dismiss(notificationRef), options.duration * 1000);
}
return notificationRef;
}
/**
* This function will return a list of all the notifications that have been shown.
*/
getHistory() {
return this.notifications;
}
/**
* This function can be called to dismiss a notification. It should be passed the object to dismiss.
* @param notificationRef - The notification that should be dismissed
*/
dismiss(notificationRef) {
notificationRef.visible = false;
this.notifications$.next(this.notifications);
}
/**
* This function will dismiss any currently visible notifications.
*/
dismissAll() {
this.notifications.forEach(notificationRef => (notificationRef.visible = false));
this.notifications$.next(this.notifications);
}
/** Remove the notification from the screen and from the notification history */
remove(notificationRef) {
this.notifications$.next(this.notifications.filter(_notificationRef => _notificationRef !== notificationRef));
}
/** Remove all notifications from the screen and from the notification history */
removeAll() {
this.notifications$.next([]);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class NotificationListComponent {
constructor() {
this._notificationService = inject(NotificationService);
this._changeDetectorRef = inject(ChangeDetectorRef);
this._renderer = inject(Renderer2);
/** Sets the position of the list of notifications within the browser window. */
this.position = 'bottom-right';
/** The list of notifications that have not been dismissed */
this.notifications$ = this._notificationService.notifications$.pipe(map(() => this._notifications));
/** Unsubscribe from all subscriptions on component destroy */
this._onDestroy = new Subject();
}
/**
* Sets the order in which notifications are displayed:
`above` - newer notifications will appear above older ones.
`below` - newer notifications will appear below older ones.
*/
set direction(direction) {
this._notificationService.direction = direction;
}
/** Filter out any hidden notifications */
get _notifications() {
return this._notificationService.notifications.filter(notification => notification.visible);
}
ngAfterViewInit() {
// whenever the notifications change we want to recalculate the positions and height
this._elements.changes
.pipe(takeUntil(this._onDestroy), tick(), map(changes => changes.toArray()), withLatestFrom(this.notifications$))
.subscribe(([elements, notifications]) => {
// Set the `top` style property of each element
this.applyElementPositions(elements, notifications);
this.updateListPosition(elements, notifications);
this._changeDetectorRef.markForCheck();
});
}
ngOnChanges() {
if (this._elements) {
this.updateListPosition(this._elements.toArray(), this._notifications);
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
applyElementPositions(elements, notifications) {
let top = 0;
for (let i = 0; i < elements.length; i += 1) {
const element = elements[i].nativeElement;
const notification = notifications[i];
this._renderer.setStyle(element, 'top', `${top}px`);
top = top + this.getNotificationHeightInPixels(notification, elements[i]);
}
}
updateListPosition(elements, notifications) {
if (this.position === 'bottom-left' || this.position === 'bottom-right') {
this._bottom = notifications.reduce((totalHeight, notification, index) => totalHeight + this.getNotificationHeightInPixels(notification, elements[index]), 0);
}
else {
// In a top position, bottom should be unset.
this._bottom = undefined;
}
}
/** Get the height of the notification, including spacing if configured. */
getNotificationHeightInPixels(notification, elementRef) {
if (notification.spacing === undefined) {
return this.getElementOuterHeightInPixels(elementRef);
}
return elementRef.nativeElement.offsetHeight + notification.spacing;
}
/** Get the total height of the element including margins. */
getElementOuterHeightInPixels(elementRef) {
const element = elementRef.nativeElement;
const { marginTop, marginBottom } = getComputedStyle(element);
return element.offsetHeight + parseInt(marginTop) + parseInt(marginBottom);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NotificationListComponent, isStandalone: false, selector: "ux-notification-list", inputs: { direction: "direction", position: "position" }, host: { properties: { "class": "this.position", "style.bottom.px": "this._bottom" } }, viewQueries: [{ propertyName: "_elements", predicate: ["notification"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@for (notificationRef of notifications$ | async; track notificationRef; let index = $index) {\n <div\n #notification\n class=\"notification\"\n [style.height.px]=\"notificationRef.height\"\n [style.background-color]=\"notificationRef.backgroundColor\"\n [@notificationState]\n >\n <!-- Notification Content -->\n <ng-container\n [ngTemplateOutlet]=\"notificationRef.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: notificationRef, data: notificationRef.data }\"\n >\n </ng-container>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], animations: [
trigger('notificationState', [
state('in', style({ transform: 'translateY(0)', opacity: 0.9 })),
transition(':enter', [style({ transform: 'translateY(-50px)', opacity: 0 }), animate(500)]),
transition(':leave', [animate(500, style({ transform: 'translateY(50px)', opacity: 0 }))]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationListComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-notification-list', changeDetection: ChangeDetectionStrategy.OnPush, animations: [
trigger('notificationState', [
state('in', style({ transform: 'translateY(0)', opacity: 0.9 })),
transition(':enter', [style({ transform: 'translateY(-50px)', opacity: 0 }), animate(500)]),
transition(':leave', [animate(500, style({ transform: 'translateY(50px)', opacity: 0 }))]),
]),
], standalone: false, template: "@for (notificationRef of notifications$ | async; track notificationRef; let index = $index) {\n <div\n #notification\n class=\"notification\"\n [style.height.px]=\"notificationRef.height\"\n [style.background-color]=\"notificationRef.backgroundColor\"\n [@notificationState]\n >\n <!-- Notification Content -->\n <ng-container\n [ngTemplateOutlet]=\"notificationRef.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: notificationRef, data: notificationRef.data }\"\n >\n </ng-container>\n </div>\n}\n" }]
}], propDecorators: { direction: [{
type: Input
}], position: [{
type: Input
}, {
type: HostBinding,
args: ['class']
}], _bottom: [{
type: HostBinding,
args: ['style.bottom.px']
}], _elements: [{
type: ViewChildren,
args: ['notification']
}] } });
class NotificationModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: NotificationModule, declarations: [NotificationListComponent], imports: [CommonModule, ColorServiceModule], exports: [NotificationListComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationModule, imports: [CommonModule, ColorServiceModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NotificationModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, ColorServiceModule],
exports: [NotificationListComponent],
declarations: [NotificationListComponent],
}]
}] });
/* eslint-disable @typescript-eslint/no-empty-object-type */
class OrganizationChartComponent {
constructor() {
this._resizeService = inject(ResizeService);
this._componentFactoryResolver = inject(ComponentFactoryResolver);
this._injector = inject(Injector);
this._elementRef = inject(ElementRef);
this._appRef = inject(ApplicationRef);
this._viewContainerRef = inject(ViewContainerRef);
this._renderer = inject(Renderer2);
this._focusIndicator = inject(FocusIndicatorService);
this._ngZone = inject(NgZone);
/** Define the presentation of the connectors */
this.connector = 'elbow';
/** Define the duration of the transition animations */
this.duration = 750;
/** Define whether or not we can reveal additional parents */
this.showReveal = false;
/** Define the aria label for the reveal button */
this.revealAriaLabel = 'Reveal More';
/** Emit whenever a node is selected */
this.selectedChange = new EventEmitter(true);
/** Emit whenever the reveal button is pressed */
this.reveal = new EventEmitter();
/** Emit when the transition ends */
this.transitionEnd = new EventEmitter();
this._toggleNodesOnClick = true;
/** Store a flattened array of nodes */
this._nodeLayout = [];
/** Store a flattened array of links */
this._linkLayout = [];
/** Store the portal/outlets associated with some data */
this._portals = new Map();
/** Store the focus indicators associated with nodes */
this._indicators = new Map();
/** Store whether or not a transition is in progress */
this._isTransitioning = false;
/** Store whether or not a camera pan is in progress */
this._isPanning = false;
/** Determine if the component is initialised */
this._isInitialised = false;
/** Determine if the connector type has changed since the last render */
this._hasConnectorChanged = false;
/** Automatically unsubscribe from all subscriptions on destroy */
this._onDestroy = new Subject();
}
/** Defines whether nodes can be toggled or not */
set toggleNodesOnClick(toggleNodesOnClick) {
this._toggleNodesOnClick = coerceBooleanProperty(toggleNodesOnClick);
}
get toggleNodesOnClick() {
return this._toggleNodesOnClick;
}
/** Programmatically select an item */
set selected(selected) {
if (this.selected === selected || !selected) {
return;
}
if (this._isInitialised) {
this.select(selected);
this.centerNode(selected);
}
else {
this._pendingSelection = selected;
}
}
ngAfterViewInit() {
// before we do anything ensure they have provided a template
if (!this.nodeTemplate) {
throw new Error('Organization Chart - You must provide a node template!');
}
if (!this.nodeWidth || !this.nodeHeight) {
throw new Error('Organization Chart - You must specify a nodeWidth and nodeHeight');
}
// create the zoom drag listener
this._zoom = zoom()
.scaleExtent([1, 1])
.interpolate(interpolate)
.on('zoom', this.applyCameraPosition.bind(this))
.on('end', () => {
if (!this._isPanning) {
this.ensureNodesAreVisible();
}
});
// set up the selections
this._linksContainer = select(this.linksContainer.nativeElement);
this._nodesContainer = select(this.nodesContainer.nativeElement);
// setup the zoom on the node layer
this._ngZone.runOutsideAngular(() => this._nodesContainer.call(this._zoom));
// perform the initial render
this.render();
// ensure we set the initial chart size
this._width = this._elementRef.nativeElement.offsetWidth;
this._height = this._elementRef.nativeElement.offsetHeight;
// watch for any resizing of the chart
const resize$ = this._resizeService.addResizeListener(this._elementRef.nativeElement);
// on size change immediate update the width and height measurements
resize$.pipe(takeUntil(this._onDestroy)).subscribe(this.onResize.bind(this));
// after a debounce ensure nodes are visible
resize$
.pipe(takeUntil(this._onDestroy), debounceTime(this.duration))
.subscribe(this.ensureNodesAreVisible.bind(this));
// initially horizontally center the root node
this.centerNode(this.dataset, OrganizationChartAxis.Horizontal, false);
// initally move the camera down slightly so the root node does not appear at the very top of the chart
this.moveCamera(0, 150, false);
// mark this component as initialised
this._isInitialised = true;
}
ngOnChanges(changes) {
if (changes.connector && !changes.connector.firstChange) {
this._hasConnectorChanged = true;
}
// if only the selected property has changed then don't re-render as this is handled by the setter
if (Object.keys(changes).length === 1 && changes.selected) {
return;
}
if (this._isInitialised) {
this.render();
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
// correctly dispose all portals and outlets
this._portals.forEach(node => {
node.portal.detach();
node.outlet.dispose();
});
}
/** Perform the actual rendering of the chart */
render() {
// perform the layout algorithm on the current dataset
this.updateLayout();
// select all the existing links and nodes
this.updateSelections();
// create a d3 transition based in the specified transition time
const defaultTransition = transition$1('organizationChartDefaultTransition')
.duration(this.duration)
.on('start', () => (this._isTransitioning = true))
.on('end', () => {
this._isTransitioning = false;
this.transitionEnd.emit();
});
// render the links when they are first added to the DOM
this._links
.enter()
.insert('path')
.attr('class', 'ux-organization-chart-link')
.attr('d', link => this.getLinkPath(link))
.attr('opacity', -2)
.transition(defaultTransition)
.attr('d', link => this.getLinkPath(link))
.attr('opacity', 1);
// define the standard transition while the link is 'alive'
this._links
.transition()
.duration(this._hasConnectorChanged ? 0 : this.duration)
.attr('d', link => this.getLinkPath(link));
// apply transitions when removing nodes
this._links
.exit()
.transition(defaultTransition)
.attr('d', (link) => this.getCollapsedLinkPath(link))
.attr('opacity', 0)
.remove();
// when a node is first added to the DOM position it
this._nodes
.enter()
.append('div')
.attr('class', 'ux-organization-chart-node')
.style('width', this.nodeWidth + 'px')
.style('height', this.nodeHeight + 'px')
.style('left', node => (node.parent ? node.parent.x : node.x) + 'px')
.style('top', node => (node.parent ? node.parent.y : node.y) + 'px')
.style('opacity', 0)
.on('keydown', (event, node) => this.onKeydown(event, node))
.on('focus', (event, node) => this.onFocus(node))
.on('click', (event, node) => this.onClick(node))
.each(this.renderNodeTemplate.bind(this))
.each((node, index, group) => this.monitorFocus(group[index], node))
.transition(defaultTransition)
.style('left', node => node.x + 'px')
.style('top', node => node.y + 'px')
.style('opacity', 1);
// apply any movements while nodes are 'alive'
this._nodes
.transition(defaultTransition)
.style('left', node => node.x + 'px')
.style('top', node => node.y + 'px');
// apply transitions when removing nodes
this._nodes
.exit()
.transition(defaultTransition)
.style('left', (node) => (node.parent ? node.parent.x : node.x) + 'px')
.style('opacity', 0)
.remove()
.on('end', (node) => this.destroyNode(node));
// update the position of the reveal button
select(this.revealElement.nativeElement)
.style('left', this.nodeWidth / 2 - this.revealElement.nativeElement.offsetWidth / 2 + 'px')
.style('top', -(this.nodeHeight / 2 + this.revealElement.nativeElement.offsetHeight / 2) + 'px');
// after any new links and nodes have been created or removed we should update the selections
this.updateSelections();
// update the selected classes - ensure there is always a selected node
if (!this._selected) {
this.select(this._pendingSelection || this.dataset);
this._pendingSelection = null;
}
// set the tab indexes and aria labels for any newly added items
this.setNodeAttributes();
// apply the current camera position to any new nodes/links
this.applyCameraPosition();
// reset the connector changed status
this._hasConnectorChanged = false;
}
/** Select a specified node */
select(node) {
// get the node in the desired format
node = this.coerceDataNode(node);
// check if the node is already selected
if (this._selected === node) {
return;
}
// ensure all parents are expanded
this.expandParents(node);
// deselect any current node
this.deselect(false);
// if the selected item has changed then store the latest selection
this._selected = node;
// emit the latest selection
this.selectedChange.emit(this._selected);
// show reveal any nodes that may previously have been hidden but are now visible due to selection
if (this._isInitialised) {
this.render();
}
// add the styling to the selected node
this._renderer.addClass(this.getNodeElement(this._selected), 'ux-organization-chart-node-selected');
// update the styling and tabindexes
this.setNodeAttributes();
}
/** Deselect the currently selected node */
deselect(emit = true) {
if (this._nodes) {
this._nodes
.nodes()
.forEach(element => this._renderer.removeClass(element, 'ux-organization-chart-node-selected'));
}
if (emit && !!this._selected) {
this._selected = null;
this.selectedChange.next(null);
// update the tab indexes and aria labels
this.setNodeAttributes();
}
}
/** Toggle the collapsed state of a node */
toggle(node) {
if (this._isTransitioning) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure the clicked node is selected
this.select(node);
// apply the appropriate action
this.isExpanded(node) ? this.collapse(node) : this.expand(node);
}
/** Expand a node */
expand(node) {
if (this._isTransitioning || !this.toggleNodesOnClick) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure this node and all parent nodes are expanded
node.ancestors().forEach(_node => (_node.data.expanded = true));
// re-render the nodes
this.render();
// if the node has children then we want to move the camera to a child node
if (Array.isArray(node.data.children) && node.data.children.length > 0) {
// center on the middle child
this.centerNode(node.data.children[Math.floor(node.data.children.length / 2)]);
}
else {
this.centerNode(node);
}
}
/** Collapse a node */
collapse(node) {
// do nothing if a transition is currently in progress
if (this._isTransitioning) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure this node and all child nodes are collapse
node.descendants().forEach(_node => (_node.data.expanded = false));
// re-render the nodes
this.render();
// center the node that has just been collapsed
this.centerNode(node);
}
/** Move a specific node to the center of the screen */
centerNode(node, axis = OrganizationChartAxis.Both, animate = true) {
// get the node in the desired format
node = this.coercePointNode(node);
// get the current camera position
const camera = this.getCameraPosition();
const x = axis === OrganizationChartAxis.Vertical
? camera.x
: this._width / 2 - (node.x + this.nodeWidth / 2);
const y = axis === OrganizationChartAxis.Horizontal
? camera.y
: this._height / 2 - (node.y + this.nodeHeight / 2);
// update the camera position
this.setCameraPosition(x, y, animate);
}
/** Explicity set the position of the camera */
setCameraPosition(x, y, animate = true) {
// get the current transform
let camera = zoomTransform(this._nodesContainer.node());
// do nothing if the co-orindates have not changed
if (camera.x === x && camera.y === y) {
return;
}
// update the camera position
camera = camera.translate(x - camera.x, y - camera.y);
// indicate that the camera is panning programmatically
this._isPanning = true;
if (animate) {
this._nodesContainer
.transition()
.duration(this.duration)
.call(this._zoom.transform, camera)
.on('end interrupt cancel', () => (this._isPanning = false));
}
else {
this._nodesContainer.call(this._zoom.transform, camera);
this._isPanning = false;
}
}
/** Move the camera an amount from its current position */
moveCamera(x, y, animate = true) {
// get the current camera position
const camera = this.getCameraPosition();
this.setCameraPosition(camera.x + x, camera.y + y, animate);
}
/** Focus a given node */
focus(node) {
this.focusNode(this.coercePointNode(node));
}
/** Focus the root node */
_focusRootNode() {
this.focusNode(this.coercePointNode(this.dataset));
}
/** Destroy the outlet and portal associated with a node */
destroyNode(node) {
// get the node in a consistent format
node = this.coercePointNode(node);
// remove focus monitoring
if (this._indicators.has(node.data)) {
// remove the focus monitoring
this._indicators.get(node.data).destroy();
// remove the indicator from the list of indicators
this._indicators.delete(node.data);
}
// if there is not portal/outlets associated with this node then do nothing
if (!this._portals.has(node.data)) {
return;
}
// get the portal and outlet from the map
const portalRef = this._portals.get(node.data);
// perform the cleanup
portalRef.portal.detach();
portalRef.outlet.dispose();
// remove this entry from the map
this._portals.delete(node.data);
}
// update the data structure for the node and link layouts
updateLayout() {
this._layout = this.getLayout();
this._nodeLayout = this._layout.descendants();
this._linkLayout = this._layout.links();
}
/** Ensure the selections stay in sync with the view */
updateSelections() {
// select all the newly added dom nodes and associate the dataset
this._nodes = this._nodesContainer
.selectAll('.ux-organization-chart-node')
.data(this._nodeLayout, (node) => node.data.id.toString());
// select all the newly added path nodes
this._links = this._linksContainer
.selectAll('.ux-organization-chart-link')
.data(this._linkLayout, (link) => {
return `${link.source.data.id}-${link.target.data.id}`;
});
}
/** Render the content of the node based on the template provided */
renderNodeTemplate(node, index, group) {
// create the context for the node
const context = {
data: node.data.data,
node: node.data,
focused: false,
};
// the focused state should be a getter
Object.defineProperty(context, 'focused', {
get: () => this._focused === node.data,
});
// create the outlet to insert the Template and the portal from the TemplateRef
const outlet = this.createPortalOutlet(group[index]);
const portal = new TemplatePortal(this.nodeTemplate, this._viewContainerRef, context);
// insert the TemplateRef into the specified region
portal.attach(outlet);
// store the portal and outlet so we can correctly dispose of the nodes
this._portals.set(node.data, { portal, outlet });
}
/** Handle any zoom events (we use zoom for panning behaviour) */
applyCameraPosition() {
// get the new x and y position
let { x, y } = zoomTransform(this._nodesContainer.node());
// round the precision to integers to prevent any anti-aliasing
x = Math.round(x);
y = Math.round(y);
// transform the position of the reveal button
this._renderer.setStyle(this.revealElement.nativeElement, 'transform', `translate(${x}px, ${y}px)`);
// transform the position of the nodes
this._nodesContainer
.selectAll('.ux-organization-chart-node')
.style('transform', `translate(${x}px, ${y}px)`);
// transform the position of the links
this._linksContainer
.selectAll('.ux-organization-chart-link')
.attr('transform', `translate(${x} ${y})`);
}
/** Get the data in with the required layout information */
getLayout() {
// create a hierarchical representation of the data - don't include collapsed nodes
const treeHierarchy = hierarchy(this.dataset, node => Array.isArray(node.children) && node.expanded ? node.children : []);
// create our layout
const layout = tree()
.nodeSize([this.nodeWidth, this.nodeHeight])
.separation(this.getNodeSpacing.bind(this));
// process the data with the layout
const treeLayout = layout(treeHierarchy);
// calculate the vertical spacing
const verticalSpacing = this.verticalSpacing === undefined ? this.nodeHeight : this.verticalSpacing;
// set the vertical spacing
treeLayout.each(data => (data.y = data.depth * (this.nodeHeight + verticalSpacing)));
return treeLayout;
}
/** Determine how much horizontal spacing should be between nodes */
getNodeSpacing(nodeOne, nodeTwo) {
// if the nodes are not siblings then space further apart
if (nodeOne.parent !== nodeTwo.parent) {
return 2;
}
// if they are siblings they should be closer together
return 1.5;
}
/** Ensure we consistently use the HierarchyPoint data structure */
coercePointNode(node) {
// determine if this is a raw data node or a hierarchy point
// eslint-disable-next-line no-prototype-builtins
if (node.hasOwnProperty('depth') && node.hasOwnProperty('x') && node.hasOwnProperty('y')) {
return node;
}
// otherwise find the matching node
const match = this._nodeLayout.find(_node => _node.data === node);
// if the data does not exist in the hierarchy throw an exception
if (!match) {
throw new Error('The node does not exist in the hierarchy');
}
return match;
}
coerceDataNode(node) {
// eslint-disable-next-line no-prototype-builtins
if (node.hasOwnProperty('depth') && node.hasOwnProperty('x') && node.hasOwnProperty('y')) {
return node.data;
}
return node;
}
/** Handle chart resize events */
onResize({ width, height }) {
this._width = width;
this._height = height;
}
/** Deteremine if a node is expanded or collapsed */
isExpanded(node) {
return !!node.data.expanded;
}
/** Get the current position of the camera */
getCameraPosition() {
return zoomTransform(this._nodesContainer.node());
}
/** Get the SVG line definition for each link */
getLinkPath(pointLink) {
if (this.connector === 'elbow') {
const source = {
x: pointLink.source.x + this.nodeWidth / 2,
y: pointLink.source.y + this.nodeHeight,
};
const target = { x: pointLink.target.x + this.nodeWidth / 2, y: pointLink.target.y };
return ('M' +
source.x +
',' +
source.y +
'v' +
(target.y - source.y) / 2 +
'h' +
(target.x - source.x) +
'v' +
(target.y - source.y) / 2);
}
else {
const source = {
x: pointLink.source.x + this.nodeWidth / 2,
y: pointLink.source.y + this.nodeHeight / 2,
};
const target = {
x: pointLink.target.x + this.nodeWidth / 2,
y: pointLink.target.y + this.nodeHeight / 2,
};
return linkVertical()({ source: [source.x, source.y], target: [target.x, target.y] });
}
}
/** Get the link path line defintion when the link is collapsing */
getCollapsedLinkPath(pointLink) {
return this.getLinkPath({ source: pointLink.source, target: pointLink.source });
}
/** Create a dynamic region that Angular can insert into */
createPortalOutlet(element) {
return new DomPortalOutlet(element, this._componentFactoryResolver, this._appRef, this._injector);
}
/** Make the appropriate node tabbable and update aria attributes */
setNodeAttributes() {
for (const element of this._nodes.nodes()) {
// intially the tab index of all items to -1
this._renderer.setAttribute(element, 'tabindex', '-1');
// set the expanded aria attribute
this._renderer.setAttribute(element, 'aria-expanded', this.getNodeData(element).data.expanded ? 'true' : 'false');
}
// if there is a selected item then it should be tabbable otherwise make the root tabbable
if (this._selected) {
this._renderer.setAttribute(this.getNodeElement(this._selected), 'tabindex', '0');
}
}
/** Get the element that represents a given node */
getNodeElement(node) {
node = this.coercePointNode(node);
// find the element that matches the node data
const index = this._nodes.data().indexOf(node);
return this._nodes.nodes()[index];
}
/** Get the element that represents a given node */
getNodeData(node) {
// find the element that matches the node element
const index = this._nodes.nodes().indexOf(node);
return this._nodes.data()[index];
}
/** Handle click events */
onClick(node) {
if (!this.toggleNodesOnClick) {
return;
}
this.toggle(node);
}
/** Handle keyboard events */
onKeydown(event, node) {
if (!this.toggleNodesOnClick) {
return;
}
switch (event.keyCode) {
case DOWN_ARROW:
event.preventDefault();
// if the node is collapsed and has children expand
if (!node.data.expanded &&
Array.isArray(node.data.children) &&
node.data.children.length > 0) {
return this.expand(node);
}
return this.focusChild(node);
case RIGHT_ARROW:
event.preventDefault();
return this.focusNextSibling(node);
case UP_ARROW:
event.preventDefault();
return this.focusParent(node);
case LEFT_ARROW:
event.preventDefault();
return this.focusPreviousSibling(node);
case ENTER:
return this.toggle(node);
}
}
/** When a node receives focus */
onFocus(node) {
if (!this.isNodeInViewport(node, this._width * 0.1, this._height * 0.1)) {
this.centerNode(node);
}
}
/** Move focus to the parent node */
focusParent(node) {
if (node.parent) {
this.focusNode(node.parent);
}
else if (this.revealElement) {
this.revealElement.nativeElement.focus();
// center the root node to ensure the reveal button is in view
this.centerNode(this.dataset);
}
}
/** Move focus to the child node */
focusChild(node) {
if (Array.isArray(node.children) && node.children.length > 0) {
this.focusNode(node.children[Math.floor(node.children.length / 2)]);
}
}
/** Move focus to the sibling on the left */
focusPreviousSibling(node) {
if (node.parent) {
this.focusNode(node.parent.children[node.parent.children.indexOf(node) - 1]);
}
}
/** Move focus to the sibling on the right */
focusNextSibling(node) {
if (node.parent) {
this.focusNode(node.parent.children[node.parent.children.indexOf(node) + 1]);
}
}
/** Focus a given node */
focusNode(node) {
if (node) {
this.getNodeElement(node).focus({ preventScroll: true });
// ensure we don't perform scrolling if the node is not in view (we rely on preventScroll as IE doesn't support it)
this.nodesContainer.nativeElement.scrollTop = 0;
this.nodesContainer.nativeElement.scrollLeft = 0;
}
}
/** Determine if a node is fully visible within the viewport */
isNodeInViewport(node, insetX = 0, insetY = 0) {
const { x, y } = this.getCameraPosition();
const left = node.x + x;
const top = node.y + y;
const right = node.x + x + this.nodeWidth;
const bottom = node.y + y + this.nodeHeight;
return (left >= insetX &&
top >= insetY &&
right <= this._width - insetX &&
bottom <= this._height - insetY);
}
/** Determine if a node is fully outside of the viewport */
isNodeOutsideViewport(node, insetX = 0, insetY = 0) {
const { x, y } = this.getCameraPosition();
const left = node.x + x + this.nodeWidth;
const top = node.y + y + this.nodeHeight;
const right = node.x + x;
const bottom = node.y + y;
return (left < insetX ||
top < insetY ||
right > this._width - insetX ||
bottom > this._height - insetY);
}
/** Determine how far a node is from being within the viewport */
getDistanceFromViewport(node, insetX = 0, insetY = 0) {
// if the node is in the viewport then it will always be 0, 0
if (!this.isNodeOutsideViewport(node, insetX, insetY)) {
return [0, 0];
}
const { x, y } = this.getCameraPosition();
const left = insetX - (node.x + x + this.nodeWidth);
const top = insetY - (node.y + y + this.nodeHeight);
const right = node.x + x - (this._width - insetX);
const bottom = node.y + y - (this._height - insetY);
let horizontal = 0;
let vertical = 0;
if (left > 0 && left > right) {
horizontal = left;
}
if (right > 0 && left < right) {
horizontal = -right;
}
if (top > 0 && top > bottom) {
vertical = top;
}
if (bottom > 0 && top < bottom) {
vertical = -bottom;
}
// calculate the distances on both axis
return [horizontal, vertical];
}
/** Begin monitoring the element focus so we only show styling when navigated by keyboard */
monitorFocus(element, node) {
// create the focus indicator
const indicator = this._focusIndicator.monitor(element, {
checkChildren: false,
programmaticFocusIndicator: true,
});
// store the currently selected node as an instance variable
indicator.isFocused$.pipe(takeUntil(this._onDestroy)).subscribe(isFocused => {
// by default the CDK runs this outside of NgZone however we need it to run inside NgZone to update the node template
this._ngZone.run(() => {
if (isFocused) {
this._focused = node.data;
}
else if (node.data === this._focused) {
this._focused = null;
}
});
});
// store the focus indicator reference
this._indicators.set(node.data, indicator);
}
// ensure that there are at least some nodes visible
ensureNodesAreVisible() {
// determine how many nodes are currently visible
const visibleCount = this._nodes.filter(node => !this.isNodeOutsideViewport(node)).size();
if (visibleCount > 0) {
return;
}
// get the distance each node is from being within the viewport
const distances = this._nodes
.data()
.map(node => this.getDistanceFromViewport(node, this.nodeWidth * 1.25, this.nodeHeight * 1.5));
// find the closest node
const [x, y] = distances.reduce((previous, current) => {
const [previousX, previousY] = previous;
const [currentX, currentY] = current;
return Math.abs(previousX) + Math.abs(previousY) < Math.abs(currentX) + Math.abs(currentY)
? previous
: current;
});
// move the camera by the required amount
this.moveCamera(x, y);
}
/** Expand all parent nodes */
expandParents(node) {
// get the parent node
let parent = this.getParent(node);
while (parent) {
parent.expanded = true;
parent = this.getParent(parent);
}
}
/** Get the parent of a given node */
getParent(node) {
return [this.coerceDataNode(this.dataset), ...this.getAllChildren(this.dataset)].find(_node => {
if (!Array.isArray(_node.children)) {
return false;
}
return _node.children.find((child) => child.id === node.id);
});
}
/** Get a flat array of all the nodes childrent */
getAllChildren(node) {
const children = node.children || [];
// check for any children on the children
return [
...children,
...children.reduce((accumulation, child) => [...accumulation, ...this.getAllChildren(child)], []),
].map(child => this.coerceDataNode(child));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: OrganizationChartComponent, isStandalone: false, selector: "ux-organization-chart", inputs: { dataset: "dataset", connector: "connector", nodeWidth: "nodeWidth", nodeHeight: "nodeHeight", duration: "duration", verticalSpacing: "verticalSpacing", showReveal: "showReveal", revealAriaLabel: "revealAriaLabel", toggleNodesOnClick: "toggleNodesOnClick", selected: "selected" }, outputs: { selectedChange: "selectedChange", reveal: "reveal", transitionEnd: "transitionEnd" }, queries: [{ propertyName: "revealTemplate", first: true, predicate: ["revealTemplate"], descendants: true }, { propertyName: "nodeTemplate", first: true, predicate: ["nodeTemplate"], descendants: true }], viewQueries: [{ propertyName: "revealElement", first: true, predicate: ["revealElement"], descendants: true, static: true }, { propertyName: "linksContainer", first: true, predicate: ["links"], descendants: true, static: true }, { propertyName: "nodesContainer", first: true, predicate: ["nodes"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Add a button above the root node to load additional parent items -->\n<button\n #revealElement\n uxFocusIndicatorOrigin\n class=\"ux-organization-chart-reveal\"\n tabindex=\"-1\"\n [attr.aria-label]=\"revealAriaLabel\"\n [hidden]=\"!showReveal\"\n (click)=\"reveal.emit(); _focusRootNode()\"\n (keydown.ArrowDown)=\"_focusRootNode(); $event.preventDefault()\"\n>\n <!-- Display Reveal Template -->\n <ng-container [ngTemplateOutlet]=\"revealTemplate || defaultRevealTemplate\"></ng-container>\n</button>\n\n<!-- Show the links connecting each node -->\n<svg #links class=\"ux-organization-chart-links\"></svg>\n\n<!-- Show the nodes containing information about each item -->\n<div #nodes class=\"ux-organization-chart-nodes\"></div>\n\n<!-- Provide a default reveal template -->\n<ng-template #defaultRevealTemplate>\n <ux-icon name=\"tab-up\"></ux-icon>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-organization-chart', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Add a button above the root node to load additional parent items -->\n<button\n #revealElement\n uxFocusIndicatorOrigin\n class=\"ux-organization-chart-reveal\"\n tabindex=\"-1\"\n [attr.aria-label]=\"revealAriaLabel\"\n [hidden]=\"!showReveal\"\n (click)=\"reveal.emit(); _focusRootNode()\"\n (keydown.ArrowDown)=\"_focusRootNode(); $event.preventDefault()\"\n>\n <!-- Display Reveal Template -->\n <ng-container [ngTemplateOutlet]=\"revealTemplate || defaultRevealTemplate\"></ng-container>\n</button>\n\n<!-- Show the links connecting each node -->\n<svg #links class=\"ux-organization-chart-links\"></svg>\n\n<!-- Show the nodes containing information about each item -->\n<div #nodes class=\"ux-organization-chart-nodes\"></div>\n\n<!-- Provide a default reveal template -->\n<ng-template #defaultRevealTemplate>\n <ux-icon name=\"tab-up\"></ux-icon>\n</ng-template>\n" }]
}], propDecorators: { dataset: [{
type: Input
}], connector: [{
type: Input
}], nodeWidth: [{
type: Input
}], nodeHeight: [{
type: Input
}], duration: [{
type: Input
}], verticalSpacing: [{
type: Input
}], showReveal: [{
type: Input
}], revealAriaLabel: [{
type: Input
}], toggleNodesOnClick: [{
type: Input
}], selected: [{
type: Input
}], selectedChange: [{
type: Output
}], reveal: [{
type: Output
}], transitionEnd: [{
type: Output
}], revealTemplate: [{
type: ContentChild,
args: ['revealTemplate', { static: false }]
}], nodeTemplate: [{
type: ContentChild,
args: ['nodeTemplate', { static: false }]
}], revealElement: [{
type: ViewChild,
args: ['revealElement', { static: true }]
}], linksContainer: [{
type: ViewChild,
args: ['links', { static: true }]
}], nodesContainer: [{
type: ViewChild,
args: ['nodes', { static: true }]
}] } });
var OrganizationChartAxis;
(function (OrganizationChartAxis) {
OrganizationChartAxis[OrganizationChartAxis["Horizontal"] = 0] = "Horizontal";
OrganizationChartAxis[OrganizationChartAxis["Vertical"] = 1] = "Vertical";
OrganizationChartAxis[OrganizationChartAxis["Both"] = 2] = "Both";
})(OrganizationChartAxis || (OrganizationChartAxis = {}));
class OrganizationChartModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartModule, declarations: [OrganizationChartComponent], imports: [AccessibilityModule, CommonModule, IconModule, ResizeModule], exports: [OrganizationChartComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartModule, imports: [AccessibilityModule, CommonModule, IconModule, ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OrganizationChartModule, decorators: [{
type: NgModule,
args: [{
declarations: [OrganizationChartComponent],
imports: [AccessibilityModule, CommonModule, IconModule, ResizeModule],
exports: [OrganizationChartComponent],
}]
}] });
class TabHeadingDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabHeadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TabHeadingDirective, isStandalone: false, selector: "[uxTabHeading]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabHeadingDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTabHeading]',
standalone: false,
}]
}] });
class TabsetService {
constructor() {
/** Store the list of tabs */
this.tabs = [];
this.activeTab$ = new BehaviorSubject(null);
/** Store the manual state */
this.manual = false;
}
/** Update the array of tabs - required to preserve order */
update(tabs) {
this.tabs = [...tabs];
}
/** Select a tab (from user input) */
select(tab) {
if (tab.disabled) {
return;
}
if (this.manual) {
// In manual mode, emit the activated/deactivated events.
// The application is responsible for updating the active state on each tab, which will then update the UI.
this.tabs.forEach(_tab => (_tab === tab ? _tab.activate() : _tab.deactivate()));
}
else {
this.activeTab$.next(tab);
}
}
/** Set tab active state */
setTabActive(tab) {
if (!tab.disabled) {
this.activeTab$.next(tab);
}
}
/** Determine if there is a selected tab */
isTabActive() {
return this.activeTab$.getValue() !== null;
}
/** Select the first non-disabled tab */
selectFirstTab() {
// find the index of the first non-disabled tab
const tab = this.tabs.find(_tab => !_tab.disabled);
if (tab) {
this.select(tab);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetService, decorators: [{
type: Injectable
}] });
/**
* This token is used to avoid circular dependency
*/
const TabsetToken = new InjectionToken('Tabset');
let uniqueTabId = 0;
class TabComponent {
constructor() {
this._tabsetService = inject(TabsetService);
this._changeDetector = inject(ChangeDetectorRef);
this._tabset = inject(TabsetToken);
/** Define if this tab is disabled */
this.disabled = false;
/** Emits when the active state changes. */
this.activeChange = new EventEmitter();
/** Emit when this tab is selected */
this.activated = new EventEmitter();
/** Emit when this tab is deselected */
this.deactivated = new EventEmitter();
// Active state of the tab, for use in the template
this._active = false;
// Id of tab, for use in the template
this._id = `ux-tab-${++uniqueTabId}`;
/** Unsubscribe from all subscriptions when component is destroyed */
this._onDestroy = new Subject();
}
/** Define the tab unique id */
set id(id) {
if (id) {
this._id = id;
}
}
get id() {
return this._id;
}
/** Define the active state of this tab */
set active(active) {
if (active) {
this._tabsetService.setTabActive(this);
}
}
ngOnInit() {
this._tabsetService.activeTab$
.pipe(tick(), distinctUntilChanged(), takeUntil(this._onDestroy))
.subscribe(activeTab => {
const isActive = activeTab === this;
if (this._active !== isActive) {
this.setActive(isActive);
}
});
}
ngOnChanges(changes) {
if (changes.disabled && changes.disabled.previousValue !== changes.disabled.currentValue) {
this._tabset.markForCheck();
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
activate() {
this.activated.emit();
}
deactivate() {
this.deactivated.emit();
}
/**
* Update the internal active state and emit appropriate events.
*/
setActive(active) {
this._active = active;
this.activeChange.emit(active);
if (!this._tabsetService.manual) {
if (active) {
this.activate();
}
else {
this.deactivate();
}
}
this._changeDetector.detectChanges();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TabComponent, isStandalone: false, selector: "ux-tab", inputs: { id: "id", active: "active", disabled: "disabled", heading: "heading", route: "route", routerLinkExtras: "routerLinkExtras", customClass: "customClass" }, outputs: { activeChange: "activeChange", activated: "activated", deactivated: "deactivated" }, queries: [{ propertyName: "headingRef", first: true, predicate: TabHeadingDirective, descendants: true, read: TemplateRef }], usesOnChanges: true, ngImport: i0, template: "<div\n role=\"tabpanel\"\n class=\"tab-pane\"\n [style.display]=\"_active ? 'block' : 'none'\"\n [id]=\"id + '-panel'\"\n [attr.aria-labelledby]=\"id\"\n [attr.aria-hidden]=\"!_active\"\n>\n <ng-content></ng-content>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-tab', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n role=\"tabpanel\"\n class=\"tab-pane\"\n [style.display]=\"_active ? 'block' : 'none'\"\n [id]=\"id + '-panel'\"\n [attr.aria-labelledby]=\"id\"\n [attr.aria-hidden]=\"!_active\"\n>\n <ng-content></ng-content>\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}], active: [{
type: Input
}], disabled: [{
type: Input
}], heading: [{
type: Input
}], route: [{
type: Input
}], routerLinkExtras: [{
type: Input
}], customClass: [{
type: Input
}], activeChange: [{
type: Output
}], activated: [{
type: Output
}], deactivated: [{
type: Output
}], headingRef: [{
type: ContentChild,
args: [TabHeadingDirective, { read: TemplateRef, static: false }]
}] } });
class TabsetComponent {
constructor() {
this._tabset = inject(TabsetService);
this._changeDetector = inject(ChangeDetectorRef);
/** Determine if the appearance of the tabset */
this.minimal = true;
/** Determine if the tabset should appear stacked */
this.stacked = 'none';
/** Remove subscriptions on destroy */
this._onDestroy$ = new Subject();
}
/** Determine if we want to manually update the active state */
set manual(manual) {
this._tabset.manual = manual;
}
ngAfterViewInit() {
// provide the service with the initial array of items
this._tabset.update(this._tabs.toArray());
// Make sure a tab is selected
if (!this._tabset.isTabActive()) {
this._tabset.selectFirstTab();
}
// run change detection once we have setup the tabs
this._changeDetector.detectChanges();
// watch for any future changes
this._tabs.changes.pipe(takeUntil(this._onDestroy$)).subscribe(tabs => {
// update the internal list of tabs
this._tabset.update(tabs);
// run change detection
this._changeDetector.detectChanges();
});
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
selectTab(tab) {
// pass tab to select method
this._tabset.select(tab instanceof TabComponent ? tab : this._tabs.toArray()[tab]);
// run change detection
this._changeDetector.detectChanges();
}
handleKeyDown(event, tab) {
if (event.keyCode === SPACE) {
event.preventDefault();
tab.click();
}
}
markForCheck() {
this._changeDetector.detectChanges();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TabsetComponent, isStandalone: false, selector: "ux-tabset", inputs: { minimal: "minimal", stacked: "stacked", manual: "manual", ariaLabel: ["aria-label", "ariaLabel"] }, host: { properties: { "class.tabs-left": "stacked === \"left\"", "class.tabs-right": "stacked === \"right\"" } }, providers: [
TabsetService,
{
provide: TabsetToken,
useExisting: forwardRef(() => TabsetComponent),
},
], queries: [{ propertyName: "_tabs", predicate: TabComponent }], ngImport: i0, template: "<!-- Nav tabs -->\n<ul\n role=\"tablist\"\n uxTabbableList\n [direction]=\"stacked === 'none' ? 'horizontal' : 'vertical'\"\n [allowBoundaryKeys]=\"true\"\n class=\"nav nav-tabs\"\n [class.minimal-tab]=\"minimal\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-orientation]=\"stacked === 'none' ? 'horizontal' : 'vertical'\"\n>\n @for (tab of _tabset.tabs; track tab; let index = $index) {\n <li\n role=\"presentation\"\n class=\"nav-item\"\n [class.active]=\"(_tabset.activeTab$ | async) === tab\"\n [class.disabled]=\"tab.disabled\"\n [ngClass]=\"tab.customClass\"\n >\n <ng-template #tabDetails>\n @if (!tab.headingRef) {\n <span>{{ tab.heading }}</span>\n }\n @if (tab.headingRef) {\n <ng-container [ngTemplateOutlet]=\"tab.headingRef\"></ng-container>\n }\n </ng-template>\n @if (tab.route) {\n <a\n class=\"nav-link\"\n [attr.id]=\"tab.id\"\n role=\"tab\"\n #anchorTab\n uxTabbableListItem\n uxFocusIndicator\n [attr.aria-controls]=\"tab.id\"\n [attr.aria-selected]=\"(_tabset.activeTab$ | async) === tab\"\n [attr.aria-disabled]=\"tab.disabled\"\n [routerLink]=\"tab.route\"\n [fragment]=\"tab.routerLinkExtras?.fragment\"\n [queryParams]=\"tab.routerLinkExtras?.queryParams\"\n [queryParamsHandling]=\"tab.routerLinkExtras?.queryParamsHandling\"\n [preserveFragment]=\"tab.routerLinkExtras?.preserveFragment\"\n [skipLocationChange]=\"tab.routerLinkExtras?.skipLocationChange\"\n [replaceUrl]=\"tab.routerLinkExtras?.replaceUrl\"\n [state]=\"tab.routerLinkExtras?.state\"\n (keydown)=\"handleKeyDown($event, anchorTab)\"\n >\n <ng-container [ngTemplateOutlet]=\"tabDetails\"> </ng-container>\n </a>\n }\n @if (!tab.route) {\n <a\n class=\"nav-link\"\n [attr.id]=\"tab.id\"\n role=\"tab\"\n uxTabbableListItem\n uxFocusIndicator\n (mousedown)=\"_tabset.select(tab)\"\n (activated)=\"_tabset.select(tab)\"\n [attr.aria-controls]=\"tab.id\"\n [attr.aria-selected]=\"(_tabset.activeTab$ | async) === tab\"\n [attr.aria-disabled]=\"tab.disabled\"\n >\n <ng-container [ngTemplateOutlet]=\"tabDetails\"> </ng-container>\n </a>\n }\n </li>\n }\n</ul>\n\n<!-- Tab panes -->\n<div class=\"tab-content\">\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-tabset', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
TabsetService,
{
provide: TabsetToken,
useExisting: forwardRef(() => TabsetComponent),
},
], host: {
'[class.tabs-left]': 'stacked === "left"',
'[class.tabs-right]': 'stacked === "right"',
}, standalone: false, template: "<!-- Nav tabs -->\n<ul\n role=\"tablist\"\n uxTabbableList\n [direction]=\"stacked === 'none' ? 'horizontal' : 'vertical'\"\n [allowBoundaryKeys]=\"true\"\n class=\"nav nav-tabs\"\n [class.minimal-tab]=\"minimal\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-orientation]=\"stacked === 'none' ? 'horizontal' : 'vertical'\"\n>\n @for (tab of _tabset.tabs; track tab; let index = $index) {\n <li\n role=\"presentation\"\n class=\"nav-item\"\n [class.active]=\"(_tabset.activeTab$ | async) === tab\"\n [class.disabled]=\"tab.disabled\"\n [ngClass]=\"tab.customClass\"\n >\n <ng-template #tabDetails>\n @if (!tab.headingRef) {\n <span>{{ tab.heading }}</span>\n }\n @if (tab.headingRef) {\n <ng-container [ngTemplateOutlet]=\"tab.headingRef\"></ng-container>\n }\n </ng-template>\n @if (tab.route) {\n <a\n class=\"nav-link\"\n [attr.id]=\"tab.id\"\n role=\"tab\"\n #anchorTab\n uxTabbableListItem\n uxFocusIndicator\n [attr.aria-controls]=\"tab.id\"\n [attr.aria-selected]=\"(_tabset.activeTab$ | async) === tab\"\n [attr.aria-disabled]=\"tab.disabled\"\n [routerLink]=\"tab.route\"\n [fragment]=\"tab.routerLinkExtras?.fragment\"\n [queryParams]=\"tab.routerLinkExtras?.queryParams\"\n [queryParamsHandling]=\"tab.routerLinkExtras?.queryParamsHandling\"\n [preserveFragment]=\"tab.routerLinkExtras?.preserveFragment\"\n [skipLocationChange]=\"tab.routerLinkExtras?.skipLocationChange\"\n [replaceUrl]=\"tab.routerLinkExtras?.replaceUrl\"\n [state]=\"tab.routerLinkExtras?.state\"\n (keydown)=\"handleKeyDown($event, anchorTab)\"\n >\n <ng-container [ngTemplateOutlet]=\"tabDetails\"> </ng-container>\n </a>\n }\n @if (!tab.route) {\n <a\n class=\"nav-link\"\n [attr.id]=\"tab.id\"\n role=\"tab\"\n uxTabbableListItem\n uxFocusIndicator\n (mousedown)=\"_tabset.select(tab)\"\n (activated)=\"_tabset.select(tab)\"\n [attr.aria-controls]=\"tab.id\"\n [attr.aria-selected]=\"(_tabset.activeTab$ | async) === tab\"\n [attr.aria-disabled]=\"tab.disabled\"\n >\n <ng-container [ngTemplateOutlet]=\"tabDetails\"> </ng-container>\n </a>\n }\n </li>\n }\n</ul>\n\n<!-- Tab panes -->\n<div class=\"tab-content\">\n <ng-content></ng-content>\n</div>\n" }]
}], propDecorators: { minimal: [{
type: Input
}], stacked: [{
type: Input
}], manual: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], _tabs: [{
type: ContentChildren,
args: [TabComponent]
}] } });
class TabsetModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TabsetModule, declarations: [TabsetComponent, TabComponent, TabHeadingDirective], imports: [AccessibilityModule, CommonModule, RouterModule], exports: [TabsetComponent, TabComponent, TabHeadingDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetModule, imports: [AccessibilityModule, CommonModule, RouterModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TabsetModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, RouterModule],
exports: [TabsetComponent, TabComponent, TabHeadingDirective],
declarations: [TabsetComponent, TabComponent, TabHeadingDirective],
}]
}] });
class PageHeaderCustomMenuDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderCustomMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: PageHeaderCustomMenuDirective, isStandalone: false, selector: "[uxPageHeaderCustomMenu], [uxPageHeaderCustomItem]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderCustomMenuDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxPageHeaderCustomMenu], [uxPageHeaderCustomItem]',
standalone: false,
}]
}] });
class PageHeaderIconMenuComponent {
select(item) {
if (item.select) {
item.select.call(item, item);
}
}
keydownHandler(item, event) {
switch (event.keyCode) {
case ENTER:
case SPACE:
this.select(item);
event.preventDefault();
event.stopPropagation();
break;
}
}
_getIconType(identifier) {
return identifier ? getIconType(identifier) : '';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderIconMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PageHeaderIconMenuComponent, isStandalone: false, selector: "ux-page-header-icon-menu", inputs: { menu: "menu" }, ngImport: i0, template: "<div class=\"page-header-icon-menu\">\n <button\n type=\"button\"\n class=\"page-header-icon-menu-button\"\n [attr.aria-label]=\"menu.label\"\n [uxMenuTriggerFor]=\"iconMenu\"\n (click)=\"select(menu)\"\n >\n <!-- Support all icon types -->\n @if (_getIconType(menu.icon) !== 'component') {\n <i [ngClass]=\"[_getIconType(menu.icon), menu.icon]\"> </i>\n }\n\n @if (_getIconType(menu.icon) === 'component') {\n <ux-icon [name]=\"menu.icon\"> </ux-icon>\n }\n\n @if (menu?.badge) {\n <span class=\"label label-primary\" aria-hidden=\"true\">{{ menu.badge }}</span>\n }\n </button>\n\n <ux-menu #iconMenu alignment=\"end\" menuClass=\"ux-page-header-icon-menu\">\n @for (dropdown of menu?.dropdown; track dropdown) {\n @if (dropdown.header) {\n <div class=\"dropdown-header\">\n <span class=\"font-bold\">{{ dropdown.title }}</span>\n </div>\n }\n @if (!dropdown.header) {\n <button\n type=\"button\"\n uxMenuItem\n (click)=\"select(dropdown)\"\n (keydown)=\"keydownHandler(dropdown, $event)\"\n >\n <span class=\"dropdown-item-title\">\n <!-- Support all icon types -->\n @if (_getIconType(dropdown.icon) !== 'component') {\n <i\n class=\"ux-fw\"\n [ngClass]=\"[_getIconType(dropdown.icon) || 'ux-icon', dropdown.icon || '']\"\n >\n </i>\n }\n @if (_getIconType(dropdown.icon) === 'component') {\n <ux-icon class=\"m-r-xs\" [name]=\"dropdown.icon\"> </ux-icon>\n }\n {{ dropdown.title }}\n </span>\n @if (dropdown.subtitle) {\n <span class=\"dropdown-item-subtitle\">{{ dropdown.subtitle }}</span>\n }\n </button>\n }\n @if (dropdown.divider) {\n <ux-menu-divider></ux-menu-divider>\n }\n }\n </ux-menu>\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "component", type: MenuItemComponent, selector: "[uxMenuItem]", inputs: ["disabled", "closeOnSelect", "role"], outputs: ["activate"] }, { kind: "component", type: MenuDividerComponent, selector: "ux-menu-divider" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderIconMenuComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-page-header-icon-menu', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"page-header-icon-menu\">\n <button\n type=\"button\"\n class=\"page-header-icon-menu-button\"\n [attr.aria-label]=\"menu.label\"\n [uxMenuTriggerFor]=\"iconMenu\"\n (click)=\"select(menu)\"\n >\n <!-- Support all icon types -->\n @if (_getIconType(menu.icon) !== 'component') {\n <i [ngClass]=\"[_getIconType(menu.icon), menu.icon]\"> </i>\n }\n\n @if (_getIconType(menu.icon) === 'component') {\n <ux-icon [name]=\"menu.icon\"> </ux-icon>\n }\n\n @if (menu?.badge) {\n <span class=\"label label-primary\" aria-hidden=\"true\">{{ menu.badge }}</span>\n }\n </button>\n\n <ux-menu #iconMenu alignment=\"end\" menuClass=\"ux-page-header-icon-menu\">\n @for (dropdown of menu?.dropdown; track dropdown) {\n @if (dropdown.header) {\n <div class=\"dropdown-header\">\n <span class=\"font-bold\">{{ dropdown.title }}</span>\n </div>\n }\n @if (!dropdown.header) {\n <button\n type=\"button\"\n uxMenuItem\n (click)=\"select(dropdown)\"\n (keydown)=\"keydownHandler(dropdown, $event)\"\n >\n <span class=\"dropdown-item-title\">\n <!-- Support all icon types -->\n @if (_getIconType(dropdown.icon) !== 'component') {\n <i\n class=\"ux-fw\"\n [ngClass]=\"[_getIconType(dropdown.icon) || 'ux-icon', dropdown.icon || '']\"\n >\n </i>\n }\n @if (_getIconType(dropdown.icon) === 'component') {\n <ux-icon class=\"m-r-xs\" [name]=\"dropdown.icon\"> </ux-icon>\n }\n {{ dropdown.title }}\n </span>\n @if (dropdown.subtitle) {\n <span class=\"dropdown-item-subtitle\">{{ dropdown.subtitle }}</span>\n }\n </button>\n }\n @if (dropdown.divider) {\n <ux-menu-divider></ux-menu-divider>\n }\n }\n </ux-menu>\n</div>\n" }]
}], propDecorators: { menu: [{
type: Input
}] } });
class PageHeaderService {
constructor() {
this._router = inject(Router);
this.items$ = new BehaviorSubject([]);
this.selected$ = new BehaviorSubject(null);
this.selectedRoot$ = new BehaviorSubject(null);
this.secondary$ = new BehaviorSubject(false);
this.secondaryNavigationAutoselect = false;
this._onDestroy = new Subject();
this.selected$
.pipe(takeUntil(this._onDestroy), map(selected => this.getRoot(selected)))
.subscribe(root => this.selectedRoot$.next(root));
this._router.events
.pipe(filter(event => event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError), takeUntil(this._onDestroy))
.subscribe(() => this.updateItemsWithActiveRoute());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
select(item, navigate = true) {
if (!item) {
return;
}
if (item.routerLink && navigate) {
// Trigger router navigation
const routerLink = Array.isArray(item.routerLink) ? item.routerLink : [item.routerLink];
this._router.navigate(routerLink, item.routerExtras);
}
else if (this.secondaryNavigationAutoselect && item.children && item.children.length > 0) {
// check to see if there is a child that is already marked as selected
const selectedChild = item.children.find(child => child.selected);
if (selectedChild) {
this.select(selectedChild);
this.selected$.next(selectedChild);
return;
}
// Select the first child that isn't disabled in secondaryNavigationAutoselect mode
const firstChild = item.children.find(_item => !_item.disabled);
if (firstChild) {
this.select(firstChild);
}
}
else {
// if we are in secondary navigation mode and we click a parent - dont deselect the child
if (this.secondary$.getValue() === true && this.isParentOf(this.selected$.getValue(), item)) {
return;
}
// Otherwise select the given item
this.selected$.next(item);
}
}
deselect(item) {
// deselect the current item
item.selected = false;
// iterate any children and deselect them
if (item.children) {
item.children.forEach(_item => this.deselect(_item));
}
}
deselectAll() {
this.items$.getValue().forEach(item => this.deselect(item));
}
updateItem(item, selected) {
// Item is selected if it is the selected item, or one of the selected item's ancestors.
item.selected = item === selected || this.isParentOf(selected, item);
if (item === selected) {
// call the select function if present
if (item.select) {
item.select.call(item, item);
}
}
}
setItems(items = []) {
// identify all parent elements
items.forEach(item => this.setParent(item));
this.items$.next(items);
// Set up the initially selected item
// If nothing is set as selected, using the initial route
const initialSelectedItem = items.find(item => item.selected === true);
if (initialSelectedItem) {
this.select(initialSelectedItem);
}
else {
this.updateItemsWithActiveRoute();
}
}
setSecondaryNavigation(enabled) {
this.secondary$.next(enabled);
}
getRoot(item) {
return item && item.parent ? this.getRoot(item.parent) : item;
}
setParent(item, parent) {
// set the parent field
item.parent = parent;
// call this function recursively on all children
if (item.children) {
item.children.forEach(child => this.setParent(child, item));
}
}
isParentOf(node, parent) {
// if there are no parents return false
if (!node || !node.parent) {
return false;
}
// if the parent is the match we are looking for return true
if (node.parent === parent) {
return true;
}
// if there are potentially grandparents then check them too
return this.isParentOf(node.parent, parent);
}
updateItemsWithActiveRoute() {
const activeItem = new PageHeaderActiveNavigationItem();
for (const item of this.items$.getValue()) {
this.findActiveItem(item, activeItem);
if (activeItem.exact) {
break;
}
}
if (activeItem.item) {
this.selected$.next(activeItem.item);
}
}
findActiveItem(item, activeItem) {
if (item.routerLink) {
const routerLink = Array.isArray(item.routerLink) ? item.routerLink : [item.routerLink];
const urlTree = this._router.createUrlTree(routerLink, item.routerExtras);
if (this._router.isActive(urlTree, true) && !activeItem.exact) {
// When the item route is an exact match, no need to look any further
activeItem.item = item;
activeItem.exact = true;
return;
}
if (this._router.isActive(urlTree, false)) {
// Store an inexact match and continue looking
activeItem.item = item;
activeItem.exact = false;
}
}
if (item.children) {
for (const childItem of item.children) {
this.findActiveItem(childItem, activeItem);
if (activeItem.exact) {
return;
}
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class PageHeaderActiveNavigationItem {
}
class PageHeaderNavigationDropdownItemComponent {
constructor() {
this._pageHeaderService = inject(PageHeaderService);
}
select(item) {
// clicking on an item that is disabled or with children then return
if (item.disabled || item.children) {
return;
}
// emit the selected item in an event
this._pageHeaderService.select(item);
}
keydownHandler(event, item) {
switch (event.keyCode) {
case ENTER:
case SPACE:
this.select(item);
event.preventDefault();
event.stopPropagation();
break;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationDropdownItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PageHeaderNavigationDropdownItemComponent, isStandalone: false, selector: "ux-page-header-horizontal-navigation-dropdown-item", inputs: { item: "item" }, exportAs: ["ux-page-header-horizontal-navigation-dropdown-item"], ngImport: i0, template: "@if (item.children && item.children.length > 0) {\n <div>\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"item.id\"\n [disabled]=\"item.disabled\"\n [class.selected]=\"item.selected\"\n [attr.aria-selected]=\"item.selected\"\n [uxMenuTriggerFor]=\"menu\"\n >\n <span class=\"dropdown-item-title\">{{ item.title }}</span>\n <ux-icon class=\"dropdown-item-icon\" name=\"next\"></ux-icon>\n </button>\n <ux-menu #menu placement=\"right\" menuClass=\"horizontal-navigation-dropdown-submenu\">\n @for (subItem of item.children; track subItem) {\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"subItem.id\"\n [disabled]=\"subItem.disabled\"\n [class.selected]=\"subItem.selected\"\n [attr.aria-selected]=\"subItem.selected\"\n (click)=\"select(subItem)\"\n (keydown)=\"keydownHandler($event, subItem)\"\n >\n <span class=\"dropdown-item-title\">{{ subItem.title }}</span>\n </button>\n }\n </ux-menu>\n </div>\n}\n\n@if (!item.children || item.children.length === 0) {\n <div>\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"item.id\"\n [disabled]=\"item.disabled\"\n [class.selected]=\"item.selected\"\n [attr.aria-selected]=\"item.selected\"\n (click)=\"select(item)\"\n (keydown)=\"keydownHandler($event, item)\"\n >\n <span class=\"dropdown-item-title\">{{ item.title }}</span>\n </button>\n </div>\n}\n", dependencies: [{ kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "component", type: MenuItemComponent, selector: "[uxMenuItem]", inputs: ["disabled", "closeOnSelect", "role"], outputs: ["activate"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationDropdownItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-page-header-horizontal-navigation-dropdown-item', exportAs: 'ux-page-header-horizontal-navigation-dropdown-item', standalone: false, template: "@if (item.children && item.children.length > 0) {\n <div>\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"item.id\"\n [disabled]=\"item.disabled\"\n [class.selected]=\"item.selected\"\n [attr.aria-selected]=\"item.selected\"\n [uxMenuTriggerFor]=\"menu\"\n >\n <span class=\"dropdown-item-title\">{{ item.title }}</span>\n <ux-icon class=\"dropdown-item-icon\" name=\"next\"></ux-icon>\n </button>\n <ux-menu #menu placement=\"right\" menuClass=\"horizontal-navigation-dropdown-submenu\">\n @for (subItem of item.children; track subItem) {\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"subItem.id\"\n [disabled]=\"subItem.disabled\"\n [class.selected]=\"subItem.selected\"\n [attr.aria-selected]=\"subItem.selected\"\n (click)=\"select(subItem)\"\n (keydown)=\"keydownHandler($event, subItem)\"\n >\n <span class=\"dropdown-item-title\">{{ subItem.title }}</span>\n </button>\n }\n </ux-menu>\n </div>\n}\n\n@if (!item.children || item.children.length === 0) {\n <div>\n <button\n type=\"button\"\n uxMenuItem\n [attr.id]=\"item.id\"\n [disabled]=\"item.disabled\"\n [class.selected]=\"item.selected\"\n [attr.aria-selected]=\"item.selected\"\n (click)=\"select(item)\"\n (keydown)=\"keydownHandler($event, item)\"\n >\n <span class=\"dropdown-item-title\">{{ item.title }}</span>\n </button>\n </div>\n}\n" }]
}], propDecorators: { item: [{
type: Input
}] } });
class PageHeaderNavigationService {
constructor() {
/**
* Emit when focus changes. We can't directly use the FocusKeyManager
* `change` observable as it cannot be instantiate until after the view
* has been instantiated.
*/
this._onChange = new Subject();
/** Unsubscribe on destroy */
this._onDestroy = new Subject();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Make menu items navigable using arrow keys */
initialize(items) {
// store the query list for future lookups
this._items = items;
// create new focus key manager with horizontal orientation
this._focusManager = new FocusKeyManager(items).withHorizontalOrientation('ltr');
// listen for changes to the focused item
this._focusManager.change
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._onChange.next());
// make the first item tabbable initially
this._focusManager.updateActiveItem(0);
// on changes ensure there is always an active item
this._items.changes.subscribe(() => {
if (this._items.length > 0 &&
this._items.toArray().indexOf(this._focusManager.activeItem) === -1) {
this._focusManager.updateActiveItem(0);
}
});
// emit the initial change
this._onChange.next();
}
/** Listen for keyboard events */
onKeydown(event) {
this._focusManager.onKeydown(event);
}
/** Get the tab index for this item as an observable */
getTabIndex(item) {
return this._onChange.pipe(map(() => this.getItemTabIndex(item)), tick(), takeUntil(this._onDestroy));
}
/** Determine the tab index of a given item */
getItemTabIndex(item) {
// until the focus key manager is set up make everything tabbable
if (!this._items) {
return 0;
}
// get the index within the query list
const index = this._items.toArray().indexOf(item);
// if it is the current active element then it is tabbable
return index === this._focusManager.activeItemIndex ? 0 : -1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationService, decorators: [{
type: Injectable
}] });
class PageHeaderNavigationItemComponent {
constructor() {
this.elementRef = inject(ElementRef);
this._pageHeaderService = inject(PageHeaderService);
this._navigationService = inject(PageHeaderNavigationService);
/** Store the secondary state */
this.secondary$ = this._pageHeaderService.secondary$;
/** Update the tabindex based on keyboard input */
this._tabindex = this._navigationService.getTabIndex(this);
/** Unsubscribe when the component is destroyed */
this._onDestroy = new Subject();
}
/** Access the data for this dropdown item */
set item(item) {
this._item = item;
this._iconType = getIconType(item.icon);
}
get item() {
return this._item;
}
ngAfterViewInit() {
this._pageHeaderService.selected$
.pipe(tick(), takeUntil(this._onDestroy))
.subscribe(selectedItem => {
// Update selected state for this item
this._pageHeaderService.updateItem(this.item, selectedItem);
if (selectedItem && this.isOpen) {
this.isOpen = false;
}
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
focus() {
this.navigationBtn.nativeElement.focus();
}
select() {
// if the item is disabled or has children then do nothing at this stage
if (this.item.disabled ||
(this.item.children && this._pageHeaderService.secondary$.getValue() === false)) {
return;
}
// if autoselect is enabled the first child should be selected when we click on the parent item (this element).
// We should remove the selected state on all children as the service will perform the selection
// of the first item and handle any routing etc..
if (this._pageHeaderService.secondaryNavigationAutoselect &&
Array.isArray(this.item.children)) {
this.item.children.forEach(item => (item.selected = false));
}
// otherwise select the current item
this._pageHeaderService.select(this.item);
}
onKeydown(event, target) {
if (event.keyCode === LEFT_ARROW || event.keyCode === RIGHT_ARROW) {
this._navigationService.onKeydown(event);
}
if (event.keyCode === SPACE) {
event.preventDefault();
target.click();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PageHeaderNavigationItemComponent, isStandalone: false, selector: "ux-page-header-horizontal-navigation-item", inputs: { item: "item" }, host: { listeners: { "keydown": "onKeydown($event,$event.target)" } }, viewQueries: [{ propertyName: "navigationBtn", first: true, predicate: ["navigationBtn"], descendants: true }], ngImport: i0, template: "@if (_item.children && _item.children.length > 0 && (secondary$ | async) === false) {\n <div>\n <button\n #navigationBtn\n type=\"button\"\n [attr.id]=\"_item.id\"\n [tabindex]=\"_tabindex | async\"\n [uxMenuTriggerFor]=\"menu\"\n [disabled]=\"_item.disabled\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n [class.open]=\"isOpen\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n <ux-icon class=\"navigation-item-dropdown-icon\" name=\"down\"></ux-icon>\n </button>\n <ux-menu\n #menu\n menuClass=\"horizontal-navigation-dropdown-menu\"\n (opened)=\"isOpen = true\"\n (closed)=\"isOpen = false\"\n >\n @for (item of _item?.children; track item) {\n <ux-page-header-horizontal-navigation-dropdown-item [item]=\"item\">\n </ux-page-header-horizontal-navigation-dropdown-item>\n }\n </ux-menu>\n </div>\n}\n\n@if (!_item.children || _item.children.length === 0 || (secondary$ | async)) {\n @if (_item.routerLink) {\n <a\n uxFocusIndicator\n #navigationBtn\n [tabindex]=\"_tabindex | async\"\n [attr.id]=\"_item.id\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n [routerLink]=\"_item.routerLink\"\n [fragment]=\"_item.routerExtras?.fragment\"\n [queryParams]=\"_item.routerExtras?.queryParams\"\n [queryParamsHandling]=\"_item.routerExtras?.queryParamsHandling\"\n [preserveFragment]=\"_item.routerExtras?.preserveFragment\"\n [skipLocationChange]=\"_item.routerExtras?.skipLocationChange\"\n [replaceUrl]=\"_item.routerExtras?.replaceUrl\"\n [state]=\"_item.routerExtras?.state\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n </a>\n }\n @if (!_item.routerLink) {\n <button\n uxFocusIndicator\n #navigationBtn\n type=\"button\"\n [tabindex]=\"_tabindex | async\"\n [attr.id]=\"_item.id\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n (click)=\"select()\"\n [disabled]=\"_item.disabled\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n </button>\n }\n}\n\n<!-- Support all icon types -->\n<ng-template #navigationItemContent>\n @if (_item.icon) {\n @if (_iconType !== 'component') {\n <i class=\"navigation-item-icon\" [ngClass]=\"[_iconType, _item.icon]\"> </i>\n }\n @if (_iconType === 'component') {\n <ux-icon class=\"navigation-item-icon\" [name]=\"_item.icon\"> </ux-icon>\n }\n }\n\n <span class=\"navigation-item-label\">{{ _item?.title }}</span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: MenuComponent, selector: "ux-menu", inputs: ["id", "placement", "alignment", "animate", "menuClass"], outputs: ["opening", "opened", "closing", "closed"] }, { kind: "directive", type: MenuTriggerDirective, selector: "[uxMenuTriggerFor]", inputs: ["uxMenuTriggerFor", "disabled", "uxMenuParent", "closeOnBlur"], outputs: ["closed"], exportAs: ["ux-menu-trigger"] }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: PageHeaderNavigationDropdownItemComponent, selector: "ux-page-header-horizontal-navigation-dropdown-item", inputs: ["item"], exportAs: ["ux-page-header-horizontal-navigation-dropdown-item"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-page-header-horizontal-navigation-item', standalone: false, template: "@if (_item.children && _item.children.length > 0 && (secondary$ | async) === false) {\n <div>\n <button\n #navigationBtn\n type=\"button\"\n [attr.id]=\"_item.id\"\n [tabindex]=\"_tabindex | async\"\n [uxMenuTriggerFor]=\"menu\"\n [disabled]=\"_item.disabled\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n [class.open]=\"isOpen\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n <ux-icon class=\"navigation-item-dropdown-icon\" name=\"down\"></ux-icon>\n </button>\n <ux-menu\n #menu\n menuClass=\"horizontal-navigation-dropdown-menu\"\n (opened)=\"isOpen = true\"\n (closed)=\"isOpen = false\"\n >\n @for (item of _item?.children; track item) {\n <ux-page-header-horizontal-navigation-dropdown-item [item]=\"item\">\n </ux-page-header-horizontal-navigation-dropdown-item>\n }\n </ux-menu>\n </div>\n}\n\n@if (!_item.children || _item.children.length === 0 || (secondary$ | async)) {\n @if (_item.routerLink) {\n <a\n uxFocusIndicator\n #navigationBtn\n [tabindex]=\"_tabindex | async\"\n [attr.id]=\"_item.id\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n [routerLink]=\"_item.routerLink\"\n [fragment]=\"_item.routerExtras?.fragment\"\n [queryParams]=\"_item.routerExtras?.queryParams\"\n [queryParamsHandling]=\"_item.routerExtras?.queryParamsHandling\"\n [preserveFragment]=\"_item.routerExtras?.preserveFragment\"\n [skipLocationChange]=\"_item.routerExtras?.skipLocationChange\"\n [replaceUrl]=\"_item.routerExtras?.replaceUrl\"\n [state]=\"_item.routerExtras?.state\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n </a>\n }\n @if (!_item.routerLink) {\n <button\n uxFocusIndicator\n #navigationBtn\n type=\"button\"\n [tabindex]=\"_tabindex | async\"\n [attr.id]=\"_item.id\"\n role=\"menuitem\"\n class=\"horizontal-navigation-button\"\n [class.disabled]=\"_item.disabled\"\n [class.selected]=\"_item.selected\"\n (click)=\"select()\"\n [disabled]=\"_item.disabled\"\n >\n <ng-container [ngTemplateOutlet]=\"navigationItemContent\"> </ng-container>\n </button>\n }\n}\n\n<!-- Support all icon types -->\n<ng-template #navigationItemContent>\n @if (_item.icon) {\n @if (_iconType !== 'component') {\n <i class=\"navigation-item-icon\" [ngClass]=\"[_iconType, _item.icon]\"> </i>\n }\n @if (_iconType === 'component') {\n <ux-icon class=\"navigation-item-icon\" [name]=\"_item.icon\"> </ux-icon>\n }\n }\n\n <span class=\"navigation-item-label\">{{ _item?.title }}</span>\n</ng-template>\n" }]
}], propDecorators: { item: [{
type: Input
}], navigationBtn: [{
type: ViewChild,
args: ['navigationBtn', { static: false }]
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event', '$event.target']]
}] } });
class PageHeaderNavigationSecondaryItemDirective {
constructor() {
this._pageHeaderService = inject(PageHeaderService);
this._onDestroy = new Subject();
}
ngOnInit() {
this._pageHeaderService.selected$.pipe(delay(0), takeUntil(this._onDestroy)).subscribe(next => {
// Update selected state for this item
this._pageHeaderService.updateItem(this.item, next);
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationSecondaryItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: PageHeaderNavigationSecondaryItemDirective, isStandalone: false, selector: "[uxPageHeaderNavigationSecondaryItem]", inputs: { item: ["uxPageHeaderNavigationSecondaryItem", "item"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationSecondaryItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxPageHeaderNavigationSecondaryItem]',
standalone: false,
}]
}], propDecorators: { item: [{
type: Input,
args: ['uxPageHeaderNavigationSecondaryItem']
}] } });
class PageHeaderNavigationComponent {
constructor() {
this.elementRef = inject(ElementRef);
this.resizeService = inject(ResizeService);
this._navigationService = inject(PageHeaderNavigationService);
this._pageHeaderService = inject(PageHeaderService);
this._changeDetectorRef = inject(ChangeDetectorRef);
this.items$ = this._pageHeaderService.items$;
this.indicatorVisible = false;
this.indicatorX = 0;
this.indicatorWidth = 0;
this._onDestroy = new Subject();
this.resizeService
.addResizeListener(this.elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(this.updateSelectedIndicator.bind(this));
this._pageHeaderService.selected$
.pipe(takeUntil(this._onDestroy), distinctUntilChanged())
.subscribe(this.updateSelectedIndicator.bind(this));
this._pageHeaderService.secondary$
.pipe(takeUntil(this._onDestroy), distinctUntilChanged())
.subscribe(this.updateSelectedIndicator.bind(this));
}
ngAfterViewInit() {
this.updateSelectedIndicator();
// setup the page focus key manager
this._navigationService.initialize(this.menuItems);
// add or remove the menubar role if there are not menuitems to remove accessibility errors
this.menuItems?.changes
.pipe(startWith(this.menuItems.toArray()), takeUntil(this._onDestroy))
.subscribe(() => {
if (this.menuItems.length > 0) {
this.elementRef.nativeElement.setAttribute('role', 'menubar');
}
else {
this.elementRef.nativeElement.removeAttribute('role');
}
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
updateSelectedIndicator() {
setTimeout(() => {
// find the selected item
const selected = this.menuItems.find(item => item.item.selected);
// determine whether or not to show the indicator
this.indicatorVisible = !!selected;
// set the width of the indicator to match the width of the navigation item
if (selected) {
const styles = getComputedStyle(selected.elementRef.nativeElement);
this.indicatorX = selected.elementRef.nativeElement.offsetLeft;
this.indicatorWidth = parseInt(styles.getPropertyValue('width'));
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PageHeaderNavigationComponent, isStandalone: false, selector: "ux-page-header-horizontal-navigation", providers: [PageHeaderNavigationService], viewQueries: [{ propertyName: "menuItems", predicate: PageHeaderNavigationItemComponent, descendants: true }], ngImport: i0, template: "@for (item of items$ | async; track item) {\n <ux-page-header-horizontal-navigation-item [item]=\"item\">\n </ux-page-header-horizontal-navigation-item>\n}\n\n<div\n class=\"selected-indicator\"\n [style.opacity]=\"indicatorVisible ? 1 : 0\"\n [style.margin-left.px]=\"indicatorX\"\n [style.width.px]=\"indicatorWidth\"\n></div>\n", dependencies: [{ kind: "component", type: PageHeaderNavigationItemComponent, selector: "ux-page-header-horizontal-navigation-item", inputs: ["item"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderNavigationComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-page-header-horizontal-navigation', providers: [PageHeaderNavigationService], standalone: false, template: "@for (item of items$ | async; track item) {\n <ux-page-header-horizontal-navigation-item [item]=\"item\">\n </ux-page-header-horizontal-navigation-item>\n}\n\n<div\n class=\"selected-indicator\"\n [style.opacity]=\"indicatorVisible ? 1 : 0\"\n [style.margin-left.px]=\"indicatorX\"\n [style.width.px]=\"indicatorWidth\"\n></div>\n" }]
}], ctorParameters: () => [], propDecorators: { menuItems: [{
type: ViewChildren,
args: [PageHeaderNavigationItemComponent]
}] } });
class PageHeaderComponent {
constructor() {
this._colorService = inject(ColorService);
this._pageHeaderService = inject(PageHeaderService);
/** The alignment of the primary navigation tabs. */
this.alignment = 'center';
/** Determines whether or not to display the page header in the regular or condensed form. */
this.condensed = false;
/** Determines whether or not a back button should be visible in the page header. */
this.backVisible = true;
/** The alignment of the secondary navigation tabs. */
this.secondaryNavigationAlignment = 'center';
/**
* The style of the breadcrumbs.
* - standard: The breadcrumbs use the same styling as the navigation tabs.
* - small: The breadcrumbs use a smaller font, and case is not adjusted.
*/
this.crumbsStyle = 'standard';
/** Emit whenever the back button is clicked */
this.backClick = new EventEmitter();
/** Emit whenever the product logo in the left corner is clicked. */
this.logoClick = new EventEmitter();
/** The currently selected page header item */
this.selected$ = this._pageHeaderService.selected$;
/** The currently selected root menu item - this may be different from selected$ if a child menu item is selected */
this.selectedRoot$ = this._pageHeaderService.selectedRoot$;
this._crumbs = [];
}
/** If set, the first child item will get selected when the parent item is selected. */
set secondaryNavigationAutoselect(value) {
this._pageHeaderService.secondaryNavigationAutoselect = value;
}
get secondaryNavigationAutoselect() {
return this._pageHeaderService.secondaryNavigationAutoselect;
}
/** The primary navigation tabs. Use the children property in combination with [secondaryNavigation]="true" to include secondary navigation tabs. */
set items(items) {
this._pageHeaderService.setItems(items);
}
/** Whether to show a second level of navigation for any items with children. */
set secondaryNavigation(enabled) {
this._pageHeaderService.setSecondaryNavigation(enabled);
}
get secondaryNavigation() {
return this._pageHeaderService.secondary$.getValue();
}
/** The optional set of breadcrumbs to display on the left side of the masthead. */
set crumbs(crumbs) {
this._crumbs = crumbs;
}
get crumbs() {
return this.condensed ? [...this._crumbs, { title: this.header }] : this._crumbs;
}
/** The logo background color. This can either be the name of a color from the color palette, or a CSS color value. */
set logoBackground(color) {
this._logoBackground = this._colorService.resolve(color);
}
get logoBackground() {
return this._logoBackground;
}
/** The logo text color, when a product acronym is specified via header. This can either be the name of a color from the color palette, or a CSS color value. */
set logoForeground(color) {
this._logoForeground = this._colorService.resolve(color);
}
get logoForeground() {
return this._logoForeground;
}
get _hasLogoClick() {
return this.logoClick.observers.length > 0;
}
select(item, navigate) {
this._pageHeaderService.select(item, navigate);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PageHeaderComponent, isStandalone: false, selector: "ux-page-header", inputs: { logo: "logo", header: "header", subheader: "subheader", alignment: "alignment", condensed: "condensed", iconMenus: "iconMenus", backVisible: "backVisible", secondaryNavigationAlignment: "secondaryNavigationAlignment", secondaryNavigationAutoselect: "secondaryNavigationAutoselect", items: "items", secondaryNavigation: "secondaryNavigation", crumbs: "crumbs", crumbsStyle: "crumbsStyle", logoBackground: "logoBackground", logoForeground: "logoForeground" }, outputs: { backClick: "backClick", logoClick: "logoClick" }, providers: [PageHeaderService], queries: [{ propertyName: "subheaderTemplate", first: true, predicate: ["subheader"], descendants: true }, { propertyName: "logoTemplate", first: true, predicate: ["logoTemplate"], descendants: true }, { propertyName: "secondaryNavigationLeadingContentTemplate", first: true, predicate: ["secondaryNavigationLeadingContent"], descendants: true }, { propertyName: "secondaryNavigationTrailingContentTemplate", first: true, predicate: ["secondaryNavigationTrailingContent"], descendants: true }, { propertyName: "customMenus", predicate: PageHeaderCustomMenuDirective, read: TemplateRef }], exportAs: ["ux-page-header"], ngImport: i0, template: "<div class=\"ux-page-header\" [class.page-header-condensed]=\"condensed\" role=\"banner\">\n @if (!condensed) {\n <div class=\"page-header-content\">\n <!-- Logo/product acronym -->\n <div\n uxFocusIndicator\n (keydown.enter)=\"logoClick.emit($event)\"\n [attr.tabindex]=\"_hasLogoClick ? 0 : -1\"\n [class.page-header-logo-template]=\"logoTemplate\"\n [class.clickable]=\"_hasLogoClick\"\n class=\"page-header-logo-container\"\n role=\"presentation\"\n [style.backgroundColor]=\"logoBackground\"\n [style.color]=\"logoForeground\"\n (click)=\"logoClick.emit($event)\"\n >\n @if (logo && !logoTemplate) {\n <img [attr.src]=\"logo\" [alt]=\"header\" class=\"page-header-logo\" />\n }\n @if (header && !logo && !logoTemplate) {\n <h1 class=\"page-header-acronym\">{{ header }}</h1>\n }\n @if (logoTemplate) {\n <ng-container [ngTemplateOutlet]=\"logoTemplate\"></ng-container>\n }\n </div>\n <!-- Sub-title -->\n @if (subheader || subheaderTemplate) {\n <div class=\"page-header-subtitle-container\">\n @if (subheader) {\n <span class=\"page-header-subtitle\">{{ subheader }}</span>\n }\n <ng-container [ngTemplateOutlet]=\"subheaderTemplate\"></ng-container>\n </div>\n }\n <div class=\"page-header-state-container\" role=\"navigation\">\n <!-- Back button -->\n @if (backVisible === true) {\n <button\n uxFocusIndicator\n type=\"button\"\n class=\"page-header-back-button\"\n (click)=\"backClick.emit($event)\"\n aria-label=\"Go Back\"\n >\n <ux-icon name=\"previous\" class=\"text-primary\"></ux-icon>\n </button>\n }\n <!-- Breadcrumbs and header -->\n <div class=\"page-header-title-container\">\n @if (crumbs && crumbs.length > 0) {\n <ux-breadcrumbs\n [class.ux-breadcrumbs-small]=\"crumbsStyle === 'small'\"\n [crumbs]=\"crumbs\"\n ></ux-breadcrumbs>\n }\n <h1 class=\"page-header-title\">{{ header }}</h1>\n </div>\n </div>\n <!-- Primary navigation -->\n <div\n class=\"page-header-navigation\"\n [ngClass]=\"alignment\"\n role=\"navigation\"\n aria-label=\"Primary Navigation\"\n >\n <ux-page-header-horizontal-navigation></ux-page-header-horizontal-navigation>\n </div>\n <!-- Icon menus -->\n <div class=\"page-header-icon-menus\" role=\"toolbar\">\n @for (menu of customMenus; track menu) {\n <ng-container [ngTemplateOutlet]=\"menu\"></ng-container>\n }\n @for (menu of iconMenus; track menu) {\n <ux-page-header-icon-menu [menu]=\"menu\"></ux-page-header-icon-menu>\n }\n </div>\n </div>\n }\n\n <!-- Display This Section Optimized for Condensed Mode -->\n @if (condensed) {\n <div class=\"page-header-condensed-content\">\n <div class=\"page-header-breadcrumbs\" role=\"navigation\">\n <ux-breadcrumbs [crumbs]=\"crumbs\"></ux-breadcrumbs>\n </div>\n <div\n class=\"page-header-navigation\"\n [ngClass]=\"alignment\"\n role=\"navigation\"\n aria-label=\"Primary Navigation\"\n >\n <!-- The Top Navigation Options -->\n <ux-page-header-horizontal-navigation></ux-page-header-horizontal-navigation>\n </div>\n <div class=\"page-header-icon-menus\" role=\"toolbar\">\n @for (menu of customMenus; track menu) {\n <ng-container [ngTemplateOutlet]=\"menu\"></ng-container>\n }\n @for (menu of iconMenus; track menu) {\n <ux-page-header-icon-menu [menu]=\"menu\"></ux-page-header-icon-menu>\n }\n </div>\n </div>\n }\n</div>\n\n<!-- Secondary Header Section -->\n@if (secondaryNavigation && (selectedRoot$ | async) !== (selected$ | async)) {\n <div class=\"ux-page-header-secondary\">\n <!-- Secondary Navigation Leading Content -->\n @if (secondaryNavigationLeadingContentTemplate || secondaryNavigationTrailingContentTemplate) {\n <div class=\"page-header-secondary-leading-content\">\n <ng-container [ngTemplateOutlet]=\"secondaryNavigationLeadingContentTemplate\"></ng-container>\n </div>\n }\n <!-- Secondary Navigation (children of top level items) -->\n <div [ngClass]=\"['page-header-navigation', secondaryNavigationAlignment]\" role=\"navigation\">\n @if ((selectedRoot$ | async)?.children; as children) {\n <ux-tabset [manual]=\"true\">\n @for (child of children; track child) {\n <ux-tab\n [heading]=\"child.title\"\n [route]=\"child.routerLink\"\n [id]=\"child.id\"\n [routerLinkExtras]=\"child.routerExtras\"\n [active]=\"child === (selected$ | async)\"\n (activated)=\"select(child, false)\"\n [uxPageHeaderNavigationSecondaryItem]=\"child\"\n [disabled]=\"child.disabled\"\n >\n </ux-tab>\n }\n </ux-tabset>\n }\n </div>\n <!-- Secondary Navigation Trialing Content -->\n @if (secondaryNavigationTrailingContentTemplate) {\n <div class=\"page-header-secondary-trailing-content\">\n <ng-container\n [ngTemplateOutlet]=\"secondaryNavigationTrailingContentTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "component", type: BreadcrumbsComponent, selector: "ux-breadcrumbs", inputs: ["crumbs"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "component", type: TabsetComponent, selector: "ux-tabset", inputs: ["minimal", "stacked", "manual", "aria-label"] }, { kind: "component", type: TabComponent, selector: "ux-tab", inputs: ["id", "active", "disabled", "heading", "route", "routerLinkExtras", "customClass"], outputs: ["activeChange", "activated", "deactivated"] }, { kind: "component", type: PageHeaderIconMenuComponent, selector: "ux-page-header-icon-menu", inputs: ["menu"] }, { kind: "component", type: PageHeaderNavigationComponent, selector: "ux-page-header-horizontal-navigation" }, { kind: "directive", type: PageHeaderNavigationSecondaryItemDirective, selector: "[uxPageHeaderNavigationSecondaryItem]", inputs: ["uxPageHeaderNavigationSecondaryItem"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-page-header', exportAs: 'ux-page-header', providers: [PageHeaderService], standalone: false, template: "<div class=\"ux-page-header\" [class.page-header-condensed]=\"condensed\" role=\"banner\">\n @if (!condensed) {\n <div class=\"page-header-content\">\n <!-- Logo/product acronym -->\n <div\n uxFocusIndicator\n (keydown.enter)=\"logoClick.emit($event)\"\n [attr.tabindex]=\"_hasLogoClick ? 0 : -1\"\n [class.page-header-logo-template]=\"logoTemplate\"\n [class.clickable]=\"_hasLogoClick\"\n class=\"page-header-logo-container\"\n role=\"presentation\"\n [style.backgroundColor]=\"logoBackground\"\n [style.color]=\"logoForeground\"\n (click)=\"logoClick.emit($event)\"\n >\n @if (logo && !logoTemplate) {\n <img [attr.src]=\"logo\" [alt]=\"header\" class=\"page-header-logo\" />\n }\n @if (header && !logo && !logoTemplate) {\n <h1 class=\"page-header-acronym\">{{ header }}</h1>\n }\n @if (logoTemplate) {\n <ng-container [ngTemplateOutlet]=\"logoTemplate\"></ng-container>\n }\n </div>\n <!-- Sub-title -->\n @if (subheader || subheaderTemplate) {\n <div class=\"page-header-subtitle-container\">\n @if (subheader) {\n <span class=\"page-header-subtitle\">{{ subheader }}</span>\n }\n <ng-container [ngTemplateOutlet]=\"subheaderTemplate\"></ng-container>\n </div>\n }\n <div class=\"page-header-state-container\" role=\"navigation\">\n <!-- Back button -->\n @if (backVisible === true) {\n <button\n uxFocusIndicator\n type=\"button\"\n class=\"page-header-back-button\"\n (click)=\"backClick.emit($event)\"\n aria-label=\"Go Back\"\n >\n <ux-icon name=\"previous\" class=\"text-primary\"></ux-icon>\n </button>\n }\n <!-- Breadcrumbs and header -->\n <div class=\"page-header-title-container\">\n @if (crumbs && crumbs.length > 0) {\n <ux-breadcrumbs\n [class.ux-breadcrumbs-small]=\"crumbsStyle === 'small'\"\n [crumbs]=\"crumbs\"\n ></ux-breadcrumbs>\n }\n <h1 class=\"page-header-title\">{{ header }}</h1>\n </div>\n </div>\n <!-- Primary navigation -->\n <div\n class=\"page-header-navigation\"\n [ngClass]=\"alignment\"\n role=\"navigation\"\n aria-label=\"Primary Navigation\"\n >\n <ux-page-header-horizontal-navigation></ux-page-header-horizontal-navigation>\n </div>\n <!-- Icon menus -->\n <div class=\"page-header-icon-menus\" role=\"toolbar\">\n @for (menu of customMenus; track menu) {\n <ng-container [ngTemplateOutlet]=\"menu\"></ng-container>\n }\n @for (menu of iconMenus; track menu) {\n <ux-page-header-icon-menu [menu]=\"menu\"></ux-page-header-icon-menu>\n }\n </div>\n </div>\n }\n\n <!-- Display This Section Optimized for Condensed Mode -->\n @if (condensed) {\n <div class=\"page-header-condensed-content\">\n <div class=\"page-header-breadcrumbs\" role=\"navigation\">\n <ux-breadcrumbs [crumbs]=\"crumbs\"></ux-breadcrumbs>\n </div>\n <div\n class=\"page-header-navigation\"\n [ngClass]=\"alignment\"\n role=\"navigation\"\n aria-label=\"Primary Navigation\"\n >\n <!-- The Top Navigation Options -->\n <ux-page-header-horizontal-navigation></ux-page-header-horizontal-navigation>\n </div>\n <div class=\"page-header-icon-menus\" role=\"toolbar\">\n @for (menu of customMenus; track menu) {\n <ng-container [ngTemplateOutlet]=\"menu\"></ng-container>\n }\n @for (menu of iconMenus; track menu) {\n <ux-page-header-icon-menu [menu]=\"menu\"></ux-page-header-icon-menu>\n }\n </div>\n </div>\n }\n</div>\n\n<!-- Secondary Header Section -->\n@if (secondaryNavigation && (selectedRoot$ | async) !== (selected$ | async)) {\n <div class=\"ux-page-header-secondary\">\n <!-- Secondary Navigation Leading Content -->\n @if (secondaryNavigationLeadingContentTemplate || secondaryNavigationTrailingContentTemplate) {\n <div class=\"page-header-secondary-leading-content\">\n <ng-container [ngTemplateOutlet]=\"secondaryNavigationLeadingContentTemplate\"></ng-container>\n </div>\n }\n <!-- Secondary Navigation (children of top level items) -->\n <div [ngClass]=\"['page-header-navigation', secondaryNavigationAlignment]\" role=\"navigation\">\n @if ((selectedRoot$ | async)?.children; as children) {\n <ux-tabset [manual]=\"true\">\n @for (child of children; track child) {\n <ux-tab\n [heading]=\"child.title\"\n [route]=\"child.routerLink\"\n [id]=\"child.id\"\n [routerLinkExtras]=\"child.routerExtras\"\n [active]=\"child === (selected$ | async)\"\n (activated)=\"select(child, false)\"\n [uxPageHeaderNavigationSecondaryItem]=\"child\"\n [disabled]=\"child.disabled\"\n >\n </ux-tab>\n }\n </ux-tabset>\n }\n </div>\n <!-- Secondary Navigation Trialing Content -->\n @if (secondaryNavigationTrailingContentTemplate) {\n <div class=\"page-header-secondary-trailing-content\">\n <ng-container\n [ngTemplateOutlet]=\"secondaryNavigationTrailingContentTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n}\n" }]
}], propDecorators: { logo: [{
type: Input
}], header: [{
type: Input
}], subheader: [{
type: Input
}], alignment: [{
type: Input
}], condensed: [{
type: Input
}], iconMenus: [{
type: Input
}], backVisible: [{
type: Input
}], secondaryNavigationAlignment: [{
type: Input
}], secondaryNavigationAutoselect: [{
type: Input
}], items: [{
type: Input
}], secondaryNavigation: [{
type: Input
}], crumbs: [{
type: Input
}], crumbsStyle: [{
type: Input
}], logoBackground: [{
type: Input
}], logoForeground: [{
type: Input
}], backClick: [{
type: Output
}], logoClick: [{
type: Output
}], subheaderTemplate: [{
type: ContentChild,
args: ['subheader', { static: false }]
}], logoTemplate: [{
type: ContentChild,
args: ['logoTemplate', { static: false }]
}], secondaryNavigationLeadingContentTemplate: [{
type: ContentChild,
args: ['secondaryNavigationLeadingContent', { static: false }]
}], secondaryNavigationTrailingContentTemplate: [{
type: ContentChild,
args: ['secondaryNavigationTrailingContent', { static: false }]
}], customMenus: [{
type: ContentChildren,
args: [PageHeaderCustomMenuDirective, { read: TemplateRef }]
}] } });
class PageHeaderModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderModule, declarations: [PageHeaderComponent,
PageHeaderIconMenuComponent,
PageHeaderCustomMenuDirective,
PageHeaderNavigationComponent,
PageHeaderNavigationItemComponent,
PageHeaderNavigationDropdownItemComponent,
PageHeaderNavigationSecondaryItemDirective], imports: [A11yModule,
AccessibilityModule,
BreadcrumbsModule,
ColorServiceModule,
CommonModule,
IconModule,
MenuModule,
ResizeModule,
TabsetModule,
RouterModule], exports: [PageHeaderComponent, PageHeaderCustomMenuDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderModule, imports: [A11yModule,
AccessibilityModule,
BreadcrumbsModule,
ColorServiceModule,
CommonModule,
IconModule,
MenuModule,
ResizeModule,
TabsetModule,
RouterModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PageHeaderModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
BreadcrumbsModule,
ColorServiceModule,
CommonModule,
IconModule,
MenuModule,
ResizeModule,
TabsetModule,
RouterModule,
],
exports: [PageHeaderComponent, PageHeaderCustomMenuDirective],
declarations: [
PageHeaderComponent,
PageHeaderIconMenuComponent,
PageHeaderCustomMenuDirective,
PageHeaderNavigationComponent,
PageHeaderNavigationItemComponent,
PageHeaderNavigationDropdownItemComponent,
PageHeaderNavigationSecondaryItemDirective,
],
}]
}] });
const PAGINATION_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PaginationComponent),
multi: true,
};
class PaginationComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Specify if we should show the next and previous buttons */
this.directionButtons = true;
/** Limit the number of pages shown at any given time */
this.maxSize = 5;
/** Specify if the component should be disabled */
this.disabled = false;
/** Aria Label for the component navigation */
this.ariaLabel = 'Pagination Navigation';
/** Aria label for the previous button */
this.previousAriaLabel = 'Navigate to the previous page';
/** Aria label for the next button */
this.nextAriaLabel = 'Navigate to the next page';
/** Emit the current page number */
this.pageChange = new EventEmitter();
/** Emit the total number of pages */
this.numPages = new EventEmitter();
/** Store a list of pages to display in the UI */
this.pages = [];
/** ControlValueAccessor functions */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouched = () => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChange = () => { };
this.isKeyboardEvent = false;
this._page = 1;
this._total = 100;
this._pagesize = 10;
}
/** Specify the index of the active page */
set page(page) {
// do nothing if the page has not changed
if (page === this._page) {
return;
}
this._page = page;
this.pages = this.getPages();
// mark this component as changed
this.onChange(this.page);
}
get page() {
return this._page;
}
/** Specify the page size */
set itemsPerPage(pagesize) {
this._pagesize = pagesize;
this.pages = this.getPages();
}
/** Specify how many items there are in total */
set totalItems(total) {
this._total = total;
this.pages = this.getPages();
}
get pageCount() {
return Math.ceil(this._total / this._pagesize);
}
ngOnInit() {
this.pages = this.getPages();
}
select(index) {
// find the page we want to go to
const target = this.pages.find(page => page.index === index);
// if the page is out of bounds then do nothing
if (!target) {
return;
}
// mark this component as touched
this.onTouched();
// set this as the current page
this.page = target.index;
// update the visible pages
this.pages = this.getPages();
// emit the current page
this.pageChange.emit(this.page);
}
trackByFn(_index, item) {
return item.index;
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
writeValue(page) {
this.page = page;
this._changeDetector.markForCheck();
}
getPages() {
// create a new array to store the pages
const pages = [];
// create all possible pages
for (let index = 1; index <= this.pageCount; index++) {
pages.push({ index, visible: this.isPageVisible(index) });
}
// emit the number of pages
this.numPages.emit(this.pageCount);
return pages;
}
isPageVisible(index) {
// if we do not have a max size specified or the number of pages is less than the max size then it is always visible
if (!this.maxSize || this.pageCount <= this.maxSize) {
return true;
}
// find the starting position
let start = Math.max(1, Math.ceil(this.page - this.maxSize / 2));
const end = Math.min(start + this.maxSize, this.pageCount + 1);
// if the range is less than the max size we need to adjust the starting point
const range = end - start;
if (range < this.maxSize) {
start = start - (this.maxSize - range);
}
// if the item equals the start position or is less than the end position then show it
return index >= start && index < end;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PaginationComponent, isStandalone: false, selector: "ux-pagination", inputs: { directionButtons: "directionButtons", maxSize: "maxSize", disabled: "disabled", classes: ["class", "classes"], pageBtnClass: "pageBtnClass", ariaLabel: ["aria-label", "ariaLabel"], previousAriaLabel: "previousAriaLabel", nextAriaLabel: "nextAriaLabel", page: "page", previousBtnTemplate: "previousBtnTemplate", nextBtnTemplate: "nextBtnTemplate", itemsPerPage: "itemsPerPage", totalItems: "totalItems" }, outputs: { pageChange: "pageChange", numPages: "numPages" }, providers: [PAGINATION_CONTROL_VALUE_ACCESSOR], ngImport: i0, template: "<nav role=\"navigation\" [attr.aria-label]=\"ariaLabel\">\n <ul\n #container\n class=\"pagination\"\n [ngClass]=\"classes\"\n direction=\"horizontal\"\n (blur)=\"isKeyboardEvent = false\"\n (keydown.ArrowLeft)=\"select(page - 1); isKeyboardEvent = true\"\n (keydown.ArrowRight)=\"select(page + 1); isKeyboardEvent = true\"\n (keydown.Home)=\"select(1); isKeyboardEvent = true; $event.preventDefault()\"\n (keydown.End)=\"select(pageCount); isKeyboardEvent = true; $event.preventDefault()\"\n >\n @if (directionButtons) {\n <li\n class=\"pagination-prev page-item\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"page === 1 || disabled\"\n >\n <button\n class=\"page-link\"\n [tabindex]=\"page === 1 || disabled ? -1 : 0\"\n [attr.aria-label]=\"previousAriaLabel\"\n [ngClass]=\"pageBtnClass\"\n (click)=\"select(page - 1)\"\n (keydown.enter)=\"select(page - 1)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"previousBtnTemplate || defaultPreviousBtnTemplate\"\n ></ng-container>\n </button>\n </li>\n }\n\n @for (pg of pages; track trackByFn($index, pg)) {\n @if (pg.visible) {\n <li\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"disabled\"\n [attr.aria-setsize]=\"pageCount\"\n [attr.aria-posinset]=\"pg.index\"\n [class.active]=\"page === pg.index\"\n class=\"pagination-page page-item\"\n >\n <button\n class=\"page-link\"\n tabindex=\"0\"\n [ngClass]=\"pageBtnClass\"\n [focusIf]=\"isKeyboardEvent && page === pg.index\"\n [attr.aria-current]=\"page === pg.index\"\n (click)=\"select(pg.index)\"\n (keydown.enter)=\"select(pg.index)\"\n >\n {{ pg.index }}\n </button>\n </li>\n }\n }\n\n @if (directionButtons) {\n <li\n class=\"pagination-next page-item\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"page === pageCount || disabled\"\n >\n <button\n class=\"page-link\"\n [tabindex]=\"page === pageCount || disabled ? -1 : 0\"\n [attr.aria-label]=\"nextAriaLabel\"\n [ngClass]=\"pageBtnClass\"\n (click)=\"select(page + 1)\"\n (keydown.enter)=\"select(page + 1)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nextBtnTemplate || defaultNextBtnTemplate\"\n ></ng-container>\n </button>\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #defaultPreviousBtnTemplate>\n <ux-icon class=\"pagination-prev-icon-default\" name=\"previous\"></ux-icon>\n</ng-template>\n\n<ng-template #defaultNextBtnTemplate>\n <ux-icon class=\"pagination-next-icon-default\" name=\"next\"></ux-icon>\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PaginationComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-pagination', providers: [PAGINATION_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<nav role=\"navigation\" [attr.aria-label]=\"ariaLabel\">\n <ul\n #container\n class=\"pagination\"\n [ngClass]=\"classes\"\n direction=\"horizontal\"\n (blur)=\"isKeyboardEvent = false\"\n (keydown.ArrowLeft)=\"select(page - 1); isKeyboardEvent = true\"\n (keydown.ArrowRight)=\"select(page + 1); isKeyboardEvent = true\"\n (keydown.Home)=\"select(1); isKeyboardEvent = true; $event.preventDefault()\"\n (keydown.End)=\"select(pageCount); isKeyboardEvent = true; $event.preventDefault()\"\n >\n @if (directionButtons) {\n <li\n class=\"pagination-prev page-item\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"page === 1 || disabled\"\n >\n <button\n class=\"page-link\"\n [tabindex]=\"page === 1 || disabled ? -1 : 0\"\n [attr.aria-label]=\"previousAriaLabel\"\n [ngClass]=\"pageBtnClass\"\n (click)=\"select(page - 1)\"\n (keydown.enter)=\"select(page - 1)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"previousBtnTemplate || defaultPreviousBtnTemplate\"\n ></ng-container>\n </button>\n </li>\n }\n\n @for (pg of pages; track trackByFn($index, pg)) {\n @if (pg.visible) {\n <li\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"disabled\"\n [attr.aria-setsize]=\"pageCount\"\n [attr.aria-posinset]=\"pg.index\"\n [class.active]=\"page === pg.index\"\n class=\"pagination-page page-item\"\n >\n <button\n class=\"page-link\"\n tabindex=\"0\"\n [ngClass]=\"pageBtnClass\"\n [focusIf]=\"isKeyboardEvent && page === pg.index\"\n [attr.aria-current]=\"page === pg.index\"\n (click)=\"select(pg.index)\"\n (keydown.enter)=\"select(pg.index)\"\n >\n {{ pg.index }}\n </button>\n </li>\n }\n }\n\n @if (directionButtons) {\n <li\n class=\"pagination-next page-item\"\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n [checkChildren]=\"true\"\n [class.disabled]=\"page === pageCount || disabled\"\n >\n <button\n class=\"page-link\"\n [tabindex]=\"page === pageCount || disabled ? -1 : 0\"\n [attr.aria-label]=\"nextAriaLabel\"\n [ngClass]=\"pageBtnClass\"\n (click)=\"select(page + 1)\"\n (keydown.enter)=\"select(page + 1)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nextBtnTemplate || defaultNextBtnTemplate\"\n ></ng-container>\n </button>\n </li>\n }\n </ul>\n</nav>\n\n<ng-template #defaultPreviousBtnTemplate>\n <ux-icon class=\"pagination-prev-icon-default\" name=\"previous\"></ux-icon>\n</ng-template>\n\n<ng-template #defaultNextBtnTemplate>\n <ux-icon class=\"pagination-next-icon-default\" name=\"next\"></ux-icon>\n</ng-template>\n" }]
}], propDecorators: { directionButtons: [{
type: Input
}], maxSize: [{
type: Input
}], disabled: [{
type: Input
}], classes: [{
type: Input,
args: ['class']
}], pageBtnClass: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], previousAriaLabel: [{
type: Input
}], nextAriaLabel: [{
type: Input
}], page: [{
type: Input
}], previousBtnTemplate: [{
type: Input
}], nextBtnTemplate: [{
type: Input
}], itemsPerPage: [{
type: Input
}], totalItems: [{
type: Input
}], pageChange: [{
type: Output
}], numPages: [{
type: Output
}] } });
class PaginationModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PaginationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: PaginationModule, declarations: [PaginationComponent], imports: [A11yModule, AccessibilityModule, CommonModule, FocusIfModule, IconModule], exports: [PaginationComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PaginationModule, imports: [A11yModule, AccessibilityModule, CommonModule, FocusIfModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PaginationModule, decorators: [{
type: NgModule,
args: [{
imports: [A11yModule, AccessibilityModule, CommonModule, FocusIfModule, IconModule],
declarations: [PaginationComponent],
exports: [PaginationComponent],
}]
}] });
class PartitionMapSegmentEventsDirective {
constructor() {
this._elementRef = inject(ElementRef);
/** Emit when the segment receives focus */
this.segmentFocus = new EventEmitter();
/** Emit when the segment is blurred */
this.segmentBlur = new EventEmitter();
/** Unsubscribe from observables */
this._onDestroy = new Subject();
}
ngAfterViewInit() {
// Get the parent segment element
// Note we cannot use DI to get the element as this is a template
// and the context has no knowledge of the partition map template
const segment = this.getSegmentElement();
if (segment) {
fromEvent(segment, 'focus')
.pipe(takeUntil(this._onDestroy))
.subscribe(event => this.segmentFocus.emit(event));
fromEvent(segment, 'blur')
.pipe(takeUntil(this._onDestroy))
.subscribe(event => this.segmentBlur.emit(event));
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Find the parent element that is a partition map segment
*/
getSegmentElement() {
let ancestor = this._elementRef.nativeElement.parentElement;
while (ancestor !== null) {
if (ancestor.classList.contains('partition-map-segment')) {
return ancestor;
}
ancestor = ancestor.parentElement;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapSegmentEventsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: PartitionMapSegmentEventsDirective, isStandalone: false, selector: "[segmentFocus],[segmentBlur]", outputs: { segmentFocus: "segmentFocus", segmentBlur: "segmentBlur" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapSegmentEventsDirective, decorators: [{
type: Directive,
args: [{
selector: '[segmentFocus],[segmentBlur]',
standalone: false,
}]
}], propDecorators: { segmentFocus: [{
type: Output
}], segmentBlur: [{
type: Output
}] } });
/* eslint-disable @typescript-eslint/no-empty-object-type */
class PartitionMapComponent {
constructor() {
this._colorService = inject(ColorService);
this._elementRef = inject(ElementRef);
this._changeDetector = inject(ChangeDetectorRef);
this._ngZone = inject(NgZone);
this._focusOrigin = inject(FocusIndicatorOriginService);
this._contrastRatio = inject(ContrastService);
this._liveAnnouncer = inject(LiveAnnouncer);
this._resizeService = inject(ResizeService);
this._segmentCache = new WeakMap();
/** Determine the pixel height of collapsed segments. */
this.collapsedHeight = 40;
/** Define a minimum desired pixel width for a segment. */
this.minSegmentWidth = 5;
/** Define the function that will return the aria announcement for a given segment. */
this.segmentAnnouncement = this.defaultSegmentAnnouncement;
/** Emits whenever a segment is selected. */
this.selectedChange = new EventEmitter();
/** Store the processed segments */
this._segments = [];
/** Store the specified color sequences */
this._colors = [[]];
/** Store the assigned colors for each segment */
this._segmentColors = new Map();
/** Store the visible x scale */
this._x = scaleLinear().range([0, 100]);
/** Store the visible y scale */
this._y = scaleLinear().range([0, 100]);
/** Store the width of the chart on resize to avoid any reflow */
this._width = this._elementRef.nativeElement.offsetWidth;
/** Store the height of the chart on resize to avoid any reflow */
this._height = this._elementRef.nativeElement.offsetHeight;
/** Flag to determine when the inputs have all been bound */
this._initialized = false;
/** Unsubscribe from any observables on destroy */
this._onDestroy = new Subject();
}
/** Define the colors to be used for each row and the order they should appear. */
set colors(colors) {
this._colors = colors;
// clear the save color mappings
this._segmentColors.clear();
}
/** Define the dataset to display in the chart. */
set dataset(dataset) {
// store the current dataset
this._dataset = dataset;
// clear any existing color assignments
this._segmentColors.clear();
// update the segment layout
this.setDataset(dataset);
}
get dataset() {
return this._dataset;
}
/** Define the currently selected item. */
set selected(selected) {
// if this is set before the dataset is process then store it to be selected later
if (this._segments.length === 0) {
this._awaitingSelection = selected;
return;
}
// perform the selection
this.select(this.getHierarchyNodeFromSegment(selected));
}
ngOnInit() {
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(dimensions => {
this._width = dimensions.width;
this._height = dimensions.height;
this._changeDetector.detectChanges();
// set our new ranges
if (this._selected) {
this._x.domain([
this.getSegmentX(this._selected),
this.getSegmentX(this._selected) + this.getSegmentWidth(this._selected),
]);
this._y.domain([this._selected.y0, 1]).range([this.getTotalCollapsedHeight(), 100]);
}
// render the chart to ensure positions and sizes are correct
this.updateSegments();
});
this._initialized = true;
// Run again so that the colors get applied
this._changeDetector.detectChanges();
}
ngOnDestroy() {
this._resizeService.removeResizeListener(this._elementRef.nativeElement);
this._onDestroy.next();
this._onDestroy.complete();
}
/** Handle segment clicks */
_onSegmentSelect(segment) {
// if the clicked node is already selected, navigate to the parent node
this.select(this._isSelected(segment) && segment.parent ? segment.parent : segment);
}
/** Get the background color for a given segment */
_getBackgroundColor(segment) {
// This can be called before `colors` is initialized, in which case return the default color
if (!this._initialized) {
return '#fff';
}
// each segment has a determinable color key based on the name and depth
const key = `${segment.data.name} - ${segment.depth}`;
// check if a segment with the same name (and depth) has previously
if (this._segmentColors.has(key)) {
return this._segmentColors.get(key);
}
// get the corresponding row of colors
const sequence = this.getColorSequence(segment.depth);
// if the sequence has not been specified return a default of white
if (!sequence || sequence.length === 0) {
return '#fff';
}
// get siblings
const siblings = this.getAllSiblings(segment);
// get the previous sibling if there is one
const sibling = siblings[siblings.indexOf(segment) - 1];
// if there is a previous sibling then get its color and use the next one in the sequence
if (sibling) {
const index = sequence.indexOf(this._getBackgroundColor(sibling));
const color = sequence[(index + 1) % sequence.length];
// store the color by key
this._segmentColors.set(key, color);
return color;
}
// store the color by key
this._segmentColors.set(key, sequence[0]);
// if there is no previous sibling then simply return the first color in the sequence
return sequence[0];
}
/** Get the tab index of a segment */
_getTabIndex(segment) {
return segment === this._focusableSegment ? 0 : -1;
}
/** Shift focus to the parent segment */
_focusParent(segment) {
// if there is no parent (ie, we are the root segment) then retain focus
if (!segment.parent) {
return;
}
// otherwise focus the parent
this.focusSegment(segment.parent);
}
/** Shift focus to the child segment */
_focusChild(segment) {
// if there are no children (ie, we are a leaf segment) then retain focus
if (!segment.children) {
return;
}
// find the first visible child
const child = segment.children.find(_segment => this.isVisible(_segment));
// otherwise focus the first visible child
if (child) {
this.focusSegment(child);
}
}
/** Shift focus to the sibling segment */
_focusSibling(segment, delta) {
// if we are the root node then do nothing
if (!segment.parent) {
return;
}
// get a list of all the siblings (at the same row regardless of the same parent)
const siblings = this.getAllSiblings(segment);
// get the index of the segment in the list of siblings
const index = siblings.indexOf(segment);
// get the target sibling
const sibling = siblings[index + delta];
// ensure the sibling is visible otherwise we can't select it
if (!sibling || !this.isVisible(sibling)) {
return;
}
// otherwise focus the sibling
this.focusSegment(sibling);
}
_focusFirstSibling(segment) {
// if we are the root node then do nothing
if (!segment.parent) {
return;
}
// get a list of all the siblings (at the same row regardless of the same parent)
const siblings = this.getAllSiblings(segment);
// find the first visible sibling
const sibling = siblings.find(_sibling => this.isVisible(_sibling));
// ensure there is a sibling
if (!sibling) {
return;
}
// otherwise focus the sibling
this.focusSegment(sibling);
}
_focusLastSibling(segment) {
// if we are the root node then do nothing
if (!segment.parent) {
return;
}
// get a list of all the siblings (at the same row regardless of the same parent)
const siblings = this.getAllSiblings(segment);
// find the last visible sibling
const sibling = siblings.reverse().find(_sibling => this.isVisible(_sibling));
// ensure there is a sibling
if (!sibling) {
return;
}
// otherwise focus the sibling
this.focusSegment(sibling);
}
/** Determine if a given segment is currently collapsed */
_isCollapsed(segment) {
return this._selected && segment.depth < this._selected.depth;
}
/** Determine if a given segment is currently selected */
_isSelected(segment) {
return this._selected === segment;
}
/** Get the contast color class for the segment */
_getContrastColor(segment) {
const backgroundColor = this._getBackgroundColor(segment);
const lightColor = ThemeColor.parse('#fff');
const darkColor = ThemeColor.parse('#000');
const color = this._contrastRatio.getContrastColor(ThemeColor.parse(backgroundColor), lightColor, darkColor);
return color === lightColor ? 'partition-map-segment-light' : 'partition-map-segment-dark';
}
/** Provide an aria announcement when the node is focused */
_onFocus(segment) {
// get all ancestors
const ancestors = segment.ancestors().map(ancestor => ancestor.data);
// get the current node and the parent nodes
const [item, ...parents] = ancestors;
// get the hierarchy node data from the item
const hierarchichalItem = this.getHierarchyNodeFromSegment(item);
// get the function that creates the announcement
const announcement = this.segmentAnnouncement({
item,
parents,
value: this._getSegmentValue(segment.data),
collapsed: this._isCollapsed(hierarchichalItem),
selected: this._isSelected(hierarchichalItem),
});
// make aria announcement
this._liveAnnouncer.announce(announcement);
}
/** Determine if the content is smaller than the width of an ellipsis */
_getSegmentContentHidden(segment) {
// get the width of the segment as a pixel value
const width = (this._width / 100) * this.getNormalizedSegmentWidth(segment);
// if the width is less than 50 px hide the content
return width < 50;
}
/** Get the value of a segment based on the accumulation of all child values */
_getSegmentValue(segment) {
// it it has a value then return the value
// eslint-disable-next-line no-prototype-builtins
if (segment.hasOwnProperty('value')) {
return segment.value;
}
return segment.children.reduce((value, child) => value + this._getSegmentValue(child), 0);
}
_getContext(segment) {
const context = {
segment: segment.data,
value: this._getSegmentValue(segment.data),
color: this._getBackgroundColor(segment),
expanded: !this._isCollapsed(segment),
depth: segment.depth,
children: [],
};
// map the children to their contexts
if (segment.children) {
context.children = segment.children.map(this._getContext.bind(this));
}
return context;
}
trackByIndex(index) {
return index;
}
/** Convert the public facing data structure into the layout format we require */
setDataset(dataset) {
// convert the segments to a hierarchichal structure
const segmentHierarchy = hierarchy(dataset).sum(this.getSegmentValue); // calculate segment values based on their children
// store the processed segments
const root = partition()(segmentHierarchy);
// store the flattened form of the segments
this._segments = root.descendants();
// mark the root node as focusable
this._focusableSegment = root;
// we need to run change detection here so the `*ngFor` will update and add all the segments to the DOM
this._changeDetector.detectChanges();
// select all the segments within the chart
this._segmentsSelection = select(this._elementRef.nativeElement)
.selectAll('.partition-map-segment')
.data(this._segments);
// set the correct sizing and position of the segments
this.updateSegments();
// if there is an item waiting to be selected then select it
if (this._awaitingSelection) {
// select the desired segment
this.select(this.getHierarchyNodeFromSegment(this._awaitingSelection));
// clear the pending selection in case the dataset changes we don't want to attempt another selection
this._awaitingSelection = null;
}
}
/** Update the size and position of the segments */
updateSegments() {
// if the chart has not yet been initialised do nothing
if (!this._segmentsSelection) {
return;
}
// clear the segment cache
this._segmentCache = new WeakMap();
// perform the chart positioning and sizing
this._segmentsSelection
.style('left', data => this.getNormalizedSegmentX(data) + '%')
.style('top', data => this.getNormalizedSegmentY(data) + '%')
.style('width', data => this.getNormalizedSegmentWidth(data) + 0.01 + '%')
.style('height', data => this.getNormalizedSegmentHeight(data) + '%')
.style('padding-right', data => this.getSegmentPaddingRight(data) + '%')
.style('padding-left', data => this.getSegmentPaddingLeft(data) + '%');
}
cacheSegment(segment, data) {
// get any existing cache data
const existing = this._segmentCache.get(segment) ?? {};
// store the new data
this._segmentCache.set(segment, { ...existing, ...data });
}
getSegmentCache(segment, key) {
return this._segmentCache.get(segment)?.[key];
}
/**
* Get the X position of a given segment. The X position can be determined
* by calculating the width of every sibling segment to the left of it
*/
getSegmentX(segment) {
// if root node then return the position
if (!segment.parent) {
return segment.x0;
}
const cachedX = this.getSegmentCache(segment.data, 'x');
if (cachedX !== undefined) {
return cachedX;
}
// set initial start position equal to that of the parent
let accumulation = this.getSegmentX(segment.parent);
// iterate each previous sibling to accumulate the widths
for (const sibling of segment.parent.children) {
// if we have reached the current node then return all previous widths
if (sibling === segment) {
this.cacheSegment(segment.data, { x: accumulation });
return accumulation;
}
// keep a tally of all the widths of previous siblings
accumulation += this.getSegmentWidth(sibling);
}
}
/** Calculate width based of each segment */
getSegmentWidth(segment) {
// if root node then return 1 always
if (!segment.parent) {
return 1;
}
const cachedWidth = this.getSegmentCache(segment.data, 'width');
if (cachedWidth !== undefined) {
return cachedWidth;
}
// get width of parent
const parentOffset = this.getSegmentWidth(segment.parent) / (segment.parent.x1 - segment.parent.x0);
// get the original width of the segment
const width = segment.x1 - segment.x0;
// if the item is a descendant of the selected item then apply the modifier
if (this.isDescendantOfSelected(segment)) {
// we want to try an ensure that children are at least the specified minimum width
// however it may not always be possible, but we should be able to at least distribute the widths better
// even if we cannot meet the minimum desired width.
const modifier = this.getDistributionModifier(segment);
// return the width of the current node relative to the parent
const result = width * modifier * parentOffset;
// store the result in the cache
this.cacheSegment(segment.data, { width: result });
return result;
}
const result = width * parentOffset;
// store the result in the cache
this.cacheSegment(segment.data, { width: result });
return result;
}
/** Return the X position of the segment in a normalized form based on the specifiec domain */
getNormalizedSegmentX(segment) {
return this._x(this.getSegmentX(segment));
}
/** Return the Y position of the segment in a normalized form based on the specifiec domain */
getNormalizedSegmentY(segment) {
// if there is a selected node we should take into account any collapsed nodes
if (this._isCollapsed(segment)) {
return segment.depth * this.getCollapsedHeight();
}
// otherwise simply return the normalized value
return this._y(segment.y0);
}
/** Return the width of the segment in a normalized form based on the specifiec domain */
getNormalizedSegmentWidth(segment) {
return (this._x(this.getSegmentX(segment) + this.getSegmentWidth(segment)) -
this._x(this.getSegmentX(segment)));
}
/** Return the height of the segment in a normalized form based on the specifiec domain */
getNormalizedSegmentHeight(segment) {
// if there is a selected node we should take into account any collapsed nodes
if (this._isCollapsed(segment)) {
return this.getCollapsedHeight();
}
// otherwise simply return the normalized value
return this._y(segment.y0 + (segment.y1 - segment.y0)) - this._y(segment.y0);
}
/**
* As parent segments collapse they increase in size, as the content is centered this can
* cause the content to appear either mis-aligned or off screen. We can calculate the padding
* required to always ensure the content appears visibly centered within the node.
*/
getSegmentPaddingRight(segment) {
// non-collapsed node do not require any padding
if (!this._isCollapsed(segment)) {
return 0;
}
return (this.getNormalizedSegmentWidth(segment) -
this.getSegmentPaddingLeft(segment) -
this.getNormalizedSegmentWidth(this._selected));
}
getSegmentPaddingLeft(segment) {
// non-collapsed node do not require any padding
if (!this._isCollapsed(segment)) {
return 0;
}
return Math.abs(this.getNormalizedSegmentX(segment));
}
/**
* This function returns the value for each segment. Leaf segments will have a value property which we can simply return, however
* non-leaf segments should get their values based on the leaf segments that are children, in which case we can return 0
*/
getSegmentValue(segment) {
// eslint-disable-next-line no-prototype-builtins
if (segment.hasOwnProperty('value')) {
const value = segment.value;
// we must ensure that a leaf node never has no width otherwise things can get weird
return Math.max(value, 1);
}
// if it has children then return 0 to base the value of the width of the children
return 0;
}
/** Get the total height of all the collapse rows */
getTotalCollapsedHeight() {
return this._selected ? this._selected.depth * this.getCollapsedHeight() : 0;
}
/** Get the collapsed height in percentage format */
getCollapsedHeight() {
return parseFloat(((this.collapsedHeight / this._height) * 100).toPrecision(3));
}
/** Determine if a given segment is currently visible based on the selected segment */
isVisible(segment) {
// if no segment is selected then all segments are visible
if (!this._selected) {
return true;
}
// if there is a selected node then it should be a direct ancestor or descendant to be visible
return !![...this._selected.ancestors(), ...this._selected.descendants()].find(_segment => _segment === segment);
}
/** Update the focusable item and perform a focus */
focusSegment(segment) {
// get the segment element from the data
const element = this._segmentsSelection
.nodes()
.find(node => select(node).data()[0] === segment);
// if for some reason an element isn't found then stop here
if (!element) {
return;
}
// update the focusable segment
this._focusableSegment = segment;
// set the focus origin as a keyboard event
this._focusOrigin.setOrigin('keyboard');
// focus the element
element.focus();
// ensure we do not change scroll position when focusing
this._elementRef.nativeElement.scrollLeft = 0;
this._elementRef.nativeElement.scrollTop = 0;
}
/** Get all the segments at a given depth */
getAllSiblings(segment) {
return this._segments.filter(_segment => _segment.depth === segment.depth);
}
getHierarchyNodeFromSegment(segment) {
return this._segments.find(_segment => _segment.data === segment);
}
/** Select a specified segment */
select(segment) {
// if no segment is specified or it is already selected then do nothing
if (!segment || this._isSelected(segment)) {
return;
}
// clear the segment cache
this._segmentCache = new WeakMap();
// emit the selection
this.selectedChange.emit(segment.data);
// store the selected segment
this._selected = segment;
// update the focusable segment
this._focusableSegment = segment;
// set our new ranges
this._x.domain([
this.getSegmentX(segment),
this.getSegmentX(segment) + this.getSegmentWidth(segment),
]);
this._y.domain([segment.y0, 1]).range([this.getTotalCollapsedHeight(), 100]);
// create the transition
const segmentTransition = transition$1('organizationChartSegmentTransition').duration(500);
// update the segment sizes - outside angular zone as there is lots of `requestAnimationFrames` triggering lots of change detection
this._ngZone.runOutsideAngular(() => {
this._segmentsSelection
.transition(segmentTransition)
.style('left', data => this.getNormalizedSegmentX(data) + '%')
.style('top', data => this.getNormalizedSegmentY(data) + '%')
.style('width', data => this.getNormalizedSegmentWidth(data) + 0.01 + '%')
.style('height', data => this.getNormalizedSegmentHeight(data) + '%')
.style('padding-right', data => this.getSegmentPaddingRight(data) + '%')
.style('padding-left', data => this.getSegmentPaddingLeft(data) + '%');
});
}
/** Normalize the available colors to a string[][] from portentially a ThemeColor[][] */
getColorSequence(depth) {
// get the target row
const colorSet = this._colors[depth];
// if no color set available throw an error
if (!colorSet) {
throw new Error('Partition Map: Please provide a color sequence for items with a depth of ' + depth);
}
// convert this row to an array of strings
return colorSet.map(color => ThemeColor.isInstanceOf(color)
? color.toRgba()
: this._colorService.resolve(color));
}
/** Determine if a segment is a descendant of the currently selected item */
isDescendantOfSelected(segment) {
// if there are no segments selected then return true
if (!this._selected) {
return true;
}
// if the segment is the selected segment then it is not a descendant
if (this._selected === segment) {
return false;
}
return !!this._selected.descendants().find(_segment => _segment === segment);
}
/**
* We have an option to allow a minimum desired width for items. This will
* allow us to attempt to determine the size a segment would be accounting for very
* small segments that have their widths artifically increased to make them more visible
*/
getDistributionModifier(segment) {
// calculate the desired number of pixels as a percentage
const minSegmentWidth = (this.minSegmentWidth / this._width) * 100;
// map to a segment width pair
const siblings = segment.parent.children.map(_segment => {
return { segment: _segment, width: this._x(_segment.x1 - _segment.x0) };
});
// a simple closure to check if we now have acceptable sizes
const isAcceptable = (segments) => !segments.find(_segment => _segment.width < minSegmentWidth) ||
segments.filter(_segment => _segment.width < minSegmentWidth).length === siblings.length;
// if all segments are above or below the desired width then we can stop here
if (isAcceptable(siblings)) {
return 1;
}
// find the total amount we need to reclaim for other segments
let amountToReclaim = siblings.reduce((accumulation, _segment) => accumulation + (_segment.width < minSegmentWidth ? minSegmentWidth - _segment.width : 0), 0);
// loop through adjusting the segments until we either make all acceptable sizes or cannot resize any further
while (!isAcceptable(siblings) && amountToReclaim !== 0) {
// determine which segments can shrink
const shrinkableSiblings = siblings.filter(sibling => sibling.width > minSegmentWidth);
// determine which segments need to grow
const growableSiblings = siblings.filter(sibling => sibling.width < minSegmentWidth);
// if there are no items that can be shrunk/grown then do nothing
if (shrinkableSiblings.length === 0 || growableSiblings.length === 0) {
break;
}
// determine the target amount to remove from each segment
const shrinkTarget = amountToReclaim / shrinkableSiblings.length;
// store the amount we have reclaimed in this pass
let reclaimed = 0;
// iterate each segment and subtract accordingly
for (const sibling of shrinkableSiblings) {
// determine how much we can actually subtract - as subtracting the target may bring the width down below the
// minimum which we don't want, so instead determine if we can subtract the target amount, otherwise figure out
// how much we can subtract without bringing the width below the desired minimum
const subtractAmount = sibling.width - shrinkTarget > minSegmentWidth
? shrinkTarget
: sibling.width - minSegmentWidth;
// update the amount to reclaim with the new value
reclaimed += subtractAmount;
// update the sibling width
sibling.width -= subtractAmount;
}
// update the amount left to reclaim
amountToReclaim -= reclaimed;
// determine the target amount to add to each segment
const growTarget = reclaimed / growableSiblings.length;
// add the available reclaimed amount to the segment that need to grow
for (const sibling of growableSiblings) {
// determine the amount we need to add. The target amount may be larger than the amount we need
// to add so ensure we only add the amount we need and no more.
const addAmount = sibling.width + growTarget < minSegmentWidth
? growTarget
: minSegmentWidth - sibling.width;
// update the sibling width
sibling.width += addAmount;
}
}
// identify the current widget from all the siblings
const matchingSegment = siblings.find(sibling => sibling.segment === segment);
// check if we are the last sibling
const isLast = siblings.findIndex(sibling => sibling.segment === segment) === siblings.length - 1;
// if we are the last and somehow we are smaller than the parent node, we want to bump up the size of the last node
if (isLast) {
// get the total parent width
const parentWidth = this._x(segment.parent.x1 - segment.parent.x0);
// get the total width of all the children
const width = siblings.reduce((total, sibling) => total + sibling.width, 0);
// check if need to expand the last node
if (parentWidth !== width) {
return ((matchingSegment.width + (parentWidth - width)) /
this._x(matchingSegment.segment.x1 - matchingSegment.segment.x0));
}
}
// determine the amount the size has changed
return matchingSegment.width / this._x(matchingSegment.segment.x1 - matchingSegment.segment.x0);
}
/** Get the default announcement when a segment is focused */
defaultSegmentAnnouncement(info) {
// create the announcement
if (info.parents.length === 0) {
return `This is the root segment. It has a value of ${info.value}.`;
}
// otherwise inform the user of the parent hierarchy
return `${info.item.name} has a value of ${info.value} and is a ${info.parents.map(parent => `descendant of ${parent.name}`).join(' and a ')}`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: PartitionMapComponent, isStandalone: false, selector: "ux-partition-map", inputs: { colors: "colors", collapsedHeight: "collapsedHeight", minSegmentWidth: "minSegmentWidth", dataset: "dataset", selected: "selected", segmentAnnouncement: "segmentAnnouncement" }, outputs: { selectedChange: "selectedChange" }, host: { attributes: { "role": "tree", "aria-orientation": "vertical" } }, queries: [{ propertyName: "segmentTemplate", first: true, predicate: ["partitionMapSegment"], descendants: true }], ngImport: i0, template: "@for (segment of _segments; track trackByIndex($index)) {\n <div\n class=\"partition-map-segment\"\n uxFocusIndicator\n [ngClass]=\"_getContrastColor(segment)\"\n [style.background-color]=\"_getBackgroundColor(segment)\"\n [tabIndex]=\"_getTabIndex(segment)\"\n (click)=\"_onSegmentSelect(segment)\"\n (focus)=\"_onFocus(segment)\"\n role=\"treeitem\"\n [attr.aria-expanded]=\"!_isCollapsed(segment)\"\n [attr.aria-selected]=\"_isSelected(segment)\"\n [attr.aria-level]=\"segment.depth + 1\"\n [attr.aria-label]=\"segment.data.name\"\n (keydown.Enter)=\"_onSegmentSelect(segment)\"\n (keydown.ArrowUp)=\"_focusParent(segment); $event.preventDefault()\"\n (keydown.ArrowDown)=\"_focusChild(segment); $event.preventDefault()\"\n (keydown.ArrowLeft)=\"_focusSibling(segment, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"_focusSibling(segment, 1); $event.preventDefault()\"\n (keydown.Home)=\"_focusFirstSibling(segment); $event.preventDefault()\"\n (keydown.End)=\"_focusLastSibling(segment); $event.preventDefault()\"\n >\n <div\n class=\"partition-map-segment-content\"\n [class.partition-map-segment-content-hidden]=\"_getSegmentContentHidden(segment)\"\n >\n <!-- Show default template if provided -->\n @if (!segmentTemplate) {\n <span class=\"partition-map-segment-label\">\n {{ segment.data.name }}\n </span>\n }\n <!-- Show custom template if provided -->\n @if (segmentTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"segmentTemplate\"\n [ngTemplateOutletContext]=\"_getContext(segment)\"\n >\n </ng-container>\n }\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-partition-map', changeDetection: ChangeDetectionStrategy.OnPush, host: {
role: 'tree',
'aria-orientation': 'vertical',
}, standalone: false, template: "@for (segment of _segments; track trackByIndex($index)) {\n <div\n class=\"partition-map-segment\"\n uxFocusIndicator\n [ngClass]=\"_getContrastColor(segment)\"\n [style.background-color]=\"_getBackgroundColor(segment)\"\n [tabIndex]=\"_getTabIndex(segment)\"\n (click)=\"_onSegmentSelect(segment)\"\n (focus)=\"_onFocus(segment)\"\n role=\"treeitem\"\n [attr.aria-expanded]=\"!_isCollapsed(segment)\"\n [attr.aria-selected]=\"_isSelected(segment)\"\n [attr.aria-level]=\"segment.depth + 1\"\n [attr.aria-label]=\"segment.data.name\"\n (keydown.Enter)=\"_onSegmentSelect(segment)\"\n (keydown.ArrowUp)=\"_focusParent(segment); $event.preventDefault()\"\n (keydown.ArrowDown)=\"_focusChild(segment); $event.preventDefault()\"\n (keydown.ArrowLeft)=\"_focusSibling(segment, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"_focusSibling(segment, 1); $event.preventDefault()\"\n (keydown.Home)=\"_focusFirstSibling(segment); $event.preventDefault()\"\n (keydown.End)=\"_focusLastSibling(segment); $event.preventDefault()\"\n >\n <div\n class=\"partition-map-segment-content\"\n [class.partition-map-segment-content-hidden]=\"_getSegmentContentHidden(segment)\"\n >\n <!-- Show default template if provided -->\n @if (!segmentTemplate) {\n <span class=\"partition-map-segment-label\">\n {{ segment.data.name }}\n </span>\n }\n <!-- Show custom template if provided -->\n @if (segmentTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"segmentTemplate\"\n [ngTemplateOutletContext]=\"_getContext(segment)\"\n >\n </ng-container>\n }\n </div>\n </div>\n}\n" }]
}], propDecorators: { colors: [{
type: Input
}], collapsedHeight: [{
type: Input
}], minSegmentWidth: [{
type: Input
}], dataset: [{
type: Input
}], selected: [{
type: Input
}], segmentAnnouncement: [{
type: Input
}], selectedChange: [{
type: Output
}], segmentTemplate: [{
type: ContentChild,
args: ['partitionMapSegment', { static: false }]
}] } });
class PartitionMapModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapModule, declarations: [PartitionMapComponent, PartitionMapSegmentEventsDirective], imports: [A11yModule, AccessibilityModule, CommonModule, ColorServiceModule, ResizeModule], exports: [PartitionMapComponent, PartitionMapSegmentEventsDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapModule, imports: [A11yModule, AccessibilityModule, CommonModule, ColorServiceModule, ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PartitionMapModule, decorators: [{
type: NgModule,
args: [{
imports: [A11yModule, AccessibilityModule, CommonModule, ColorServiceModule, ResizeModule],
declarations: [PartitionMapComponent, PartitionMapSegmentEventsDirective],
exports: [PartitionMapComponent, PartitionMapSegmentEventsDirective],
}]
}] });
class ProgressBarComponent {
constructor() {
this.value = 0;
this.min = 0;
this.max = 100;
this.indeterminate = false;
}
/** When indeteminate we should omit the valuenow label */
get valueNow() {
return this.indeterminate ? null : this.value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ProgressBarComponent, isStandalone: false, selector: "ux-progress-bar", inputs: { value: "value", min: "min", max: "max", indeterminate: "indeterminate", trackColor: "trackColor", barColor: "barColor" }, host: { attributes: { "role": "progressbar" }, properties: { "attr.aria-valuemin": "this.min", "attr.aria-valuemax": "this.max", "attr.aria-valuenow": "this.valueNow" } }, ngImport: i0, template: "@if (!indeterminate) {\n <div\n class=\"progressbar-track\"\n [style.width.%]=\"((value - min) / (max - min)) * 100\"\n [style.backgroundColor]=\"barColor\"\n >\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </div>\n}\n@if (indeterminate) {\n <div class=\"progressbar-track indeterminate\" [style.backgroundColor]=\"barColor\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </div>\n}\n\n<!-- Workaround for Multiple ng-content tags issue: https://github.com/angular/angular/issues/22972 -->\n<ng-template #content><ng-content></ng-content></ng-template>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-progress-bar', changeDetection: ChangeDetectionStrategy.OnPush, host: {
role: 'progressbar',
}, standalone: false, template: "@if (!indeterminate) {\n <div\n class=\"progressbar-track\"\n [style.width.%]=\"((value - min) / (max - min)) * 100\"\n [style.backgroundColor]=\"barColor\"\n >\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </div>\n}\n@if (indeterminate) {\n <div class=\"progressbar-track indeterminate\" [style.backgroundColor]=\"barColor\">\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\n </div>\n}\n\n<!-- Workaround for Multiple ng-content tags issue: https://github.com/angular/angular/issues/22972 -->\n<ng-template #content><ng-content></ng-content></ng-template>\n" }]
}], propDecorators: { value: [{
type: Input
}], min: [{
type: Input
}, {
type: HostBinding,
args: ['attr.aria-valuemin']
}], max: [{
type: Input
}, {
type: HostBinding,
args: ['attr.aria-valuemax']
}], indeterminate: [{
type: Input
}], trackColor: [{
type: Input
}], barColor: [{
type: Input
}], valueNow: [{
type: HostBinding,
args: ['attr.aria-valuenow']
}] } });
class ProgressBarModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarModule, declarations: [ProgressBarComponent], imports: [CommonModule], exports: [ProgressBarComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ProgressBarModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule],
exports: [ProgressBarComponent],
declarations: [ProgressBarComponent],
}]
}] });
const RADIOBUTTON_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RadioButtonComponent),
multi: true,
};
let uniqueRadioId = 0;
class RadioButtonComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
this._group = inject(RadioButtonGroupDirective, { optional: true });
/** Provide a default unique id value for the radiobutton */
this._radioButtonId = `ux-radio-button-${++uniqueRadioId}`;
/** Specify a unique Id for this component */
this.id = this._radioButtonId;
/** If set to `true` the radio button will not change state when clicked. */
this.clickable = true;
/** If this value is set to `true` then the radio button will be disabled */
this.disabled = false;
/** If set to `true` the checkbox will be displayed without a border and background. */
this.simplified = false;
/** Specify an aria label for the input element */
this.ariaLabel = '';
/** Specify an aria labelledby property for the input element */
this.ariaLabelledby = null;
/** Specify an aria describedby property for the input element */
this.ariaDescribedby = null;
/** Emits when the value has been changed. */
this.valueChange = new EventEmitter();
/** Determine if the underlying input component has been focused with the keyboard */
this._focused = false;
/** Internally store the current tabindex */
this._internalTabindex = null;
/** Used to inform Angular forms that the component has been touched */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
/** Used to inform Angular forms that the component value has changed */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
}
ngOnChanges(changes) {
if (changes.disabled && this._group && !changes.disabled.firstChange) {
this._group.determineAndSetInternalTabIndex();
}
}
/** Select the current option */
select() {
if (this.disabled || !this.clickable) {
return;
}
// toggle the checked state
this.value = this.option;
// if there is a group set the selected value
if (this._group) {
this._group.value = this.option;
this._group.emitChange(this.option);
}
// emit the value
this.valueChange.emit(this.value);
// update the value if used within a form control
this.onChangeCallback(this.value);
// mark the component as touched
this.onTouchedCallback();
}
// Functions required to update ng-model
writeValue(value) {
if (value !== this.value) {
this.value = value;
this._changeDetector.detectChanges();
}
}
/** Allow Angular forms for provide us with a callback for when the input value changes */
registerOnChange(fn) {
this.onChangeCallback = fn;
}
/** Allow Angular forms for provide us with a callback for when the touched state changes */
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
/** Allow Angular forms to disable the component */
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
/** Set the internal tab index of the radio button */
setInternalTabindex(tabIndex) {
this._internalTabindex = tabIndex;
this._changeDetector.detectChanges();
}
/** Focus the input element */
focus(origin) {
this._focusIndicator.focus(origin);
}
setInputTabIndex(tabindex) {
this.tabindex = tabindex;
this._changeDetector.detectChanges();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: RadioButtonComponent, isStandalone: false, selector: "ux-radio-button", inputs: { id: "id", name: "name", inputRole: "inputRole", value: "value", required: "required", tabindex: "tabindex", clickable: "clickable", disabled: "disabled", simplified: "simplified", option: "option", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], ariaDescribedby: ["aria-describedby", "ariaDescribedby"] }, outputs: { valueChange: "valueChange" }, providers: [
RADIOBUTTON_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: RadioButtonComponent,
},
], viewQueries: [{ propertyName: "_focusIndicator", first: true, predicate: FocusIndicatorDirective, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<label\n [attr.for]=\"(id || _radioButtonId) + '-input'\"\n class=\"ux-radio-button\"\n [class.ux-radio-button-checked]=\"value === option\"\n [class.ux-radio-button-simplified]=\"simplified\"\n [class.ux-radio-button-disabled]=\"disabled\"\n [class.ux-radio-button-focused]=\"_focused\"\n>\n <div class=\"ux-radio-button-container\">\n <input\n #input\n class=\"ux-radio-button-input\"\n uxFocusIndicator\n type=\"radio\"\n [id]=\"(id || _radioButtonId) + '-input'\"\n [attr.role]=\"inputRole\"\n [checked]=\"value === option\"\n [disabled]=\"disabled\"\n [tabindex]=\"tabindex ?? _internalTabindex\"\n [attr.name]=\"name\"\n [required]=\"required\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-describedby]=\"ariaDescribedby\"\n [attr.aria-checked]=\"value === option\"\n (indicator)=\"_focused = $event\"\n (change)=\"select()\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n\n <span class=\"ux-radio-button-label\">\n <ng-content></ng-content>\n </span>\n</label>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-radio-button', providers: [
RADIOBUTTON_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: RadioButtonComponent,
},
], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<label\n [attr.for]=\"(id || _radioButtonId) + '-input'\"\n class=\"ux-radio-button\"\n [class.ux-radio-button-checked]=\"value === option\"\n [class.ux-radio-button-simplified]=\"simplified\"\n [class.ux-radio-button-disabled]=\"disabled\"\n [class.ux-radio-button-focused]=\"_focused\"\n>\n <div class=\"ux-radio-button-container\">\n <input\n #input\n class=\"ux-radio-button-input\"\n uxFocusIndicator\n type=\"radio\"\n [id]=\"(id || _radioButtonId) + '-input'\"\n [attr.role]=\"inputRole\"\n [checked]=\"value === option\"\n [disabled]=\"disabled\"\n [tabindex]=\"tabindex ?? _internalTabindex\"\n [attr.name]=\"name\"\n [required]=\"required\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-describedby]=\"ariaDescribedby\"\n [attr.aria-checked]=\"value === option\"\n (indicator)=\"_focused = $event\"\n (change)=\"select()\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n\n <span class=\"ux-radio-button-label\">\n <ng-content></ng-content>\n </span>\n</label>\n" }]
}], propDecorators: { id: [{
type: Input
}], name: [{
type: Input
}], inputRole: [{
type: Input
}], value: [{
type: Input
}], required: [{
type: Input
}], tabindex: [{
type: Input
}], clickable: [{
type: Input
}], disabled: [{
type: Input
}], simplified: [{
type: Input
}], option: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], ariaDescribedby: [{
type: Input,
args: ['aria-describedby']
}], valueChange: [{
type: Output
}], _focusIndicator: [{
type: ViewChild,
args: [FocusIndicatorDirective]
}] } });
const RADIO_GROUP_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RadioButtonGroupDirective),
multi: true,
};
class RadioButtonGroupDirective {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Emit when the currently selected value changes */
this.valueChange = new EventEmitter();
/** Used to inform Angular forms that the component has been touched */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouched = () => { };
/** Used to inform Angular forms that the component value has changed */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChange = () => { };
/** Unsubscribe from all subscriptions on destroy */
this._onDestroy$ = new Subject();
/** Internally store the current value */
this._value = null;
}
/** Define the current selected value within the group */
set value(value) {
this._value = value;
this.updateSelectedRadioButton();
}
/** Return the currently selected value */
get value() {
return this._value;
}
ngAfterContentInit() {
this.updateSelectedRadioButton();
// update the selected items any time new ones are added
this._radioButtons.changes
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => this.updateSelectedRadioButton());
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
}
/** Allow Angular forms for provide us with a callback for when the input value changes */
registerOnChange(fn) {
this.onChange = fn;
}
/** Allow Angular forms for provide us with a callback for when the touched state changes */
registerOnTouched(fn) {
this.onTouched = fn;
}
/** Allow Angular forms to give us the current value */
writeValue(value) {
this.value = value;
this._changeDetector.markForCheck();
}
/** Allow Angular forms to disable the component */
setDisabledState(isDisabled) {
if (this._radioButtons) {
this._radioButtons.forEach(radio => radio.setDisabledState(isDisabled));
this._changeDetector.markForCheck();
}
}
/** Emit the currently selected value */
emitChange(value) {
this.valueChange.next(value);
this.onChange(value);
this.onTouched();
}
/** Determine and set the correct internal tabindex */
determineAndSetInternalTabIndex() {
const firstEnabled = this._radioButtons.find(radio => {
return radio.disabled === false;
});
this._radioButtons.forEach(radio => {
if (this._value !== undefined) {
radio.setInternalTabindex(radio.option === this._value ? 0 : -1);
}
else {
radio.setInternalTabindex(firstEnabled === radio ? 0 : -1);
}
});
}
/** Inform all child radio buttons of the latest value */
updateSelectedRadioButton() {
// update the selected value in all radio buttons
if (this._radioButtons) {
this.determineAndSetInternalTabIndex();
this._radioButtons.forEach(radio => radio.writeValue(this._value));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: RadioButtonGroupDirective, isStandalone: false, selector: "ux-radio-button-group, [uxRadioButtonGroup]", inputs: { value: "value" }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "radiogroup" } }, providers: [RADIO_GROUP_CONTROL_VALUE_ACCESSOR], queries: [{ propertyName: "_radioButtons", predicate: i0.forwardRef(() => RadioButtonComponent), descendants: true }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonGroupDirective, decorators: [{
type: Directive,
args: [{
selector: 'ux-radio-button-group, [uxRadioButtonGroup]',
providers: [RADIO_GROUP_CONTROL_VALUE_ACCESSOR],
host: {
role: 'radiogroup',
},
standalone: false,
}]
}], propDecorators: { value: [{
type: Input
}], valueChange: [{
type: Output
}], _radioButtons: [{
type: ContentChildren,
args: [forwardRef(() => RadioButtonComponent), { descendants: true }]
}] } });
class RadioButtonModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonModule, declarations: [RadioButtonComponent, RadioButtonGroupDirective], imports: [AccessibilityModule, FormsModule], exports: [RadioButtonComponent, RadioButtonGroupDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonModule, imports: [AccessibilityModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RadioButtonModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, FormsModule],
exports: [RadioButtonComponent, RadioButtonGroupDirective],
declarations: [RadioButtonComponent, RadioButtonGroupDirective],
}]
}] });
class SankeyChart {
constructor() {
/** Define the nodes in the chart */
this._nodes = [];
/** Define the links in the chart */
this._links = [];
/** Store the node-links */
this._nodeLinks = [];
/** Define the minimum width of the nodes */
this._minWidth = 0;
/** Define the maximum width of the nodes */
this._maxWidth = Infinity;
/** Define the minimum distance from the edge of the chart */
this._padding = 24;
}
/** Define the spacing of the chart */
spacing(spacing) {
this._spacing = spacing;
return this;
}
/** Define the width of the chart */
width(width) {
this._width = width;
return this;
}
/** Define the height of the chart */
height(height) {
this._height = height;
return this;
}
/** Define the nodes */
nodes(nodes) {
this._nodes = nodes;
return this;
}
/** Define the links */
links(links) {
this._links = links;
return this;
}
/** Define the minimum and maximum size of the nodes */
size(minWidth, maxWidth, minHeight) {
this._minWidth = minWidth;
this._maxWidth = maxWidth;
this._minHeight = minHeight;
return this;
}
/** Get the sizes of each column */
columns() {
// get the number of columns - we use this a lot so avoid multiple function calls
const columnCount = this.getColumnCount();
// get the amount of padding there should be on each side of a node
const padding = this.getColumnPadding();
const columnWidths = [];
for (let idx = 0; idx < columnCount; idx++) {
columnWidths[idx] = this.getNodeWidth() + padding * 2;
// do no have the default padding on the left of the start node
// or right of the last node, instead have a default padding
if (idx === 0 || idx === columnCount - 1) {
columnWidths[idx] -= padding - this._padding;
}
}
return columnWidths;
}
/**
* Perform the various stages of the layout
* in the correct order as some steps are dependant
* on the previous layout stages.
*/
layout() {
this.getNodeLinks();
this.getNodeValues();
this.getNodeColumns();
this.getNodeWidths();
this.getNodeHeights();
this.getNodePositions();
this.getLinkPlots();
return this._nodeLinks;
}
/** The curve equation for links */
link(link) {
// const dist = chart.blockSpacing / 2;
const { topLeft, topRight, bottomLeft, bottomRight } = link;
const dist = (topRight[0] - topLeft[0]) / 2;
const topLeftCurve = [topLeft[0] + dist, topLeft[1]];
const topRightCurve = [topRight[0] - dist, topRight[1]];
const bottomLeftCurve = [bottomLeft[0] + dist, bottomLeft[1]];
const bottomRightCurve = [bottomRight[0] - dist, bottomRight[1]];
return ('M' +
topLeft[0] +
',' +
topLeft[1] +
'C' +
topLeftCurve[0] +
',' +
topLeftCurve[1] +
' ' +
topRightCurve[0] +
',' +
topRightCurve[1] +
' ' +
topRight[0] +
',' +
topRight[1] +
'L' +
bottomRight[0] +
',' +
bottomRight[1] +
'C' +
bottomRightCurve[0] +
',' +
bottomRightCurve[1] +
' ' +
bottomLeftCurve[0] +
',' +
bottomLeftCurve[1] +
' ' +
bottomLeft[0] +
',' +
bottomLeft[1] +
'L' +
topLeft[0] +
',' +
topLeft[1]);
}
getFalloffPath(nodeLink) {
const x = nodeLink.x + nodeLink.width;
const y = nodeLink.outputs.reduce((bottom, output) => Math.max(bottom, output.bottomLeft[1]), 0);
const width = 20;
const radius = 6;
const height = nodeLink.y + nodeLink.height - y + this._spacing / 2;
return ('M' +
x +
',' +
y +
'h ' +
(width - radius) +
'a' +
radius +
',' +
radius +
' 0 0,1' +
radius +
',' +
radius +
' ' +
'v' +
Math.max(radius, height) +
'h-' +
width +
'Z');
}
/**
* Get a `SankeyNodeLink` object from the id of a node
*/
getNodeLink(id) {
return this._nodeLinks.find(nodeLink => nodeLink.node.id === id);
}
/** Replace the node ids with actual references */
getNodeLinks() {
this._nodeLinks = this._nodes.map(node => {
// get all the links that input into and output from this node
const inputs = this._links.filter(link => link.target === node.id);
const outputs = this._links.filter(link => link.source === node.id);
return {
node,
inputs,
outputs,
value: 0,
column: 0,
x: 0,
y: 0,
width: 0,
height: 0,
naturalHeight: 0,
falloff: 0,
active: false,
focus: false,
};
});
}
/** Get the value for the node based on all its inputs and outputs */
getNodeValues() {
for (const node of this._nodeLinks) {
// the node value can be determined by the total values from all inputs
// however the first column of nodes have no inputs so must be based of their outputs.
// We should take the maximum value based on the inputs and outputs as nodes that are
// not in the first column may not output all of the amount the receive from inputs,
// for example in the case of falloff etc..
node.value = Math.max(sum(node.inputs, input => input.value), sum(node.outputs, output => output.value));
}
}
/**
* We need to determine which column the node should
* be placed in. This is determined by taking the input
* and adding one.
*/
getNodeColumns(nodeLinks = this._nodeLinks.filter(node => node.inputs.length === 0), column = 0) {
for (const nodeLink of nodeLinks) {
nodeLink.column = column;
// call this function to all output links
this.getNodeColumns(nodeLink.outputs.map(output => this.getNodeLink(output.target)), column + 1);
}
}
/** Get the width of each node */
getNodeWidths() {
this._nodeLinks.forEach(node => (node.width = this.getNodeWidth()));
}
/**
* Scale the nodes height based on the value the represent
*/
getNodeHeights() {
// get columns by group
const groups = this.getColumnGroups();
const groupList = Object.keys(groups).map(group => groups[group]);
// get the column with the largest total value
const total = groupList.reduce((count, nodes) => Math.max(count, nodes.reduce((accumulation, node) => accumulation + node.value, 0)), 0);
// Calculate node heights
for (const nodeLinks of groupList) {
// get the proportional size of each node based on the available space
for (const nodeLink of nodeLinks) {
nodeLink.naturalHeight = (nodeLink.value / total) * this._height - this._spacing;
nodeLink.height = Math.max(nodeLink.naturalHeight, this._minHeight);
}
}
// If minHeight is defined, it might cause some columns to exceed the height of the chart following the
// initial height calculation.
if (this._minHeight > 0) {
try {
// Recalculate node heights until they fit (if possible)
this.adjustNodeHeightsToFit(groupList);
}
catch (error) {
// If the above recalculation fails, give up and use the naturalHeight (ignore minHeight)
this.setNodesToNaturalHeight(groupList);
}
}
}
/**
* Recalculate node heights within height limits until they fit (if possible).
* @throws If it is not possible to fit all nodes in the chart due to `minHeight`.
*/
adjustNodeHeightsToFit(groupList) {
let largestColumn = this.getLargestColumn(groupList);
while (largestColumn.height > this._height) {
// Get the list of nodes whose height cannot be reduced
const fixedNodes = largestColumn.nodes.filter(nodeLink => nodeLink.height <= this._minHeight);
// Get the total height in the column which cannot shrink (including spacing)
const fixedHeight = fixedNodes.length * this._minHeight + largestColumn.nodes.length * this._spacing;
// If the unshrinkable height is greater than the available height, we can't continue
if (fixedHeight > this._height) {
throw new Error(`Cannot fit data into chart with minHeight = ${this._minHeight}px (need ${fixedHeight}px; ${this._height}px available)`);
}
// Find the amount of height which can potentially be reduced
const flexibleHeight = largestColumn.height - fixedHeight;
// Find the amount of height that the above needs to fit into
const availableHeight = this._height - fixedHeight;
// Get the multiplier to reduce the nodes in order to fit the available height
const ratio = availableHeight / flexibleHeight;
// Adjust the nodes and reapply the minHeight
for (const group of groupList) {
for (const nodeLink of group) {
if (nodeLink.height > this._minHeight) {
nodeLink.height *= ratio;
}
if (nodeLink.height < this._minHeight) {
nodeLink.height = this._minHeight;
}
}
}
largestColumn = this.getLargestColumn(groupList);
}
}
/** Set all nodes height to be the same as the naturalHeight. */
setNodesToNaturalHeight(groupList) {
for (const group of groupList) {
for (const nodeLink of group) {
nodeLink.height = nodeLink.naturalHeight;
}
}
}
/**
* Get all nodes grouped in their corresponding columns
*/
getColumnGroups() {
// group nodes by columns
return this._nodeLinks.reduce((collection, nodeLink) => {
collection[nodeLink.column] = collection[nodeLink.column] || [];
collection[nodeLink.column].push(nodeLink);
return collection;
}, {});
}
/**
* Get the number of columns
*/
getColumnCount() {
return this._nodeLinks.reduce((column, nodeLink) => Math.max(nodeLink.column + 1, column), 0);
}
/**
* Position the nodes in their corresponding x and y positions
*/
getNodePositions() {
// get all nodes by group
const groups = this.getColumnGroups();
// get the amount of padding required between each item
const padding = this.getColumnPadding();
for (const nodeLink of this._nodeLinks) {
// get the x position based on the column
nodeLink.x = this.getColumnPosition(nodeLink.column) + padding;
if (nodeLink.column === 0) {
nodeLink.x = this._padding;
}
// get the y position based on the accumulative height of the nodes above it
nodeLink.y =
groups[nodeLink.column]
.slice(0, groups[nodeLink.column].indexOf(nodeLink))
.reduce((top, _node) => top + _node.height, 0) +
this._spacing * groups[nodeLink.column].indexOf(nodeLink);
}
}
getColumnPadding() {
// get the number of columns - we use this a lot so avoid multiple function calls
const columnCount = this.getColumnCount();
// get the chart width minus the width of the nodes
const width = this._width - columnCount * this.getNodeWidth() - this._padding * 2;
// get the total amount of places requiring padding (the first and last columns only have padding on one side)
const paddingCount = Math.max(columnCount * 2 - 2, 0);
// get the actual size of the padding
return width / paddingCount;
}
getLinkPlots() {
for (const nodeLink of this._nodeLinks) {
let inputY = nodeLink.y;
// process each input link
for (const link of nodeLink.inputs) {
link.topRight = [nodeLink.x, inputY];
inputY += (link.value / nodeLink.value) * nodeLink.height;
link.bottomRight = [nodeLink.x, inputY];
}
let outputValue = 0;
let outputY = nodeLink.y;
// process each output link
for (const link of nodeLink.outputs) {
link.topLeft = [nodeLink.x + nodeLink.width, outputY];
outputY += (link.value / nodeLink.value) * nodeLink.height;
link.bottomLeft = [nodeLink.x + nodeLink.width, outputY];
outputValue += link.value;
}
// determine how much falloff there is
nodeLink.falloff = nodeLink.value - outputValue;
}
}
/** Determine the position at which a column starts */
getColumnPosition(column) {
// the position is the acculation of the widths of all previous columns
return this.columns()
.splice(0, column)
.reduce((total, width) => total + width, 0);
}
/** Get the pixel width of a node */
getNodeWidth() {
const width = (this._width - this._padding * 2) / (this.getColumnCount() * 2 - 1);
return Math.min(this._maxWidth, Math.max(this._minWidth, width));
}
/** Get the column with the greatest height (along with its height) */
getLargestColumn(groupList) {
let largestColumn = null;
let largestColumnHeight = 0;
for (const group of groupList) {
const totalHeight = group.reduce((acc, node) => (acc += node.height), 0) + group.length * this._spacing;
if (totalHeight > largestColumnHeight) {
largestColumnHeight = totalHeight;
largestColumn = group;
}
}
return {
nodes: largestColumn,
height: largestColumnHeight,
};
}
}
class SankeyFocusManager {
constructor() {
/** Store the node that can currently be tabbed to */
this.active$ = new BehaviorSubject(null);
/** Emit whenever an item should receive focus */
this.focused$ = new Subject();
/** Store the nodes */
this._nodes = [];
}
/** Get the current active item */
get _active() {
return this.active$.value;
}
ngOnDestroy() {
this.active$.complete();
this.focused$.complete();
}
/** Update the list of possible nodes */
setNodes(nodes) {
this._nodes = nodes;
// check if there is currently a tabbable node, if not we should make the first node tabbable
if (!this.hasActiveNode()) {
this.setActiveItem(this._nodes[0]);
}
}
/** Set the current active item */
setActiveItem(node) {
this.active$.next(node);
}
/** Handle keyboard input from nodes */
onKeydown(event) {
switch (event.which) {
case UP_ARROW:
this.shiftFocusVertically(-1);
event.preventDefault();
break;
case DOWN_ARROW:
this.shiftFocusVertically(1);
event.preventDefault();
break;
case LEFT_ARROW:
this.shiftFocusHorizontally(-1);
event.preventDefault();
break;
case RIGHT_ARROW:
this.shiftFocusHorizontally(1);
event.preventDefault();
break;
}
}
setFocusedItem(item) {
this.setActiveItem(item);
this.focused$.next(item);
}
shiftFocusVertically(delta) {
const nodes = this.getNodesInColumn(this._active.column);
// get the node below or above the active node
const target = nodes[nodes.findIndex(node => node.node.id === this._active.node.id) + delta];
if (target) {
this.setFocusedItem(target);
}
}
/** Shift the focus to a node in a sibling column */
shiftFocusHorizontally(delta) {
// get nodes in the sibling column in the desired direction
const nodes = this.getNodesInColumn(this._active.column + delta);
// if there are no nodes then do nothing as we cannot reduce an empty array
if (nodes.length === 0) {
return;
}
// get the node with the most similar y position
const target = nodes.reduce((closest, node) => {
const closestDiff = Math.max(closest.y, this._active.y) - Math.min(closest.y, this._active.y);
const currentDiff = Math.max(node.y, this._active.y) - Math.min(node.y, this._active.y);
return closestDiff < currentDiff ? closest : node;
});
if (target) {
this.setFocusedItem(target);
}
}
/** Get a list of nodes that are in a given column */
getNodesInColumn(column) {
return this.getNodesInOrder(this._nodes.filter(node => node.column === column));
}
/** Sort the nodes based on the Y position */
getNodesInOrder(nodes) {
return [...nodes].sort((nodeOne, nodeTwo) => nodeOne.y - nodeTwo.y);
}
/** Determine whether or not there is a not that is tabbable */
hasActiveNode() {
return (!!this.active$.value &&
!!this._nodes.find(node => node.node.id === this.active$.value.node.id));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyFocusManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyFocusManager }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyFocusManager, decorators: [{
type: Injectable
}] });
class SankeyNodeDirective {
constructor() {
this._focusManager = inject(SankeyFocusManager);
this._elementRef = inject(ElementRef);
/** Specify the tab index of the current item */
this.tabIndex = -1;
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
}
ngOnInit() {
// Update the tabindex based on the current active item
this._focusManager.active$
.pipe(map(item => item && item.node.id === this.node.node.id), takeUntil(this._onDestroy))
.subscribe(isActive => (this.tabIndex = isActive ? 0 : -1));
// If this element should be focused perform the focus
this._focusManager.focused$
.pipe(filter(node => node.node.id === this.node.node.id), takeUntil(this._onDestroy))
.subscribe(() => this._elementRef.nativeElement.focus());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
onClick() {
this._focusManager.setActiveItem(this.node);
}
onKeydown(event) {
this._focusManager.onKeydown(event);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyNodeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SankeyNodeDirective, isStandalone: false, selector: "[uxSankeyNode]", inputs: { node: ["uxSankeyNode", "node"] }, host: { listeners: { "click": "onClick()", "keydown": "onKeydown($event)" }, properties: { "tabIndex": "this.tabIndex" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyNodeDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxSankeyNode]',
standalone: false,
}]
}], propDecorators: { node: [{
type: Input,
args: ['uxSankeyNode']
}], tabIndex: [{
type: HostBinding
}], onClick: [{
type: HostListener,
args: ['click']
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class SankeyChartComponent {
constructor() {
this._focusManager = inject(SankeyFocusManager);
this._changeDetector = inject(ChangeDetectorRef);
this._colorService = inject(ColorService);
/** Define the nodes to display */
this.nodes = [];
/** Define the links to display */
this.links = [];
/** Define the headers of each column */
this.columns = [];
/** Define the minimum width of a node */
this.minWidth = 0;
/** Define the maximum width of a node */
this.maxWidth = Infinity;
/** The minimum height of a node. */
this.minHeight = 0;
/** Define the function to get the contents of a link tooltip */
this.linkTooltip = this.getLinkTooltip;
/** Define the function to get the contents of a falloff tooltip */
this.falloffTooltip = this.getFalloffTooltip;
/** Define the nodes that should be rendered */
this._nodes = [];
/** Define the columns to display */
this._columns = [];
/** Determine if the tooltip should be visible or not */
this._isTooltipOpen = false;
/** Define the position of the tooltip */
this._tooltipPosition = { x: 0, y: 0 };
/** Determine if the component is initialised */
this._isInitialised = false;
/** Store the instance of the sankey layout */
this._sankey = new SankeyChart();
}
ngAfterViewInit() {
// verify we have a node template defined before proceeding
if (!this.nodeTemplate) {
throw new Error('Sankey Chart - Node Template has not been defined.');
}
// set the initial chart size
this._width = this.nodeContainer.nativeElement.offsetWidth;
this._height = this.nodeContainer.nativeElement.offsetHeight;
// perform the initial render
this._render();
// mark the component as initialised
this._isInitialised = true;
}
/**
* Detect any changes from Inputs. We can skip
* the first function call as this happens before
* the initial render so it has no effect.
*/
ngOnChanges() {
if (this._isInitialised) {
this._render();
}
}
/** Re-render the chart */
_render() {
this._nodes = this._sankey
.nodes(this.nodes)
.links(this.links)
.spacing(14)
.size(this.minWidth, this.maxWidth, this.minHeight)
.width(this._width || this.nodeContainer.nativeElement.offsetWidth)
.height(this._height || this.nodeContainer.nativeElement.offsetHeight)
.layout();
// ensure the focus manager has the latest node data
this._focusManager.setNodes(this._nodes);
this._columns = this.getColumns();
this._changeDetector.detectChanges();
}
/** Update the layout whenever the dimensions change changes */
_onResize(dimensions) {
this._width = dimensions.width;
this._height = dimensions.height;
this._render();
}
/**
* Column count should be based on the data, not the titles
* as they may not specify titles but the nodes will still be
* rendered.
*/
_getColumnCount() {
return this._nodes.reduce((column, node) => Math.max(column, node.column), 0);
}
/**
* Get the SVG path that defines the shape of the link
*/
_getPath(link) {
return this._sankey.link(link);
}
/**
* Set the active state of a node and the inputs and outputs
* associated with this node.
*/
_setNodeActive(nodeLink, active) {
// set the node active state
nodeLink.active = active;
// set the active state of each link
nodeLink.inputs.forEach(link => (link.active = active));
nodeLink.outputs.forEach(link => (link.active = active));
// set the active state of all input and output nodes
nodeLink.inputs
.map(link => this._sankey.getNodeLink(link.source))
.forEach(_node => (_node.active = active));
nodeLink.inputs
.map(link => this._sankey.getNodeLink(link.target))
.forEach(_node => (_node.active = active));
nodeLink.outputs
.map(link => this._sankey.getNodeLink(link.source))
.forEach(_node => (_node.active = active));
nodeLink.outputs
.map(link => this._sankey.getNodeLink(link.target))
.forEach(_node => (_node.active = active));
// ensure we update the view to show highlights
this._changeDetector.detectChanges();
}
/**
* Set the focused state of a node and the inputs and outputs
* associated with this node.
*/
_setNodeFocus(nodeLink, focused, element) {
// set the node focus state
nodeLink.focus = focused;
// set the active state of each link
nodeLink.inputs.forEach(link => (link.focus = focused));
nodeLink.outputs.forEach(link => (link.focus = focused));
// set the active state of all input and output nodes
nodeLink.inputs
.map(link => this._sankey.getNodeLink(link.source))
.forEach(_node => (_node.focus = focused));
nodeLink.inputs
.map(link => this._sankey.getNodeLink(link.target))
.forEach(_node => (_node.focus = focused));
nodeLink.outputs
.map(link => this._sankey.getNodeLink(link.source))
.forEach(_node => (_node.focus = focused));
nodeLink.outputs
.map(link => this._sankey.getNodeLink(link.target))
.forEach(_node => (_node.focus = focused));
// we need to add the focus indicator here programmatically. The default quantum-ux-aspects focus indicator
// styling uses `!important` so our inline style needs to also be `!important` to override this, and unfortunately
// there is a known issue with `NgStyle` and `[style.xyz]` bindings preventing them from adding the `!important`
// modifier so we must do it manually (not using `Renderer2`).
if (this.color) {
element.style.setProperty('box-shadow', this._getFocusIndicator(nodeLink), 'important');
}
// ensure we update the view to show highlights
this._changeDetector.detectChanges();
}
/**
* Set the active state of a link and the source and target
* nodes associated with the link
*/
_setLinkActive(link, active) {
link.active = active;
if (link.source !== undefined) {
this._sankey.getNodeLink(link.source).active = active;
}
if (link.target !== undefined) {
this._sankey.getNodeLink(link.target).active = active;
}
// update the tooltip visibility
this._isTooltipOpen = active;
// update the tooltip content
this._tooltipContent = active ? this.linkTooltip(link) : '';
// ensure we update the view to show highlights
this._changeDetector.detectChanges();
}
/**
* This is required because we want to toggle a class based on the `active`
* property on a link, however toggling classes using `NgClass` or the class
* binding syntax `[class.xyz]` does not work in IE when applied to an SVG
* element. (https://github.com/angular/angular/issues/6327)
*
* The alternatice is to bind directly to the `class` attribute and return a
* string that will toggle the class based on the `active` property.
*/
_getLinkClass(link) {
return `ux-sankey-chart-link ${link.active || link.focus ? 'ux-sankey-chart-link-active' : ''}`;
}
/**
* Get the SVG path that defines the shape of the falloff indicator
*/
_getFalloffPath(node) {
return this._sankey.getFalloffPath(node);
}
/**
* Falloff represents the amount of data that does not get passed on,
* for example, if a node gets 1,000,000 items from inputs and only outputs
* 500,000 then there is falloff of 500,000. However, items in the last column
* never pass on any information, so tecnhically 100% of their input is falloff
* so we shouldn't show it in the last column.
*/
_showFalloff(nodeLink) {
return nodeLink.column < this._columns.length - 1;
}
/** Update the visibility and content of the tooltip on falloff hover */
_setFalloffTooltip(nodeLink, isVisible) {
this._isTooltipOpen = isVisible;
this._tooltipContent = isVisible ? this.falloffTooltip(nodeLink.falloff) : '';
this._changeDetector.detectChanges();
}
/**
* Update the position of the tooltip
*/
_setTooltipPosition(event) {
const { left, top } = this.nodeContainer.nativeElement.getBoundingClientRect();
const x = event.pageX - left - (window.scrollX || document.documentElement.scrollLeft);
const y = event.pageY - top - (window.scrollY || document.documentElement.scrollTop);
this._tooltipPosition = { x, y };
this._changeDetector.detectChanges();
}
/**
* Correctly track the node changes in `*ngFor` based on
* the unique node ids to prevent unnecessary re-rendering
*/
_trackNodeBy(_index, nodeLink) {
return nodeLink.node.id;
}
/**
* Correctly track the link changes in `*ngFor` based on
* the source and target to prevent unnecessary re-rendering
*/
_trackLinkBy(_index, link) {
return `${link.source}-${link.target}`;
}
/**
* Get the color of node based on whether or not
* the `color` input has been provided.
*/
_getColor(item) {
// if we are not node hovering or focusing or no custom color is defined then return nothing
if ((!item.active && !item.focus) || !this.color) {
return;
}
// return an rgba value if it is a `ThemeColor` to support transparency
return this.color instanceof ThemeColor
? this.color.toRgba()
: this._colorService.resolve(this.color);
}
/**
* We want the focus indicator color to match the active color,
* which if programmatically defined need to be overriden
*/
_getFocusIndicator(nodeLink) {
// if the node is not focused or there is no custom color
// then return null in which case CSS indicator will show
if (!nodeLink.focus || !this.color) {
return '';
}
// otherwise return the shadow based on the color provided.
const color = this.color instanceof ThemeColor
? this.color
: ThemeColor.parse(this._colorService.resolve(this.color));
// generate a box shadow based on the specified color
return `0 0 0 1px #fff, 0 0 0 3px ${color.setAlpha(0.5).toRgba()}`;
}
/**
* Get columns mapped with their title if they have any
*/
getColumns() {
return this._sankey.columns().map((width, index) => ({
width,
title: this.columns[index] || '',
position: this.getColumnPosition(index),
}));
}
/**
* Get the start position of a column which can be determined
* by finding a node that is in that column and using its
* x position as all nodes start at the same position within a column.
*/
getColumnPosition(column) {
// find a node in this column and take its x position
const node = this._nodes.find(_node => _node.column === column);
return node ? node.x : 0;
}
/**
* Get the default content of a link tooltip
*/
getLinkTooltip(link) {
return link.value.toLocaleString('en') + ' items';
}
/**
* Get the default content of a falloff tooltip
*/
getFalloffTooltip(falloff) {
return falloff.toLocaleString('en') + ' items';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SankeyChartComponent, isStandalone: false, selector: "ux-sankey-chart", inputs: { nodes: "nodes", links: "links", columns: "columns", minWidth: "minWidth", maxWidth: "maxWidth", minHeight: "minHeight", linkTooltip: "linkTooltip", falloffTooltip: "falloffTooltip", color: "color" }, queries: [{ propertyName: "nodeTemplate", first: true, predicate: ["sankeyNodeTemplate"], descendants: true }], viewQueries: [{ propertyName: "linkContainer", first: true, predicate: ["linkContainer"], descendants: true, static: true }, { propertyName: "nodeContainer", first: true, predicate: ["nodeContainer"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "@if (columns && columns.length > 0) {\n <div class=\"ux-sankey-chart-columns\">\n @for (column of _columns; track column.title) {\n <div class=\"ux-sankey-chart-column\" [style.width.px]=\"column.width\">\n <p class=\"ux-sankey-chart-column-title\" [style.left.px]=\"column.position\">\n {{ column.title }}\n </p>\n </div>\n }\n </div>\n}\n\n<svg\n #linkContainer\n [attr.width]=\"_width\"\n [attr.height]=\"_height\"\n class=\"ux-sankey-chart-links\"\n [style.top.px]=\"!columns || columns.length === 0 ? 8 : null\"\n>\n <defs>\n <linearGradient id=\"falloff-gradient\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\">\n <stop class=\"ux-sankey-chart-falloff-gradient-start\" offset=\"25%\"></stop>\n <stop class=\"ux-sankey-chart-falloff-gradient-end\" offset=\"100%\"></stop>\n </linearGradient>\n </defs>\n <g>\n @for (node of _nodes; track _trackNodeBy($index, node)) {\n <ng-container>\n @for (link of node.outputs; track _trackLinkBy($index, link)) {\n <path\n [attr.class]=\"_getLinkClass(link)\"\n [attr.d]=\"_getPath(link)\"\n [style.fill]=\"_getColor(link)\"\n (mouseenter)=\"_setLinkActive(link, true)\"\n (mouseleave)=\"_setLinkActive(link, false)\"\n (mousemove)=\"_setTooltipPosition($event)\"\n ></path>\n }\n @if (node.falloff && _showFalloff(node)) {\n <path\n class=\"ux-sankey-chart-falloff-indicator\"\n [attr.d]=\"_getFalloffPath(node)\"\n (mouseenter)=\"_setFalloffTooltip(node, true)\"\n (mouseleave)=\"_setFalloffTooltip(node, false)\"\n (mousemove)=\"_setTooltipPosition($event)\"\n ></path>\n }\n </ng-container>\n }\n </g>\n</svg>\n\n<div\n #nodeContainer\n class=\"ux-sankey-chart-nodes\"\n (uxResize)=\"_onResize($event)\"\n [style.top.px]=\"!columns || columns.length === 0 ? 8 : null\"\n>\n @for (node of _nodes; track _trackNodeBy($index, node)) {\n <div\n #nodeElement\n [uxSankeyNode]=\"node\"\n uxFocusIndicator\n class=\"ux-sankey-chart-node\"\n [class.ux-sankey-chart-node-active]=\"node.active || node.focus\"\n [style.left.px]=\"node.x\"\n [style.top.px]=\"node.y\"\n [style.width.px]=\"node.width\"\n [style.height.px]=\"node.height\"\n [style.background-color]=\"_getColor(node)\"\n (mouseenter)=\"_setNodeActive(node, true)\"\n (mouseleave)=\"_setNodeActive(node, false)\"\n (indicator)=\"_setNodeFocus(node, $event, nodeElement)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ node: node.node, active: node.active, focus: node.focus }\"\n >\n </ng-container>\n </div>\n }\n\n @if (_isTooltipOpen) {\n <ux-tooltip\n class=\"ux-sankey-tooltip\"\n placement=\"top\"\n [content]=\"_tooltipContent\"\n alignment=\"center\"\n [style.left.px]=\"_tooltipPosition.x\"\n [style.top.px]=\"_tooltipPosition.y\"\n [@tooltipAnimation]\n >\n </ux-tooltip>\n }\n</div>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ResizeDirective, selector: "[uxResize]", inputs: ["throttle"], outputs: ["uxResize"] }, { kind: "component", type: TooltipComponent, selector: "ux-tooltip", inputs: ["content", "context", "placement", "alignment"] }, { kind: "directive", type: SankeyNodeDirective, selector: "[uxSankeyNode]", inputs: ["uxSankeyNode"] }], viewProviders: [SankeyFocusManager], animations: [
trigger('tooltipAnimation', [
transition(':enter', [style({ opacity: 0 }), animate(160, style({ opacity: 1 }))]),
transition(':leave', [animate(160, style({ opacity: 0 }))]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-sankey-chart', changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [SankeyFocusManager], animations: [
trigger('tooltipAnimation', [
transition(':enter', [style({ opacity: 0 }), animate(160, style({ opacity: 1 }))]),
transition(':leave', [animate(160, style({ opacity: 0 }))]),
]),
], standalone: false, template: "@if (columns && columns.length > 0) {\n <div class=\"ux-sankey-chart-columns\">\n @for (column of _columns; track column.title) {\n <div class=\"ux-sankey-chart-column\" [style.width.px]=\"column.width\">\n <p class=\"ux-sankey-chart-column-title\" [style.left.px]=\"column.position\">\n {{ column.title }}\n </p>\n </div>\n }\n </div>\n}\n\n<svg\n #linkContainer\n [attr.width]=\"_width\"\n [attr.height]=\"_height\"\n class=\"ux-sankey-chart-links\"\n [style.top.px]=\"!columns || columns.length === 0 ? 8 : null\"\n>\n <defs>\n <linearGradient id=\"falloff-gradient\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\">\n <stop class=\"ux-sankey-chart-falloff-gradient-start\" offset=\"25%\"></stop>\n <stop class=\"ux-sankey-chart-falloff-gradient-end\" offset=\"100%\"></stop>\n </linearGradient>\n </defs>\n <g>\n @for (node of _nodes; track _trackNodeBy($index, node)) {\n <ng-container>\n @for (link of node.outputs; track _trackLinkBy($index, link)) {\n <path\n [attr.class]=\"_getLinkClass(link)\"\n [attr.d]=\"_getPath(link)\"\n [style.fill]=\"_getColor(link)\"\n (mouseenter)=\"_setLinkActive(link, true)\"\n (mouseleave)=\"_setLinkActive(link, false)\"\n (mousemove)=\"_setTooltipPosition($event)\"\n ></path>\n }\n @if (node.falloff && _showFalloff(node)) {\n <path\n class=\"ux-sankey-chart-falloff-indicator\"\n [attr.d]=\"_getFalloffPath(node)\"\n (mouseenter)=\"_setFalloffTooltip(node, true)\"\n (mouseleave)=\"_setFalloffTooltip(node, false)\"\n (mousemove)=\"_setTooltipPosition($event)\"\n ></path>\n }\n </ng-container>\n }\n </g>\n</svg>\n\n<div\n #nodeContainer\n class=\"ux-sankey-chart-nodes\"\n (uxResize)=\"_onResize($event)\"\n [style.top.px]=\"!columns || columns.length === 0 ? 8 : null\"\n>\n @for (node of _nodes; track _trackNodeBy($index, node)) {\n <div\n #nodeElement\n [uxSankeyNode]=\"node\"\n uxFocusIndicator\n class=\"ux-sankey-chart-node\"\n [class.ux-sankey-chart-node-active]=\"node.active || node.focus\"\n [style.left.px]=\"node.x\"\n [style.top.px]=\"node.y\"\n [style.width.px]=\"node.width\"\n [style.height.px]=\"node.height\"\n [style.background-color]=\"_getColor(node)\"\n (mouseenter)=\"_setNodeActive(node, true)\"\n (mouseleave)=\"_setNodeActive(node, false)\"\n (indicator)=\"_setNodeFocus(node, $event, nodeElement)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ node: node.node, active: node.active, focus: node.focus }\"\n >\n </ng-container>\n </div>\n }\n\n @if (_isTooltipOpen) {\n <ux-tooltip\n class=\"ux-sankey-tooltip\"\n placement=\"top\"\n [content]=\"_tooltipContent\"\n alignment=\"center\"\n [style.left.px]=\"_tooltipPosition.x\"\n [style.top.px]=\"_tooltipPosition.y\"\n [@tooltipAnimation]\n >\n </ux-tooltip>\n }\n</div>\n" }]
}], propDecorators: { nodes: [{
type: Input
}], links: [{
type: Input
}], columns: [{
type: Input
}], minWidth: [{
type: Input
}], maxWidth: [{
type: Input
}], minHeight: [{
type: Input
}], linkTooltip: [{
type: Input
}], falloffTooltip: [{
type: Input
}], color: [{
type: Input
}], nodeTemplate: [{
type: ContentChild,
args: ['sankeyNodeTemplate', { static: false }]
}], linkContainer: [{
type: ViewChild,
args: ['linkContainer', { static: true }]
}], nodeContainer: [{
type: ViewChild,
args: ['nodeContainer', { static: true }]
}] } });
class SankeyChartModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartModule, declarations: [SankeyChartComponent, SankeyNodeDirective], imports: [AccessibilityModule, CommonModule, ResizeModule, TooltipModule, ColorServiceModule], exports: [SankeyChartComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartModule, imports: [AccessibilityModule, CommonModule, ResizeModule, TooltipModule, ColorServiceModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SankeyChartModule, decorators: [{
type: NgModule,
args: [{
declarations: [SankeyChartComponent, SankeyNodeDirective],
imports: [AccessibilityModule, CommonModule, ResizeModule, TooltipModule, ColorServiceModule],
exports: [SankeyChartComponent],
}]
}] });
const UNSET_FOCUS = { groupId: null, index: -1 };
class SearchBuilderFocusService {
constructor() {
this.focus$ = new BehaviorSubject(UNSET_FOCUS);
}
/**
* Set focus on a search builder component.
* @param groupId The `id` of the group containing the component.
* @param index The (zero-based) index of the component.
*/
setFocus(groupId, index) {
this.focus$.next({ groupId, index });
}
/**
* Removes focus from all components. If focus is not on a search builder component, this does nothing.
*/
clearFocus() {
this.focus$.next(UNSET_FOCUS);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderFocusService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderFocusService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class SearchBuilderService {
constructor() {
this.query = {};
this.queryChange = new Subject();
this.validationChange = new BehaviorSubject(true);
this._componentId = 0;
this._components = [];
this._validation = {};
}
/**
* Add a component to the internal list of components
*/
registerComponent(component) {
// ensure there are no components with a matching name
if (this._components.find(cmp => cmp.name === component.name)) {
throw new Error(`Search builder components must have a unique name. The name ${component.name} has already been used.`);
}
// if unique then add the component to the list
this._components.push(component);
}
/**
* Bulk registration of components
* (Just a helper method)
*/
registerComponents(components) {
components.forEach(component => this.registerComponent(component));
}
/**
* Get a registered component class
*/
getComponent(name) {
// find the component
const component = this._components.find(cmp => cmp.name === name);
// if there is no match throw an exception
if (!component) {
throw new Error(`No search build component with the name ${name} exists`);
}
// ensure config is defined - at least to an empty object
component.config = component.config || {};
return component;
}
/**
* Update the internal search query state
* note that the query will be immutable
*/
setQuery(query) {
this.query = Object.assign({}, query);
}
/**
* Return the current query state
*/
getQuery() {
return this.query;
}
/**
* Trigger the observable to indicate the query has been updated
*/
queryHasChanged() {
this.queryChange.next(this.query);
}
/**
* Store the validation state of the query
*/
setValid(id, valid) {
// store the state for this specific component
this._validation[id] = valid;
// evaluate the entire validation state
this.validationChange.next(!Object.keys(this._validation).some(key => !this._validation[key]));
}
/**
* Generate a unique id for each component
*/
generateComponentId() {
return this._componentId++;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderService, decorators: [{
type: Injectable
}] });
class SearchBuilderGroupService {
constructor() {
this._searchBuilderService = inject(SearchBuilderService);
this._searchBuilderFocusService = inject(SearchBuilderFocusService);
}
/**
* Initialise the group by defining an id
*/
init(id) {
// store the name of the group
this._id = id;
// create the entry in the query object if it doesn't exist
if (!this._searchBuilderService.query[this._id]) {
// create the section
this._searchBuilderService.query[this._id] = [];
// emit the changes after the initial setup
setTimeout(() => this._searchBuilderService.queryHasChanged());
}
}
/**
* Remove a field from the search builder query and return focus to the previous field.
*/
removeAtIndex(index) {
// get the query for this group
const query = this.getQuery();
// remove the field from the array
query.splice(index, 1);
// Focus the previous item if available
this._searchBuilderFocusService.setFocus(this._id, index <= 0 ? 0 : index - 1);
}
/**
* Get the query for this specific search group
*/
getQuery() {
return this._searchBuilderService.query[this._id]
? this._searchBuilderService.query[this._id]
: [];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderGroupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderGroupService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderGroupService, decorators: [{
type: Injectable
}] });
class SearchBuilderOutletDirective {
constructor() {
this._viewContainerRef = inject(ViewContainerRef);
this._componentFactoryResolver = inject(ComponentFactoryResolver);
this._searchBuilderService = inject(SearchBuilderService);
this._searchBuilderFocusService = inject(SearchBuilderFocusService);
this._onDestroy = new Subject();
}
ngOnInit() {
// get the class from the type
const componentDefinition = this._searchBuilderService.getComponent(this.outlet);
// create the component factory
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(componentDefinition.component);
// create the component instance
this._componentRef = this._viewContainerRef.createComponent(componentFactory);
// combine the predefined config with any dynmaic config
const config = Object.assign({}, componentDefinition.config, this.context.config || {});
// set the context and config property on the component instance
this._componentRef.instance.context = this.context;
this._componentRef.instance.config = config;
this._searchBuilderFocusService.focus$
.pipe(distinctUntilChanged(), delay(0), takeUntil(this._onDestroy))
.subscribe(focus => {
this._componentRef.instance.focus =
focus.groupId === this.groupId && focus.index === this.index;
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SearchBuilderOutletDirective, isStandalone: false, selector: "[uxSearchBuilderOutlet]", inputs: { outlet: ["uxSearchBuilderOutlet", "outlet"], context: ["uxSearchBuilderOutletContext", "context"], groupId: ["uxSearchBuilderOutletGroupId", "groupId"], index: ["uxSearchBuilderOutletIndex", "index"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderOutletDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxSearchBuilderOutlet]',
standalone: false,
}]
}], propDecorators: { outlet: [{
type: Input,
args: ['uxSearchBuilderOutlet']
}], context: [{
type: Input,
args: ['uxSearchBuilderOutletContext']
}], groupId: [{
type: Input,
args: ['uxSearchBuilderOutletGroupId']
}], index: [{
type: Input,
args: ['uxSearchBuilderOutletIndex']
}] } });
class SearchBuilderGroupComponent {
constructor() {
this.searchBuilderGroupService = inject(SearchBuilderGroupService);
this._searchBuilderFocusService = inject(SearchBuilderFocusService);
this.operator = 'and';
this.addText = 'Add a field';
this.showPlaceholder = false;
this.removeFieldButtonAriaLabel = 'Remove field';
this.add = new EventEmitter();
this.remove = new EventEmitter();
this.focusIndex = -1;
this._onDestroy = new Subject();
}
ngOnInit() {
// ensure we have a name otherwise throw an error
if (!this.id) {
throw new Error('Search builder group must have an id attribute.');
}
// otherwise register the group
this.searchBuilderGroupService.init(this.id);
// Track focus for child components
this._searchBuilderFocusService.focus$.pipe(takeUntil(this._onDestroy)).subscribe(focus => {
this.focusIndex = focus.groupId === this.id ? focus.index : -1;
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
addField(event) {
this.add.emit(event);
}
removeFieldAtIndex(index, field) {
this.searchBuilderGroupService.removeAtIndex(index);
this.remove.emit(field);
}
setFocus(index) {
this._searchBuilderFocusService.setFocus(this.id, index);
}
clearFocus() {
this._searchBuilderFocusService.clearFocus();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SearchBuilderGroupComponent, isStandalone: false, selector: "ux-search-builder-group", inputs: { id: "id", header: "header", operator: "operator", addText: "addText", placeholder: "placeholder", showPlaceholder: "showPlaceholder", removeFieldButtonAriaLabel: "removeFieldButtonAriaLabel" }, outputs: { add: "add", remove: "remove" }, providers: [SearchBuilderGroupService], ngImport: i0, template: "<h4 class=\"search-group-title\">{{ header }}</h4>\n\n<div class=\"search-group-content\">\n <div\n class=\"search-group-operator search-group-operator-{{ operator }}\"\n [class.hidden-operator]=\"searchBuilderGroupService.getQuery().length < 2\"\n >\n {{ operator }}\n </div>\n\n <div class=\"search-group-items\">\n @for (field of searchBuilderGroupService.getQuery(); track field; let i = $index) {\n <div\n class=\"search-group-item-container\"\n [class.search-group-item-focus]=\"focusIndex === i\"\n (uxFocusWithin)=\"setFocus(i)\"\n (uxBlurWithin)=\"clearFocus()\"\n >\n <div class=\"search-group-item\">\n <ng-container\n *uxSearchBuilderOutlet=\"field.type; context: field; groupId: id; index: i\"\n ></ng-container>\n </div>\n <button\n type=\"button\"\n uxFocusIndicator\n [attr.aria-label]=\"removeFieldButtonAriaLabel\"\n class=\"search-group-item-remove\"\n (click)=\"removeFieldAtIndex(i, field)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n </div>\n }\n\n <!-- Placeholder Item -->\n @if (showPlaceholder) {\n <!-- The Default Placeholder -->\n @if (!placeholder) {\n <div class=\"search-group-item-container placeholder-item\">\n <div class=\"search-group-item\">\n <label class=\"form-label\">New field</label>\n <div class=\"form-control\"></div>\n </div>\n </div>\n }\n <!-- Allow a custom placeholder -->\n <ng-container *ngTemplateOutlet=\"placeholder\"></ng-container>\n }\n </div>\n\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"search-builder-group-add-field\"\n (click)=\"addField($event)\"\n >\n <ux-icon class=\"search-builder-group-add-field-icon\" name=\"add\"></ux-icon>\n <span class=\"search-builder-group-add-field-label\">{{ addText }}</span>\n </button>\n</div>\n\n<hr class=\"search-builder-group-divider\" />\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: FocusWithinDirective, selector: "[uxFocusWithin],[uxBlurWithin]", outputs: ["uxFocusWithin", "uxBlurWithin"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: SearchBuilderOutletDirective, selector: "[uxSearchBuilderOutlet]", inputs: ["uxSearchBuilderOutlet", "uxSearchBuilderOutletContext", "uxSearchBuilderOutletGroupId", "uxSearchBuilderOutletIndex"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderGroupComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-builder-group', providers: [SearchBuilderGroupService], standalone: false, template: "<h4 class=\"search-group-title\">{{ header }}</h4>\n\n<div class=\"search-group-content\">\n <div\n class=\"search-group-operator search-group-operator-{{ operator }}\"\n [class.hidden-operator]=\"searchBuilderGroupService.getQuery().length < 2\"\n >\n {{ operator }}\n </div>\n\n <div class=\"search-group-items\">\n @for (field of searchBuilderGroupService.getQuery(); track field; let i = $index) {\n <div\n class=\"search-group-item-container\"\n [class.search-group-item-focus]=\"focusIndex === i\"\n (uxFocusWithin)=\"setFocus(i)\"\n (uxBlurWithin)=\"clearFocus()\"\n >\n <div class=\"search-group-item\">\n <ng-container\n *uxSearchBuilderOutlet=\"field.type; context: field; groupId: id; index: i\"\n ></ng-container>\n </div>\n <button\n type=\"button\"\n uxFocusIndicator\n [attr.aria-label]=\"removeFieldButtonAriaLabel\"\n class=\"search-group-item-remove\"\n (click)=\"removeFieldAtIndex(i, field)\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n </div>\n }\n\n <!-- Placeholder Item -->\n @if (showPlaceholder) {\n <!-- The Default Placeholder -->\n @if (!placeholder) {\n <div class=\"search-group-item-container placeholder-item\">\n <div class=\"search-group-item\">\n <label class=\"form-label\">New field</label>\n <div class=\"form-control\"></div>\n </div>\n </div>\n }\n <!-- Allow a custom placeholder -->\n <ng-container *ngTemplateOutlet=\"placeholder\"></ng-container>\n }\n </div>\n\n <button\n type=\"button\"\n uxFocusIndicator\n class=\"search-builder-group-add-field\"\n (click)=\"addField($event)\"\n >\n <ux-icon class=\"search-builder-group-add-field-icon\" name=\"add\"></ux-icon>\n <span class=\"search-builder-group-add-field-label\">{{ addText }}</span>\n </button>\n</div>\n\n<hr class=\"search-builder-group-divider\" />\n" }]
}], propDecorators: { id: [{
type: Input
}], header: [{
type: Input
}], operator: [{
type: Input
}], addText: [{
type: Input
}], placeholder: [{
type: Input
}], showPlaceholder: [{
type: Input
}], removeFieldButtonAriaLabel: [{
type: Input
}], add: [{
type: Output
}], remove: [{
type: Output
}] } });
class SearchBuilderComponent {
set components(components) {
this._searchBuilderService.registerComponents(components);
}
set query(value) {
this._searchBuilderService.setQuery(value);
}
get query() {
return this._searchBuilderService.getQuery();
}
/**
* Register the default search builder components
*/
constructor() {
this._searchBuilderService = inject(SearchBuilderService);
this.queryChange = new EventEmitter();
this.valid = new EventEmitter(true);
// watch for any query changes
this._querySubscription = this._searchBuilderService.queryChange.subscribe(query => this.queryChange.emit(query));
// watch for any changes to the validation
this._validSubscription = this._searchBuilderService.validationChange
.pipe(distinctUntilChanged())
.subscribe(valid => this.valid.emit(valid));
}
/**
* Remove any subscriptions and cleanup
*/
ngOnDestroy() {
this._querySubscription.unsubscribe();
this._validSubscription.unsubscribe();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: SearchBuilderComponent, isStandalone: false, selector: "ux-search-builder", inputs: { components: "components", query: "query" }, outputs: { queryChange: "queryChange", valid: "valid" }, providers: [SearchBuilderService], ngImport: i0, template: "<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-builder', providers: [SearchBuilderService], standalone: false, template: "<ng-content></ng-content>\n" }]
}], ctorParameters: () => [], propDecorators: { components: [{
type: Input
}], query: [{
type: Input
}], queryChange: [{
type: Output
}], valid: [{
type: Output
}] } });
class TagInputEvent {
constructor(tag) {
this.tag = tag;
this._defaultPrevented = false;
}
preventDefault() {
this._defaultPrevented = true;
}
defaultPrevented() {
return this._defaultPrevented;
}
}
let uniqueId$3 = 0;
const TAGINPUT_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TagInputComponent),
multi: true,
};
const TAGINPUT_VALIDATOR = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TagInputComponent),
multi: true,
};
class TagInputComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
this._element = inject(ElementRef);
this._typeaheadKeyService = inject(TypeaheadKeyService);
this._document = inject(DOCUMENT);
/** Specify a unique Id for the component */
this.id = `ux-tag-input-${++uniqueId$3}`;
/** The editable text appearing in the tag input. */
this.input = '';
/** Controls whether pasting text into the text input area automatically converts that text into one or more tags. */
this.addOnPaste = true;
/** Controls the disabled state of the tag input. */
this.disabled = false;
/**
* If set to `true`, the tag input will prevent addition and removal of tags to enforce the minTags and maxTags settings.
* Otherwise, a validation error will be raised.
*/
this.enforceTagLimits = false;
/**
* If `true`, input entered into the text input area can be converted into a tag by pressing enter.
* Otherwise, tags can only be added from the typeahead list or other external means.
* (Note that the `maxTags` and `tagPattern` will prevent invalid inputs regardless of this setting.)
*/
this.freeInput = true;
/** If `true` the input field will be readonly and selection can only occur by using the dropdown. */
this.readonlyInput = false;
/**
* The maximum number of tags permitted in the tag input. If the number of tags is equal to `maxTags` and
* `enforceTagLimits` is `true`, addition of tags will be prevented until a tag is removed
*/
this.maxTags = Number.MAX_VALUE;
/**
* The minimum number of tags permitted in the tag input. If the number of tags is equal to `minTags` and `enforceTagLimits` is
* `true`, removal of tags will be prevented until a new tag is added.
*/
this.minTags = 0;
/** The placeholder text which appears in the text input area when it is empty. */
this.placeholder = '';
/** Controls whether the typeahead appears when the text input area is clicked. This has no effect if the ux-typeahead component is not configured. */
this.showTypeaheadOnClick = false;
/**
* A string containing the characters which delimit tags.
* Typing one of the characters in `tagDelimiters` will cause the preceding text to be added as a tag,
* and the text input area will be cleared. Pasting a string containing one or more of characters in
* `tagDelimiters` will cause the string to be split into multiple tags.
* Note that the delimiter character will not be part of the tag text.
*/
this.tagDelimiters = '';
/**
* A function which returns either a string, string[], or Set<string>, compatible with the NgClass directive. The function receives the following parameters:
* - `tag: any` - the string or custom object representing the tag.
* - `index: number` - the zero-based index of the tag as it appears in the tag input.
* - `selected: boolean` - true if the tag is currently selected.
*/
this.tagClass = () => undefined;
/**
* An object which contains details of validation errors. The following properties will be present if there is a related validation error:
* - `tagRangeError` - present if the number of tags is outside the range specified by minTags and maxTags.
* - `inputPattern` - present if an input has been submitted which does not match the tagPattern.
*/
this.validationErrors = {};
/** Defines the autocomplete property on the input field which can be used to prevent the browser from displaying autocomplete suggestions. */
this.autocomplete = 'off';
/** Determine if we should show the clear all button */
this.clearButton = false;
/** Determine an aria label for the clear button */
this.clearButtonAriaLabel = 'Reset selection';
/** Emits when tags is changed. */
this.tagsChange = new EventEmitter();
/** Emits when input is changed. */
this.inputChange = new EventEmitter();
/** Raised when a tag is about to be added. The `tag` property of the event contains the tag to be added. Call `preventDefault()` on the event to prevent addition. */
this.tagAdding = new EventEmitter();
/** Raised when a tag has been added. The tag property of the event contains the tag. */
this.tagAdded = new EventEmitter();
/** Raised when a tag has failed validation according to the `tagPattern`. The tag property of the event contains the string which failed validation. */
this.tagInvalidated = new EventEmitter();
/** Raised when a tag is about to be removed. The `tag` property of the event contains the tag to be removed. Call `preventDefault()` on the event to prevent removal. */
this.tagRemoving = new EventEmitter();
/** Raised when a tag has been removed. The tag property of the event contains the tag. */
this.tagRemoved = new EventEmitter();
/** Raised when a tag has been clicked. The `tag` property of the event contains the clicked tag. Call `preventDefault()` on the event to prevent the default behaviour of selecting the tag. */
this.tagClick = new EventEmitter();
// When clicking on the input during multiple mode it will send a on touched event to the parent component
this.inputFocus = new EventEmitter();
// Emits when the component loses focus
this.inputBlur = new EventEmitter();
this.selectedIndex = -1;
this.tagApi = {
getTagDisplay: this.getTagDisplay.bind(this),
removeTagAt: this.removeTagAt.bind(this),
canRemoveTagAt: this.canRemoveTagAt.bind(this),
};
this.valid = true;
this.inputValid = true;
this._tags = [];
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._onChangeHandler = () => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._onTouchedHandler = () => { };
this._onDestroy = new Subject();
this._autoCloseDropdown = true;
}
/**
* The list of tags appearing in the tag input. This can be an array of strings or custom objects.
* See the `displayProperty` property for details of using a custom object.
*/
get tags() {
if (!this._tags) {
this._tags = [];
}
return this._tags;
}
set tags(value) {
this._tags = Array.isArray(value) ? value : [];
}
/** Determine if the dropdown panel should close on external click.*/
set autoCloseDropdown(value) {
this._autoCloseDropdown = coerceBooleanProperty(value);
}
get autoCloseDropdown() {
return this._autoCloseDropdown;
}
get _showClearButton() {
return this.clearButton && this._tags && this._tags.length > 0;
}
ngAfterContentInit() {
// Watch for optional child typeahead control
this.connectTypeahead(this.typeaheadQuery.first);
this.typeaheadQuery.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(query => this.connectTypeahead(query.first));
}
ngOnChanges(changes) {
if (changes.disabled) {
if (changes.disabled.currentValue) {
// Clear selection and close dropdown
this.selectedIndex = -1;
if (this.typeahead) {
this.typeahead.open = false;
}
}
}
// Update validation status
this.validate();
}
ngOnDestroy() {
if (this._subscription) {
this._subscription.unsubscribe();
}
this._onDestroy.next();
this._onDestroy.complete();
}
writeValue(value) {
if (value) {
this.tags = value;
this._changeDetector.markForCheck();
}
}
registerOnChange(fn) {
this._onChangeHandler = fn;
}
registerOnTouched(fn) {
this._onTouchedHandler = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
/**
* Set focus on the input field.
*/
focus() {
if (this.tagInput) {
this.tagInput.nativeElement.focus();
}
}
/**
* Validate the value of the control (tags property).
*/
validate() {
this.valid = true;
let tagRangeError = null;
if (this._tags && (this._tags.length < this.minTags || this._tags.length > this.maxTags)) {
tagRangeError = {
given: this._tags.length,
min: this.minTags,
max: this.maxTags,
};
this.valid = false;
}
this.validationErrors.tagRangeError = tagRangeError;
// forward any error to the form control
return tagRangeError;
}
keyHandler(event) {
if (this.disabled) {
return;
}
// Get the input field cursor location
const inputCursorPos = this.tagInput?.nativeElement.selectionStart ?? 0;
// Determine if the input field has any text selected
const hasSelection = this.tagInput?.nativeElement.selectionStart !== this.tagInput?.nativeElement.selectionEnd;
// Determine if a tag has focus
const tagSelected = this.isValidTagIndex(this.selectedIndex);
const inputLength = this.input ? this.input.length : 0;
// Check whether the arrow keys can move the selection. Otherwise the input field takes the event.
const canNavigateLeft = tagSelected || (inputCursorPos <= 0 && !hasSelection);
const canNavigateRight = tagSelected || (inputCursorPos >= inputLength && !hasSelection);
// Forward key events to the typeahead component.
this._typeaheadKeyService.handleKey(event, this.typeahead);
switch (event.which) {
case ENTER:
// Check if a typeahead option is highlighted
if (this.typeahead && this.typeahead.open && this.typeahead.highlighted) {
// Add the typeahead option as a tag, clear the input, and close the dropdown
this.commitTypeahead(this.typeahead.highlighted);
this.typeahead.open = false;
}
else if (this.typeahead && !this.typeahead.open && !this.freeInput) {
this.typeahead.open = true;
}
else {
// Validate and add the input text as a tag, if possible
this.commitInput();
}
event.preventDefault();
break;
case BACKSPACE:
if (canNavigateLeft) {
this.backspace();
event.stopPropagation();
event.preventDefault();
}
break;
case DELETE:
if (tagSelected) {
this.removeTagAt(this.selectedIndex);
}
break;
case LEFT_ARROW:
if (canNavigateLeft) {
this.moveSelection(-1);
event.preventDefault();
}
break;
case RIGHT_ARROW:
if (canNavigateRight) {
this.moveSelection(1);
event.preventDefault();
}
break;
}
// Check for keys in the tagDelimiters
if (this.tagDelimiters && this.tagDelimiters.indexOf(this.getKeyChar(event)) >= 0) {
// Commit previous text
this.commitInput();
event.stopPropagation();
event.preventDefault();
}
}
focusOutHandler() {
// If a click on the typeahead is in progress, don't do anything.
// This works around an issue in IE where clicking a scrollbar drops focus.
if (this.typeahead?.clicking) {
return;
}
// Close the dropdown on blur
setTimeout(() => {
if (!this._element.nativeElement.contains(this._document.activeElement) &&
this.autoCloseDropdown) {
this.selectedIndex = -1;
if (this.typeahead) {
this.typeahead.open = false;
this._changeDetector.markForCheck();
}
}
}, 200);
}
onClick() {
// Prevent error if you click input when at max tag limit
if (this.tagInput === undefined) {
return;
}
// focus the input element
this.tagInput.nativeElement.focus();
// show the typeahead if we need to
this.inputClickHandler();
}
tagClickHandler(event, tag, index) {
if (this.disabled) {
return;
}
// Send tagClick event
const tagClickEvent = new TagInputEvent(tag);
this.tagClick.emit(tagClickEvent);
// Prevent focus if preventDefault() was called
if (tagClickEvent.defaultPrevented()) {
event.preventDefault();
return;
}
// Select the tag (for IE that doesn't propagate focus)
this.selectTagAt(index);
}
inputClickHandler() {
if (this.disabled) {
return;
}
if (this.typeahead && this.showTypeaheadOnClick) {
this.typeahead.open = true;
}
}
inputFocusHandler() {
if (this.disabled) {
return;
}
this.selectInput();
// mark form control as touched
this._onTouchedHandler();
}
inputPasteHandler(event) {
if (this.disabled) {
return;
}
if (this.addOnPaste) {
// Get text from the clipboard
let input = null;
if (event.clipboardData) {
input = event.clipboardData.getData('text/plain');
}
else if (window.clipboardData) {
// Internet Explorer only
input = window.clipboardData.getData('Text');
}
// Commit the clipboard text directly
if (this.commit(input)) {
this.selectInput();
event.stopPropagation();
event.preventDefault();
}
}
}
typeaheadOptionSelectedHandler(event) {
if (this.disabled) {
return;
}
// When the typeahead sends the optionSelected event, commit the object directly
this.commitTypeahead(event.option);
}
/**
* Commit the current input value and clear the input field if successful.
*/
commitInput() {
if (this.commit(this.input)) {
this.selectInput();
this.toggle();
this.setInputValue('');
}
}
/**
* Commit the given tag object and clear the input if successful.
*/
commitTypeahead(tag) {
if (this.addTag(tag)) {
this.selectInput();
this.setInputValue('');
}
}
/**
* Commit the given string value as one or more tags, if validation passes. Returns true if the tag(s) were created.
*/
commit(input) {
if (input && this.freeInput) {
// Split the tags by the tagDelimiters if configured
const newTags = this.splitTagInput(input);
// Check tag validation for all of the individual values
let allValid = true;
for (const newTag of newTags) {
const valid = this.validateTag(newTag);
if (!valid) {
allValid = false;
}
}
// Add the tags if all are valid
if (allValid) {
for (const newTag of newTags) {
this.addTag(this.createTag(newTag));
}
return true;
}
}
return false;
}
/**
* If no tag is selected, select the rightmost tag. If a tag is selected, remove it.
*/
backspace() {
if (this.disabled) {
return;
}
if (!this.isValidTagIndex(this.selectedIndex)) {
this.selectTagAt(this._tags.length - 1);
}
else {
this.removeTagAt(this.selectedIndex);
}
}
/**
* Move the highlighted option forwards or backwards in the list. Wraps at the limits.
* @param delta Value to be added to the selected index, i.e. -1 to move backwards, +1 to move forwards.
*/
moveSelection(delta) {
if (this.disabled) {
return;
}
if (this.isValidSelectIndex(this.selectedIndex)) {
this.selectedIndex += delta;
// Do wrapping of selection when out of bounds
if (this.selectedIndex < 0) {
this.selectedIndex = this._tags.length;
}
else if (this.selectedIndex > this._tags.length) {
this.selectedIndex = 0;
}
}
}
/**
* Returns a value to display for the given tag. Uses display function/property name if set, otherwise assumes that the tag is a simple string.
*/
getTagDisplay(tag) {
if (typeof this.display === 'function') {
return this.display(tag);
}
if (typeof this.display === 'string') {
return tag[this.display];
}
return tag;
}
/**
* Returns true if the given index is selected (tag index or input field).
*/
isSelected(index) {
return index === this.selectedIndex;
}
/**
* Select the tag at the given index. Does nothing if disabled is true.
*/
selectTagAt(tagIndex) {
if (this.disabled) {
return;
}
if (this.isValidTagIndex(tagIndex)) {
this.selectedIndex = tagIndex;
}
}
/**
* Select the input field, giving it focus. Does nothing if disabled is true.
*/
selectInput() {
if (this.disabled) {
return;
}
this.selectedIndex = this._tags.length;
}
/**
* Remove the tag at the given index. Does nothing if disabled is true or the minTags property prevents removal.
*/
removeTagAt(tagIndex) {
if (this.disabled || !this.canRemoveTagAt()) {
return;
}
// Check that the tagIndex is in range
if (this.isValidTagIndex(tagIndex)) {
const tag = this._tags[tagIndex];
const tagRemovingEvent = new TagInputEvent(tag);
this.tagRemoving.emit(tagRemovingEvent);
if (!tagRemovingEvent.defaultPrevented()) {
// Select input first to avoid issues with dropping focus
this.selectInput();
// Remove the tag
this.tags = this._tags.filter((_tag, index) => index !== tagIndex);
this.setTagsValue(this._tags);
// Set focus again since indices have changed
this.selectInput();
this.tagRemoved.emit(new TagInputEvent(tag));
this.validate();
}
}
}
/**
* Returns true if the tag at the given index can be removed.
*/
canRemoveTagAt() {
return this._tags.length > this.minTags || !this.enforceTagLimits;
}
/**
* Returns true if the input field should be available.
*/
isInputVisible() {
return this._tags.length < this.maxTags || !this.enforceTagLimits;
}
/**
* Returns true if any part of the control has focus.
*/
hasFocus() {
return this.isValidSelectIndex(this.selectedIndex);
}
toggle() {
this.typeahead && this.typeahead.open
? (this.typeahead.open = false)
: this.inputClickHandler();
}
clear() {
if (this.disabled) {
return;
}
this.tags = [];
this.setTagsValue(this._tags);
this.setInputValue('');
this.focus();
}
setInputValue(text) {
this.input = text;
this.inputChange.emit(text);
}
setTagsValue(tags) {
this._onChangeHandler(tags);
this.tagsChange.emit(tags);
}
connectTypeahead(typeahead) {
if (this._subscription) {
this._subscription.unsubscribe();
this._subscription = null;
}
this.typeahead = typeahead;
if (this.typeahead) {
// Set up event handler for selected options
this._subscription = this.typeahead.optionSelected.subscribe(this.typeaheadOptionSelectedHandler.bind(this));
// Set up event handler for the highlighted element
// Added a delay to move it out of the current change detection cycle
this._subscription.add(this.typeahead.highlightedElementChange
.pipe(tick())
.subscribe((element) => (this.highlightedElement = element)));
}
}
/**
* Validate the given tagValue with the tagPattern, if set. Update validationErrors on validation failure.
*/
validateTag(tagValue) {
let inputPattern = null;
this.inputValid = true;
if (this.tagPattern && !this.tagPattern.test(tagValue)) {
inputPattern = {
given: tagValue,
pattern: this.tagPattern,
};
this.inputValid = false;
}
this.validationErrors.inputPattern = inputPattern;
return this.inputValid;
}
/**
* Create a tag object for the given tagValue. If createTagHandler is specified, use it; otherwise if displayProperty is specified, create an object with the tagValue as the single named property; otherwise return the tagValue itself.
*/
createTag(tagValue) {
let tag = null;
if (this.createTagHandler && typeof this.createTagHandler === 'function') {
tag = this.createTagHandler(tagValue);
}
else if (typeof this.display === 'string') {
tag = {};
tag[this.display] = tagValue;
}
else {
tag = tagValue;
}
return tag;
}
/**
* Add a tag object, calling the tagAdding and tagAdded events. Returns true if the tag was added to the tags array.
*/
addTag(tag) {
if (tag !== null) {
// Verify that the new tag can be displayed
const displayValue = this.getTagDisplay(tag);
if (typeof displayValue === 'string') {
const tagAddingEvent = new TagInputEvent(tag);
this.tagAdding.emit(tagAddingEvent);
if (!tagAddingEvent.defaultPrevented()) {
this.tags = [...this._tags, tag];
this.setTagsValue(this._tags);
this.tagAdded.emit(new TagInputEvent(tag));
this.validate();
return true;
}
}
}
return false;
}
/**
* Returns true if the given tagIndex is a valid tag index.
*/
isValidTagIndex(tagIndex) {
return tagIndex >= 0 && tagIndex < this._tags.length;
}
/**
* Returns true if the given index is a valid selection index (tags or input field).
*/
isValidSelectIndex(index) {
return index >= 0 && index <= this._tags.length;
}
/**
* Returns the character corresponding to the given key event, mainly for IE compatibility.
*/
getKeyChar(event) {
switch (event.which) {
case SPACE:
return ' ';
}
return event.key;
}
/**
* Returns an array of strings corresponding to the input string split by the tagDelimiters characters.
*/
splitTagInput(input) {
let tagValues = [input];
if (this.tagDelimiters && typeof this.tagDelimiters === 'string') {
// eslint-disable-next-line no-useless-escape
const escapedDelimiters = this.tagDelimiters.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const delimiterRegex = new RegExp(`[${escapedDelimiters}]`, 'g');
tagValues = input.split(delimiterRegex).filter(s => s.length > 0);
}
return tagValues;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: TagInputComponent, isStandalone: false, selector: "ux-tag-input", inputs: { id: "id", tags: "tags", input: "input", display: "display", addOnPaste: "addOnPaste", ariaLabel: "ariaLabel", ariaLabelledby: "ariaLabelledby", disabled: "disabled", required: "required", enforceTagLimits: "enforceTagLimits", freeInput: "freeInput", readonlyInput: "readonlyInput", maxTags: "maxTags", minTags: "minTags", placeholder: "placeholder", showTypeaheadOnClick: "showTypeaheadOnClick", tagDelimiters: "tagDelimiters", tagPattern: "tagPattern", tagTemplate: "tagTemplate", tagClass: "tagClass", validationErrors: "validationErrors", autocomplete: "autocomplete", createTagHandler: ["createTag", "createTagHandler"], icon: "icon", clearButton: "clearButton", clearButtonAriaLabel: "clearButtonAriaLabel", autoCloseDropdown: "autoCloseDropdown" }, outputs: { tagsChange: "tagsChange", inputChange: "inputChange", tagAdding: "tagAdding", tagAdded: "tagAdded", tagInvalidated: "tagInvalidated", tagRemoving: "tagRemoving", tagRemoved: "tagRemoved", tagClick: "tagClick", inputFocus: "inputFocus", inputBlur: "inputBlur" }, host: { listeners: { "keydown": "keyHandler($event)", "focusout": "focusOutHandler()", "click": "onClick()" }, properties: { "class.disabled": "disabled", "class.focus": "hasFocus()", "class.invalid": "!valid || !inputValid", "attr.id": "this.id" } }, providers: [TAGINPUT_VALUE_ACCESSOR, TAGINPUT_VALIDATOR], queries: [{ propertyName: "typeaheadQuery", predicate: TypeaheadComponent }], viewQueries: [{ propertyName: "tagInput", first: true, predicate: ["tagInput"], descendants: true }], exportAs: ["ux-tag-input"], usesOnChanges: true, ngImport: i0, template: "<ol\n [attr.aria-haspopup]=\"typeahead ? 'listbox' : null\"\n [attr.aria-expanded]=\"typeahead ? typeahead.open : null\"\n [attr.aria-controls]=\"typeahead ? typeahead.id : null\"\n [class.ux-tag-input-clear-inset]=\"_showClearButton\"\n [class.ux-tag-input-icon-inset]=\"icon\"\n (click)=\"toggle()\"\n>\n @for (tag of _tags; track tag; let i = $index) {\n <li\n class=\"ux-tag\"\n [class.disabled]=\"disabled\"\n [ngClass]=\"tagClass(tag, i, isSelected(i))\"\n [attr.tabindex]=\"disabled ? null : 0\"\n [focusIf]=\"isSelected(i)\"\n (click)=\"tagClickHandler($event, tag, i); $event.stopPropagation()\"\n (focus)=\"selectTagAt(i)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"tagTemplate || defaultTagTemplate\"\n [ngTemplateOutletContext]=\"{ tag: tag, index: i, disabled: disabled, api: tagApi }\"\n >\n </ng-container>\n </li>\n }\n @if (isInputVisible()) {\n <li class=\"ux-tag-input\">\n <input\n #tagInput\n type=\"text\"\n attr.id=\"{{ id }}-input\"\n class=\"ux-tag-input\"\n [ngModel]=\"input\"\n (ngModelChange)=\"setInputValue($event)\"\n [autocomplete]=\"autocomplete\"\n [class.invalid]=\"!inputValid\"\n [required]=\"required\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n [attr.aria-autocomplete]=\"typeahead ? 'list' : 'none'\"\n [attr.aria-controls]=\"typeahead?.id\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n aria-multiline=\"false\"\n [placeholder]=\"disabled ? '' : placeholder || ''\"\n [disabled]=\"disabled\"\n [focusIf]=\"isSelected(_tags.length)\"\n (click)=\"toggle(); $event.stopPropagation()\"\n (focus)=\"inputFocusHandler(); inputFocus.emit($event)\"\n (blur)=\"inputBlur.emit($event)\"\n (paste)=\"inputPasteHandler($event)\"\n [readonly]=\"readonlyInput\"\n />\n </li>\n }\n</ol>\n\n<!-- Insert the custom icon if provided -->\n@if (icon || _showClearButton) {\n <div class=\"ux-tag-icons\" (click)=\"toggle(); $event.stopPropagation()\">\n <!-- Clear All Button -->\n @if (_showClearButton) {\n <i\n uxFocusIndicator\n class=\"ux-tag-icon ux-icon ux-icon-close ux-select-clear-icon\"\n [attr.tabindex]=\"disabled ? -1 : 0\"\n [attr.aria-label]=\"clearButtonAriaLabel\"\n (click)=\"clear(); $event.stopPropagation()\"\n (keydown.enter)=\"clear(); $event.stopPropagation()\"\n >\n </i>\n }\n <!-- Custom Icon -->\n @if (icon) {\n <div class=\"ux-custom-icon\">\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n </div>\n }\n </div>\n}\n\n<ng-content #typeahead></ng-content>\n\n<ng-template\n #defaultTagTemplate\n let-tag=\"tag\"\n let-index=\"index\"\n let-disabled=\"disabled\"\n let-api=\"api\"\n>\n <span class=\"ux-tag-text\">{{ api.getTagDisplay(tag) }}</span>\n @if (api.canRemoveTagAt(index)) {\n <button\n uxFocusIndicator\n type=\"button\"\n class=\"ux-tag-remove\"\n aria-label=\"Remove Item\"\n [disabled]=\"disabled\"\n (click)=\"api.removeTagAt(index); $event.stopPropagation()\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TagInputComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-tag-input', exportAs: 'ux-tag-input', providers: [TAGINPUT_VALUE_ACCESSOR, TAGINPUT_VALIDATOR], changeDetection: ChangeDetectionStrategy.OnPush, host: {
'[class.disabled]': 'disabled',
'[class.focus]': 'hasFocus()',
'[class.invalid]': '!valid || !inputValid',
}, standalone: false, template: "<ol\n [attr.aria-haspopup]=\"typeahead ? 'listbox' : null\"\n [attr.aria-expanded]=\"typeahead ? typeahead.open : null\"\n [attr.aria-controls]=\"typeahead ? typeahead.id : null\"\n [class.ux-tag-input-clear-inset]=\"_showClearButton\"\n [class.ux-tag-input-icon-inset]=\"icon\"\n (click)=\"toggle()\"\n>\n @for (tag of _tags; track tag; let i = $index) {\n <li\n class=\"ux-tag\"\n [class.disabled]=\"disabled\"\n [ngClass]=\"tagClass(tag, i, isSelected(i))\"\n [attr.tabindex]=\"disabled ? null : 0\"\n [focusIf]=\"isSelected(i)\"\n (click)=\"tagClickHandler($event, tag, i); $event.stopPropagation()\"\n (focus)=\"selectTagAt(i)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"tagTemplate || defaultTagTemplate\"\n [ngTemplateOutletContext]=\"{ tag: tag, index: i, disabled: disabled, api: tagApi }\"\n >\n </ng-container>\n </li>\n }\n @if (isInputVisible()) {\n <li class=\"ux-tag-input\">\n <input\n #tagInput\n type=\"text\"\n attr.id=\"{{ id }}-input\"\n class=\"ux-tag-input\"\n [ngModel]=\"input\"\n (ngModelChange)=\"setInputValue($event)\"\n [autocomplete]=\"autocomplete\"\n [class.invalid]=\"!inputValid\"\n [required]=\"required\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n [attr.aria-autocomplete]=\"typeahead ? 'list' : 'none'\"\n [attr.aria-controls]=\"typeahead?.id\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n aria-multiline=\"false\"\n [placeholder]=\"disabled ? '' : placeholder || ''\"\n [disabled]=\"disabled\"\n [focusIf]=\"isSelected(_tags.length)\"\n (click)=\"toggle(); $event.stopPropagation()\"\n (focus)=\"inputFocusHandler(); inputFocus.emit($event)\"\n (blur)=\"inputBlur.emit($event)\"\n (paste)=\"inputPasteHandler($event)\"\n [readonly]=\"readonlyInput\"\n />\n </li>\n }\n</ol>\n\n<!-- Insert the custom icon if provided -->\n@if (icon || _showClearButton) {\n <div class=\"ux-tag-icons\" (click)=\"toggle(); $event.stopPropagation()\">\n <!-- Clear All Button -->\n @if (_showClearButton) {\n <i\n uxFocusIndicator\n class=\"ux-tag-icon ux-icon ux-icon-close ux-select-clear-icon\"\n [attr.tabindex]=\"disabled ? -1 : 0\"\n [attr.aria-label]=\"clearButtonAriaLabel\"\n (click)=\"clear(); $event.stopPropagation()\"\n (keydown.enter)=\"clear(); $event.stopPropagation()\"\n >\n </i>\n }\n <!-- Custom Icon -->\n @if (icon) {\n <div class=\"ux-custom-icon\">\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n </div>\n }\n </div>\n}\n\n<ng-content #typeahead></ng-content>\n\n<ng-template\n #defaultTagTemplate\n let-tag=\"tag\"\n let-index=\"index\"\n let-disabled=\"disabled\"\n let-api=\"api\"\n>\n <span class=\"ux-tag-text\">{{ api.getTagDisplay(tag) }}</span>\n @if (api.canRemoveTagAt(index)) {\n <button\n uxFocusIndicator\n type=\"button\"\n class=\"ux-tag-remove\"\n aria-label=\"Remove Item\"\n [disabled]=\"disabled\"\n (click)=\"api.removeTagAt(index); $event.stopPropagation()\"\n >\n <ux-icon name=\"close\"></ux-icon>\n </button>\n }\n</ng-template>\n" }]
}], propDecorators: { id: [{
type: Input
}, {
type: HostBinding,
args: ['attr.id']
}], tags: [{
type: Input
}], input: [{
type: Input
}], display: [{
type: Input
}], addOnPaste: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaLabelledby: [{
type: Input
}], disabled: [{
type: Input
}], required: [{
type: Input
}], enforceTagLimits: [{
type: Input
}], freeInput: [{
type: Input
}], readonlyInput: [{
type: Input
}], maxTags: [{
type: Input
}], minTags: [{
type: Input
}], placeholder: [{
type: Input
}], showTypeaheadOnClick: [{
type: Input
}], tagDelimiters: [{
type: Input
}], tagPattern: [{
type: Input
}], tagTemplate: [{
type: Input
}], tagClass: [{
type: Input
}], validationErrors: [{
type: Input
}], autocomplete: [{
type: Input
}], createTagHandler: [{
type: Input,
args: ['createTag']
}], icon: [{
type: Input
}], clearButton: [{
type: Input
}], clearButtonAriaLabel: [{
type: Input
}], autoCloseDropdown: [{
type: Input
}], tagsChange: [{
type: Output
}], inputChange: [{
type: Output
}], tagAdding: [{
type: Output
}], tagAdded: [{
type: Output
}], tagInvalidated: [{
type: Output
}], tagRemoving: [{
type: Output
}], tagRemoved: [{
type: Output
}], tagClick: [{
type: Output
}], inputFocus: [{
type: Output
}], inputBlur: [{
type: Output
}], typeaheadQuery: [{
type: ContentChildren,
args: [TypeaheadComponent]
}], tagInput: [{
type: ViewChild,
args: ['tagInput', { static: false }]
}], keyHandler: [{
type: HostListener,
args: ['keydown', ['$event']]
}], focusOutHandler: [{
type: HostListener,
args: ['focusout']
}], onClick: [{
type: HostListener,
args: ['click']
}] } });
let uniqueId$2 = 0;
const SELECT_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true,
};
class SelectComponent {
constructor() {
this._element = inject(ElementRef);
this._platform = inject(Platform);
this._typeaheadKeyService = inject(TypeaheadKeyService);
this._changeDetector = inject(ChangeDetectorRef);
this._document = inject(DOCUMENT);
/** A unique id for the component. */
this.id = `ux-select-${++uniqueId$2}`;
/**
* Controls whether the value of the single select control can be cleared by deleting the selected value in the
* input field. This does not affect the initial state of the control, so specify a value for `value` if null should
* never be allowed.
*/
this.allowNull = false;
/** Controls the disabled state of the tag input. */
this.disabled = false;
/** The positioning of the typeahead dropdown in relation to its parent. */
this.dropDirection = 'down';
/** The maximum height of the typeahead dropdown, as a CSS value. */
this.maxHeight = '250px';
/**
* Controls whether the user can select more than one option in the select control. If set to true, selected
* options will appear as tags in the input area. If set to false, the selected value will appear as editable text
* in the input area.
*/
this.multiple = false;
/**
* The number of options to request in a page. This should ideally be more than twice the number of items which
* fit into the height of the dropdown, but this is not required.
*/
this.pageSize = 20;
/** The placeholder text which appears in the text input area when it is empty. */
this.placeholder = '';
/**
* Defines the `autocomplete` property on the `input` element which can be used to prevent the browser from
* displaying autocomplete suggestions.
*/
this.autocomplete = 'off';
/** If `true` the input field will be readonly and selection can only occur by using the dropdown. */
this.readonlyInput = false;
/** Determine if we should show the clear all button */
this.clearButton = false;
/** Determine an aria label for the clear button */
this.clearButtonAriaLabel = 'Reset selection';
/** Emits when `value` changes. */
this.valueChange = new EventEmitter();
/** Emits when `input` changes. */
this.inputChange = new EventEmitter();
/** Emits when `dropdownOpen` changes. */
this.dropdownOpenChange = new EventEmitter();
/** Emits when recently selected options change. */
this.recentOptionsChange = new EventEmitter();
this._value$ = new ReplaySubject(1);
this._hasValue = false;
this._input$ = new BehaviorSubject({ userInteraction: false, value: '' });
this._dropdownOpen = false;
this._userInput = false;
this._filterDebounceTime = 200;
this._autoCloseDropdown = true;
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._onChange = (_) => { };
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._onTouched = () => { };
this._onDestroy = new Subject();
}
/** The selected option (for single select) or array of options (for multiple select). */
set value(value) {
this._value$.next(value);
}
get value() {
return this._value;
}
/** The text in the input area. This is used to filter the options dropdown. */
set input(value) {
this._input$.next({ ...this._input$.value, value });
}
get input() {
return this._input$.value.value;
}
/** The status of the typeahead dropdown. */
set dropdownOpen(value) {
this._dropdownOpen = value;
this.dropdownOpenChange.emit(value);
}
get dropdownOpen() {
return this._dropdownOpen;
}
/** Determine if the dropdown panel should close on external click.*/
set autoCloseDropdown(value) {
this._autoCloseDropdown = coerceBooleanProperty(value);
}
get autoCloseDropdown() {
return this._autoCloseDropdown;
}
/** Specify the debounceTime value for the select filter */
get filterDebounceTime() {
return this._filterDebounceTime;
}
set filterDebounceTime(filterDebounceTime) {
this._filterDebounceTime = coerceNumberProperty(filterDebounceTime);
}
ngOnInit() {
// Emit change events
this._value$.pipe(takeUntil(this._onDestroy), distinctUntilChanged()).subscribe(value => {
this._value = value;
this._hasValue = !!value;
});
// Changes to the input field
this._input$
.pipe(skip(1), filter(() => this.allowNull), filter(value => !this.multiple && value.value !== this.getDisplay(this.value)), takeUntil(this._onDestroy))
.subscribe(input => {
if (input.userInteraction && input.value === '') {
this.value = null;
this._onChange(null);
this.valueChange.next(null);
}
});
// open the dropdown once the filter debounce has elapsed
this.filter$
.pipe(filter(() => this._userInput), take(1), takeUntil(this._onDestroy))
.subscribe(() => {
this.dropdownOpen = true;
this._userInput = false;
});
// Update the single-select input when the model changes
this._value$
.pipe(distinctUntilChanged(), delay(0), filter(value => value !== null && !this.multiple), takeUntil(this._onDestroy))
.subscribe(value => {
const inputValue = this.getDisplay(value);
// check if the input value has changed and if so the emit
if (inputValue !== this.input) {
this.input = inputValue;
this.inputChange.emit(this.input);
}
this._changeDetector.markForCheck();
});
}
ngOnChanges(changes) {
if (changes.multiple &&
!changes.multiple.firstChange &&
changes.multiple.currentValue !== changes.multiple.previousValue) {
this.input = '';
}
// Set up filter from input
this.filter$ = this._input$.pipe(map(input => !this.multiple && input.value === this.getDisplay(this.value) ? '' : input.value), debounceTime(this.filterDebounceTime));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
writeValue(obj) {
if (obj !== undefined && obj !== this.value) {
this.value = obj;
this._changeDetector.markForCheck();
}
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
inputClickHandler() {
this.selectInputText();
this.dropdownOpen = true;
}
inputBlurHandler() {
// If a click on the typeahead is in progress, just refocus the input.
// This works around an issue in IE where clicking a scrollbar drops focus.
if (this.singleTypeahead && this.singleTypeahead.clicking) {
this.singleInput.nativeElement.focus();
return;
}
// Close dropdown and reset text input if focus is lost
setTimeout(() => {
if (!this._element.nativeElement.contains(this._document.activeElement) &&
this._autoCloseDropdown) {
this.dropdownOpen = false;
if (!this.multiple) {
this.input = this.getDisplay(this.value);
}
}
}, 200);
}
/**
* Key handler for single select only. Multiple select key handling is in TagInputComponent.
*/
inputKeyHandler(event) {
// Standard keys for typeahead (up/down/esc)
this._typeaheadKeyService.handleKey(event, this.singleTypeahead);
if (event.keyCode === ENTER) {
if (this._dropdownOpen) {
// Set the highlighted option as the value and close
this.singleTypeahead.selectHighlighted();
}
else {
this.dropdownOpen = true;
}
// Update the input field. If dropdown isn't open then reset it to the previous value.
this.input = this.getDisplay(this.value);
event.preventDefault();
}
// when the user types and the value is not empty then we should open the dropdown except for non printable keys.
if (event.key.length === 1) {
this._userInput = true;
this._dropdownOpen = true;
}
}
/** This gets called whenever the user types in the input */
onInputChange(input) {
this._input$.next({
value: input,
userInteraction: true,
});
this.inputChange.emit(this.input);
}
/** Whenever a single select item is selected emit the values */
_singleOptionSelected(event) {
if (event.option !== null && event.option !== this.value) {
this.value = event.option;
this.dropdownOpen = false;
this.valueChange.emit(this.value);
this._onChange(this.value);
}
}
/** Whenever a multi-select item is selected emit the values */
_multipleOptionSelected(selection) {
// update the internal selection
this._value$.next(selection);
this.valueChange.emit(this.value);
this._onChange(this.value);
}
/**
* Returns the display value of the given option.
*/
getDisplay(option) {
if (option === null || option === undefined) {
return '';
}
if (typeof this.display === 'function') {
return this.display(option);
}
if (typeof this.display === 'string' &&
typeof option === 'object' &&
// eslint-disable-next-line no-prototype-builtins
option.hasOwnProperty(this.display)) {
return option[this.display];
}
return option;
}
/** Toggle the dropdown open state */
toggle() {
// if the select is disabled then do not show the dropdown
if (this.disabled) {
return;
}
if (this.dropdownOpen) {
this.dropdownOpen = false;
}
else {
this.inputClickHandler();
}
}
/** Handle input focus events */
onFocus() {
// mark form control as touched
this._onTouched();
// if the input is readonly we do not want to select the text on focus
if (this.readonlyInput) {
// cast the select input element
const element = this.singleInput.nativeElement;
// firefox requires a delay before clearing the selection (other browsers don't)
this._platform.FIREFOX
? requestAnimationFrame(() => element.setSelectionRange(0, 0))
: element.setSelectionRange(0, 0);
}
}
clear() {
if (this.disabled) {
return;
}
// clear the value and input text
this.value = null;
this.input = null;
this.selectInputText();
// emit the latest values
this.valueChange.emit(this.value);
this._onChange(this.value);
this.inputChange.emit(this.input);
}
selectInputText() {
if (!this.readonlyInput) {
this.singleInput.nativeElement.select();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SelectComponent, isStandalone: false, selector: "ux-select, ux-combobox, ux-dropdown", inputs: { id: "id", value: "value", input: "input", dropdownOpen: "dropdownOpen", options: "options", display: "display", key: "key", allowNull: "allowNull", ariaLabel: "ariaLabel", ariaLabelledby: "ariaLabelledby", listboxAriaLabel: "listboxAriaLabel", disabled: "disabled", dropDirection: "dropDirection", maxHeight: "maxHeight", multiple: "multiple", pageSize: "pageSize", placeholder: "placeholder", tagTemplate: "tagTemplate", optionsHeadingTemplate: "optionsHeadingTemplate", recentOptionsHeadingTemplate: "recentOptionsHeadingTemplate", autocomplete: "autocomplete", loadingTemplate: "loadingTemplate", noOptionsTemplate: "noOptionsTemplate", readonlyInput: "readonlyInput", clearButton: "clearButton", clearButtonAriaLabel: "clearButtonAriaLabel", autoCloseDropdown: "autoCloseDropdown", optionTemplate: "optionTemplate", recentOptions: "recentOptions", recentOptionsMaxCount: "recentOptionsMaxCount", required: "required", filterDebounceTime: "filterDebounceTime" }, outputs: { valueChange: "valueChange", inputChange: "inputChange", dropdownOpenChange: "dropdownOpenChange", recentOptionsChange: "recentOptionsChange" }, host: { properties: { "class.ux-select-custom-icon": "!!icon", "class.ux-select-disabled": "disabled", "attr.id": "this.id" } }, providers: [SELECT_VALUE_ACCESSOR], queries: [{ propertyName: "icon", first: true, predicate: ["icon"], descendants: true }], viewQueries: [{ propertyName: "singleInput", first: true, predicate: ["singleInput"], descendants: true }, { propertyName: "tagInput", first: true, predicate: ["tagInput"], descendants: true }, { propertyName: "multipleTypeahead", first: true, predicate: ["multipleTypeahead"], descendants: true }, { propertyName: "singleTypeahead", first: true, predicate: ["singleTypeahead"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (multiple) {\n <ux-tag-input\n #tagInput=\"ux-tag-input\"\n [id]=\"id + '-input'\"\n [tags]=\"_value$ | async\"\n (tagsChange)=\"_multipleOptionSelected($event)\"\n [ariaLabelledby]=\"ariaLabelledby\"\n [(input)]=\"input\"\n [required]=\"required\"\n (inputChange)=\"onInputChange($event)\"\n [ariaLabel]=\"ariaLabel\"\n [autocomplete]=\"autocomplete\"\n [addOnPaste]=\"false\"\n [disabled]=\"disabled\"\n [display]=\"display\"\n [freeInput]=\"false\"\n [placeholder]=\"placeholder || ''\"\n [tagTemplate]=\"tagTemplate\"\n (inputFocus)=\"onFocus()\"\n [showTypeaheadOnClick]=\"true\"\n [readonlyInput]=\"readonlyInput\"\n [icon]=\"icon\"\n [clearButton]=\"clearButton\"\n [autoCloseDropdown]=\"autoCloseDropdown\"\n [clearButtonAriaLabel]=\"clearButtonAriaLabel\"\n >\n <ux-typeahead\n #multipleTypeahead\n [ariaLabel]=\"listboxAriaLabel\"\n [id]=\"id + '-typeahead'\"\n [options]=\"options\"\n [filter]=\"filter$ | async\"\n [(open)]=\"dropdownOpen\"\n [display]=\"display\"\n [key]=\"key\"\n [disabledOptions]=\"_value$ | async\"\n [dropDirection]=\"dropDirection\"\n [maxHeight]=\"maxHeight\"\n [multiselectable]=\"true\"\n [pageSize]=\"pageSize\"\n [selectFirst]=\"true\"\n [loadingTemplate]=\"loadingTemplate\"\n [optionTemplate]=\"optionTemplate\"\n [noOptionsTemplate]=\"noOptionsTemplate\"\n [recentOptions]=\"recentOptions\"\n [recentOptionsMaxCount]=\"recentOptionsMaxCount\"\n (recentOptionsChange)=\"recentOptionsChange.emit($event)\"\n [optionsHeadingTemplate]=\"optionsHeadingTemplate\"\n [recentOptionsHeadingTemplate]=\"recentOptionsHeadingTemplate\"\n >\n </ux-typeahead>\n </ux-tag-input>\n}\n\n@if (!multiple) {\n <div class=\"ux-select-container\" [class.disabled]=\"disabled\" aria-haspopup=\"listbox\">\n <input\n #singleInput\n type=\"text\"\n [attr.id]=\"id + '-input'\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n class=\"form-control\"\n [required]=\"required\"\n [class.ux-tag-input-clear-inset]=\"clearButton && allowNull && _hasValue\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n aria-autocomplete=\"list\"\n role=\"combobox\"\n [attr.aria-controls]=\"id + '-typeahead'\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-expanded]=\"dropdownOpen\"\n [autocomplete]=\"autocomplete\"\n [(ngModel)]=\"input\"\n (ngModelChange)=\"onInputChange($event)\"\n [placeholder]=\"placeholder || ''\"\n [disabled]=\"disabled\"\n (click)=\"toggle()\"\n (focus)=\"onFocus()\"\n (blur)=\"inputBlurHandler()\"\n (keydown)=\"inputKeyHandler($event)\"\n [readonly]=\"readonlyInput\"\n />\n <div class=\"ux-select-icons\">\n @if (clearButton && allowNull && _hasValue) {\n <i\n uxFocusIndicator\n [attr.tabindex]=\"disabled ? -1 : 0\"\n [attr.aria-label]=\"clearButtonAriaLabel\"\n class=\"ux-select-icon ux-icon ux-icon-close ux-select-clear-icon\"\n (click)=\"clear(); $event.stopPropagation()\"\n (keydown.enter)=\"clear(); $event.stopPropagation()\"\n >\n </i>\n }\n @if (!icon) {\n <i\n class=\"ux-select-icon ux-icon ux-select-chevron-icon\"\n [class.ux-icon-up]=\"dropDirection === 'up'\"\n [class.ux-icon-down]=\"dropDirection === 'down'\"\n (click)=\"toggle(); $event.stopPropagation(); singleInput.focus()\"\n >\n </i>\n }\n @if (icon) {\n <div class=\"ux-custom-icon\">\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n </div>\n }\n </div>\n <ux-typeahead\n #singleTypeahead\n [ariaLabel]=\"listboxAriaLabel\"\n [id]=\"id + '-typeahead'\"\n [active]=\"_value$ | async\"\n [options]=\"options\"\n [filter]=\"filter$ | async\"\n [(open)]=\"dropdownOpen\"\n [display]=\"display\"\n [key]=\"key\"\n [dropDirection]=\"dropDirection\"\n [maxHeight]=\"maxHeight\"\n [multiselectable]=\"false\"\n [openOnFilterChange]=\"false\"\n [pageSize]=\"pageSize\"\n [selectFirst]=\"true\"\n [loadingTemplate]=\"loadingTemplate\"\n [optionTemplate]=\"optionTemplate\"\n [noOptionsTemplate]=\"noOptionsTemplate\"\n [recentOptions]=\"recentOptions\"\n [recentOptionsMaxCount]=\"recentOptionsMaxCount\"\n [optionsHeadingTemplate]=\"optionsHeadingTemplate\"\n [recentOptionsHeadingTemplate]=\"recentOptionsHeadingTemplate\"\n (optionSelected)=\"_singleOptionSelected($event)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n (recentOptionsChange)=\"recentOptionsChange.emit($any($event))\"\n >\n </ux-typeahead>\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TagInputComponent, selector: "ux-tag-input", inputs: ["id", "tags", "input", "display", "addOnPaste", "ariaLabel", "ariaLabelledby", "disabled", "required", "enforceTagLimits", "freeInput", "readonlyInput", "maxTags", "minTags", "placeholder", "showTypeaheadOnClick", "tagDelimiters", "tagPattern", "tagTemplate", "tagClass", "validationErrors", "autocomplete", "createTag", "icon", "clearButton", "clearButtonAriaLabel", "autoCloseDropdown"], outputs: ["tagsChange", "inputChange", "tagAdding", "tagAdded", "tagInvalidated", "tagRemoving", "tagRemoved", "tagClick", "inputFocus", "inputBlur"], exportAs: ["ux-tag-input"] }, { kind: "component", type: TypeaheadComponent, selector: "ux-typeahead", inputs: ["id", "options", "filter", "open", "display", "key", "disabledOptions", "dropDirection", "maxHeight", "multiselectable", "ariaLabel", "openOnFilterChange", "pageSize", "selectFirst", "selectOnEnter", "loading", "loadingTemplate", "optionTemplate", "noOptionsTemplate", "active", "recentOptions", "recentOptionsMaxCount", "recentOptionsHeadingTemplate", "optionsHeadingTemplate"], outputs: ["openChange", "optionSelected", "highlightedChange", "highlightedElementChange", "recentOptionsChange"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-select, ux-combobox, ux-dropdown', providers: [SELECT_VALUE_ACCESSOR], host: {
'[class.ux-select-custom-icon]': '!!icon',
'[class.ux-select-disabled]': 'disabled',
}, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "@if (multiple) {\n <ux-tag-input\n #tagInput=\"ux-tag-input\"\n [id]=\"id + '-input'\"\n [tags]=\"_value$ | async\"\n (tagsChange)=\"_multipleOptionSelected($event)\"\n [ariaLabelledby]=\"ariaLabelledby\"\n [(input)]=\"input\"\n [required]=\"required\"\n (inputChange)=\"onInputChange($event)\"\n [ariaLabel]=\"ariaLabel\"\n [autocomplete]=\"autocomplete\"\n [addOnPaste]=\"false\"\n [disabled]=\"disabled\"\n [display]=\"display\"\n [freeInput]=\"false\"\n [placeholder]=\"placeholder || ''\"\n [tagTemplate]=\"tagTemplate\"\n (inputFocus)=\"onFocus()\"\n [showTypeaheadOnClick]=\"true\"\n [readonlyInput]=\"readonlyInput\"\n [icon]=\"icon\"\n [clearButton]=\"clearButton\"\n [autoCloseDropdown]=\"autoCloseDropdown\"\n [clearButtonAriaLabel]=\"clearButtonAriaLabel\"\n >\n <ux-typeahead\n #multipleTypeahead\n [ariaLabel]=\"listboxAriaLabel\"\n [id]=\"id + '-typeahead'\"\n [options]=\"options\"\n [filter]=\"filter$ | async\"\n [(open)]=\"dropdownOpen\"\n [display]=\"display\"\n [key]=\"key\"\n [disabledOptions]=\"_value$ | async\"\n [dropDirection]=\"dropDirection\"\n [maxHeight]=\"maxHeight\"\n [multiselectable]=\"true\"\n [pageSize]=\"pageSize\"\n [selectFirst]=\"true\"\n [loadingTemplate]=\"loadingTemplate\"\n [optionTemplate]=\"optionTemplate\"\n [noOptionsTemplate]=\"noOptionsTemplate\"\n [recentOptions]=\"recentOptions\"\n [recentOptionsMaxCount]=\"recentOptionsMaxCount\"\n (recentOptionsChange)=\"recentOptionsChange.emit($event)\"\n [optionsHeadingTemplate]=\"optionsHeadingTemplate\"\n [recentOptionsHeadingTemplate]=\"recentOptionsHeadingTemplate\"\n >\n </ux-typeahead>\n </ux-tag-input>\n}\n\n@if (!multiple) {\n <div class=\"ux-select-container\" [class.disabled]=\"disabled\" aria-haspopup=\"listbox\">\n <input\n #singleInput\n type=\"text\"\n [attr.id]=\"id + '-input'\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n class=\"form-control\"\n [required]=\"required\"\n [class.ux-tag-input-clear-inset]=\"clearButton && allowNull && _hasValue\"\n [attr.aria-activedescendant]=\"highlightedElement?.id\"\n aria-autocomplete=\"list\"\n role=\"combobox\"\n [attr.aria-controls]=\"id + '-typeahead'\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-expanded]=\"dropdownOpen\"\n [autocomplete]=\"autocomplete\"\n [(ngModel)]=\"input\"\n (ngModelChange)=\"onInputChange($event)\"\n [placeholder]=\"placeholder || ''\"\n [disabled]=\"disabled\"\n (click)=\"toggle()\"\n (focus)=\"onFocus()\"\n (blur)=\"inputBlurHandler()\"\n (keydown)=\"inputKeyHandler($event)\"\n [readonly]=\"readonlyInput\"\n />\n <div class=\"ux-select-icons\">\n @if (clearButton && allowNull && _hasValue) {\n <i\n uxFocusIndicator\n [attr.tabindex]=\"disabled ? -1 : 0\"\n [attr.aria-label]=\"clearButtonAriaLabel\"\n class=\"ux-select-icon ux-icon ux-icon-close ux-select-clear-icon\"\n (click)=\"clear(); $event.stopPropagation()\"\n (keydown.enter)=\"clear(); $event.stopPropagation()\"\n >\n </i>\n }\n @if (!icon) {\n <i\n class=\"ux-select-icon ux-icon ux-select-chevron-icon\"\n [class.ux-icon-up]=\"dropDirection === 'up'\"\n [class.ux-icon-down]=\"dropDirection === 'down'\"\n (click)=\"toggle(); $event.stopPropagation(); singleInput.focus()\"\n >\n </i>\n }\n @if (icon) {\n <div class=\"ux-custom-icon\">\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n </div>\n }\n </div>\n <ux-typeahead\n #singleTypeahead\n [ariaLabel]=\"listboxAriaLabel\"\n [id]=\"id + '-typeahead'\"\n [active]=\"_value$ | async\"\n [options]=\"options\"\n [filter]=\"filter$ | async\"\n [(open)]=\"dropdownOpen\"\n [display]=\"display\"\n [key]=\"key\"\n [dropDirection]=\"dropDirection\"\n [maxHeight]=\"maxHeight\"\n [multiselectable]=\"false\"\n [openOnFilterChange]=\"false\"\n [pageSize]=\"pageSize\"\n [selectFirst]=\"true\"\n [loadingTemplate]=\"loadingTemplate\"\n [optionTemplate]=\"optionTemplate\"\n [noOptionsTemplate]=\"noOptionsTemplate\"\n [recentOptions]=\"recentOptions\"\n [recentOptionsMaxCount]=\"recentOptionsMaxCount\"\n [optionsHeadingTemplate]=\"optionsHeadingTemplate\"\n [recentOptionsHeadingTemplate]=\"recentOptionsHeadingTemplate\"\n (optionSelected)=\"_singleOptionSelected($event)\"\n (highlightedElementChange)=\"highlightedElement = $event\"\n (recentOptionsChange)=\"recentOptionsChange.emit($any($event))\"\n >\n </ux-typeahead>\n </div>\n}\n" }]
}], propDecorators: { id: [{
type: Input
}, {
type: HostBinding,
args: ['attr.id']
}], value: [{
type: Input
}], input: [{
type: Input
}], dropdownOpen: [{
type: Input
}], options: [{
type: Input
}], display: [{
type: Input
}], key: [{
type: Input
}], allowNull: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaLabelledby: [{
type: Input
}], listboxAriaLabel: [{
type: Input
}], disabled: [{
type: Input
}], dropDirection: [{
type: Input
}], maxHeight: [{
type: Input
}], multiple: [{
type: Input
}], pageSize: [{
type: Input
}], placeholder: [{
type: Input
}], tagTemplate: [{
type: Input
}], optionsHeadingTemplate: [{
type: Input
}], recentOptionsHeadingTemplate: [{
type: Input
}], autocomplete: [{
type: Input
}], loadingTemplate: [{
type: Input
}], noOptionsTemplate: [{
type: Input
}], readonlyInput: [{
type: Input
}], clearButton: [{
type: Input
}], clearButtonAriaLabel: [{
type: Input
}], autoCloseDropdown: [{
type: Input
}], optionTemplate: [{
type: Input
}], recentOptions: [{
type: Input
}], recentOptionsMaxCount: [{
type: Input
}], required: [{
type: Input
}], filterDebounceTime: [{
type: Input
}], valueChange: [{
type: Output
}], inputChange: [{
type: Output
}], dropdownOpenChange: [{
type: Output
}], recentOptionsChange: [{
type: Output
}], icon: [{
type: ContentChild,
args: ['icon', { static: false }]
}], singleInput: [{
type: ViewChild,
args: ['singleInput', { static: false }]
}], tagInput: [{
type: ViewChild,
args: ['tagInput', { static: false }]
}], multipleTypeahead: [{
type: ViewChild,
args: ['multipleTypeahead', { static: false }]
}], singleTypeahead: [{
type: ViewChild,
args: ['singleTypeahead', { static: false }]
}] } });
class TagInputModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TagInputModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TagInputModule, declarations: [TagInputComponent], imports: [AccessibilityModule,
CommonModule,
FormsModule,
FocusIfModule,
IconModule,
TypeaheadModule], exports: [TagInputComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TagInputModule, imports: [AccessibilityModule,
CommonModule,
FormsModule,
FocusIfModule,
IconModule,
TypeaheadModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TagInputModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
FormsModule,
FocusIfModule,
IconModule,
TypeaheadModule,
],
exports: [TagInputComponent],
declarations: [TagInputComponent],
}]
}] });
class SelectModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SelectModule, declarations: [SelectComponent], imports: [AccessibilityModule,
CommonModule,
FormsModule,
InfiniteScrollModule,
TagInputModule,
TypeaheadModule,
PlatformModule], exports: [SelectComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectModule, imports: [AccessibilityModule,
CommonModule,
FormsModule,
InfiniteScrollModule,
TagInputModule,
TypeaheadModule,
PlatformModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
FormsModule,
InfiniteScrollModule,
TagInputModule,
TypeaheadModule,
PlatformModule,
],
exports: [SelectComponent],
declarations: [SelectComponent],
}]
}] });
class BaseSearchComponent {
constructor() {
this._searchBuilderService = inject(SearchBuilderService);
this._id = this._searchBuilderService.generateComponentId();
this._valid = true;
}
get id() {
return `ux-search-builder-search-component-${this._id}`;
}
/**
* Get the current value of the component
*/
get value() {
return this.context.value;
}
/**
* Set the current value of the component
*/
set value(value) {
this.context.value = value;
this._searchBuilderService.queryHasChanged();
// if value has been set perform validation
this.validate();
}
get valid() {
return this._valid;
}
set valid(valid) {
this._valid = valid;
this._searchBuilderService.setValid(this._id, valid);
}
/**
* Make sure we clean up after ourselves
*/
ngOnDestroy() {
this.valid = true;
}
/**
* Perform any required validation on the value
*/
validate() {
// if a custom validation function has been provided then use it
this.valid = this.config.validation ? this.config.validation(this, this.value) : true;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BaseSearchComponent, isStandalone: false, selector: "ux-base-search", ngImport: i0, template: '', isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseSearchComponent, decorators: [{
type: Component,
args: [{
selector: 'ux-base-search',
template: '',
standalone: false,
}]
}] });
class SearchDateRangeComponent extends BaseSearchComponent {
constructor() {
super(...arguments);
this.type = 'date-range';
}
get label() {
return this.config.label;
}
get from() {
// if value does not exist the set it
if (!this.value || !this.value.from) {
this.from = new Date();
}
// ensure that the from value is a date object
if (this.value.from instanceof Date === false) {
this.value.from = new Date(this.value.from);
}
return this.value.from;
}
set from(fromValue) {
// create new object based on the current value
const value = Object.assign({}, this.value);
// ensure that the from value is a date
if (fromValue instanceof Date === false) {
fromValue = new Date(fromValue);
}
// set the latest value
value.from = fromValue;
// update the value object while ensuring immutability
this.value = value;
}
get to() {
// if value does not exist the set it
if (!this.value || !this.value.to) {
this.to = new Date();
}
// ensure that the to value is a date object
if (this.value.to instanceof Date === false) {
this.value.to = new Date(this.value.to);
}
return this.value.to;
}
set to(toValue) {
// create new object based on the current value
const value = Object.assign({}, this.value);
// ensure that the to value is a date
if (toValue instanceof Date === false) {
toValue = new Date(toValue);
}
// set the latest value
value.to = toValue;
// update the value object while ensuring immutability
this.value = value;
}
get fromLabel() {
return this.config.fromLabel || 'From';
}
get toLabel() {
return this.config.toLabel || 'To';
}
get fromPlaceholder() {
return this.config.fromPlaceholder;
}
get toPlaceholder() {
return this.config.toPlaceholder;
}
get toDateInputAriaLabel() {
return this.config.toDateInputAriaLabel || 'Selected date';
}
get fromDateInputAriaLabel() {
return this.config.fromDateInputAriaLabel || 'Selected date';
}
/**
* Override the default validation
*/
validate() {
// check if there is a config validation function
if (this.config.validation) {
return super.validate();
}
// create copies of the dates so we can modify time value (to ignore it)
const from = new Date(this.value.from);
const to = new Date(this.value.to);
// set the time to the same so we dont compare it
from.setHours(0, 0, 0, 0);
to.setHours(0, 0, 0, 0);
// valid if the from date is less than or equal to the to date
this.valid = from <= to;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchDateRangeComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SearchDateRangeComponent, isStandalone: false, selector: "ux-search-date-range", usesInheritance: true, ngImport: i0, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<div class=\"row\">\n <div class=\"col-sm-12\">\n <div class=\"form-inline\" [class.has-error]=\"!valid\">\n <div class=\"form-group p-r-md\">\n <label class=\"form-label m-r-xs\">{{ fromLabel }}</label>\n\n <div class=\"input-group date m-nil\">\n <span class=\"input-group-addon p-r-xs\" tabindex=\"1\" (click)=\"fromPopover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n #fromPopover=\"ux-popover\"\n [ngModel]=\"from | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"fromPopoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n class=\"form-control\"\n [attr.aria-label]=\"fromDateInputAriaLabel\"\n [placeholder]=\"fromPlaceholder\"\n [focusIf]=\"focus\"\n />\n </div>\n </div>\n\n <div class=\"form-group p-r-xs\">\n <label class=\"form-label m-r-xs\">{{ toLabel }}</label>\n\n <div class=\"input-group date m-nil\">\n <span class=\"input-group-addon\" tabindex=\"1\" (click)=\"toPopover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n #toPopover=\"ux-popover\"\n [ngModel]=\"to | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"toPopoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n class=\"form-control\"\n [attr.aria-label]=\"toDateInputAriaLabel\"\n [placeholder]=\"toPlaceholder\"\n />\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<ng-template #fromPopoverTemplate>\n <ux-date-time-picker [(date)]=\"from\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n\n<ng-template #toPopoverTemplate>\n <ux-date-time-picker [(date)]=\"to\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n", dependencies: [{ kind: "component", type: DateTimePickerComponent, selector: "ux-date-time-picker", inputs: ["showDate", "showTime", "showTimezone", "showSeconds", "showMeridian", "showSpinners", "weekdays", "months", "monthsShort", "meridians", "nowBtnText", "showNowBtn", "timezones", "startOfWeek", "nowBtnAriaLabel", "date", "timezone", "min", "max"], outputs: ["dateChange", "timezoneChange"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: PopoverDirective, selector: "[uxPopover]", inputs: ["uxPopover", "popoverTitle", "popoverDisabled", "popoverClass", "popoverRole", "popoverContext", "popoverDelay", "showTriggers", "hideTriggers"], exportAs: ["ux-popover"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchDateRangeComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-date-range', standalone: false, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<div class=\"row\">\n <div class=\"col-sm-12\">\n <div class=\"form-inline\" [class.has-error]=\"!valid\">\n <div class=\"form-group p-r-md\">\n <label class=\"form-label m-r-xs\">{{ fromLabel }}</label>\n\n <div class=\"input-group date m-nil\">\n <span class=\"input-group-addon p-r-xs\" tabindex=\"1\" (click)=\"fromPopover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n #fromPopover=\"ux-popover\"\n [ngModel]=\"from | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"fromPopoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n class=\"form-control\"\n [attr.aria-label]=\"fromDateInputAriaLabel\"\n [placeholder]=\"fromPlaceholder\"\n [focusIf]=\"focus\"\n />\n </div>\n </div>\n\n <div class=\"form-group p-r-xs\">\n <label class=\"form-label m-r-xs\">{{ toLabel }}</label>\n\n <div class=\"input-group date m-nil\">\n <span class=\"input-group-addon\" tabindex=\"1\" (click)=\"toPopover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n #toPopover=\"ux-popover\"\n [ngModel]=\"to | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"toPopoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n class=\"form-control\"\n [attr.aria-label]=\"toDateInputAriaLabel\"\n [placeholder]=\"toPlaceholder\"\n />\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<ng-template #fromPopoverTemplate>\n <ux-date-time-picker [(date)]=\"from\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n\n<ng-template #toPopoverTemplate>\n <ux-date-time-picker [(date)]=\"to\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n" }]
}] });
class SearchDateComponent extends BaseSearchComponent {
constructor() {
super(...arguments);
this.type = 'date';
}
get label() {
return this.config.label;
}
get placeholder() {
return this.config.placeholder || 'Enter date';
}
get dateInputAriaLabel() {
return this.config.dateInputAriaLabel || 'Selected date';
}
ngOnInit() {
// by default set to the current date if not specified
if (!this.value) {
this.value = new Date();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchDateComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SearchDateComponent, isStandalone: false, selector: "ux-search-date", usesInheritance: true, ngImport: i0, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<div class=\"input-group date m-nil\">\n <span class=\"input-group-addon\" tabindex=\"1\" (click)=\"popover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n [attr.aria-label]=\"dateInputAriaLabel\"\n [placeholder]=\"placeholder\"\n #popover=\"ux-popover\"\n [ngModel]=\"value | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"popoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n [focusIf]=\"focus\"\n />\n</div>\n\n<ng-template #popoverTemplate>\n <ux-date-time-picker [(date)]=\"value\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n", dependencies: [{ kind: "component", type: DateTimePickerComponent, selector: "ux-date-time-picker", inputs: ["showDate", "showTime", "showTimezone", "showSeconds", "showMeridian", "showSpinners", "weekdays", "months", "monthsShort", "meridians", "nowBtnText", "showNowBtn", "timezones", "startOfWeek", "nowBtnAriaLabel", "date", "timezone", "min", "max"], outputs: ["dateChange", "timezoneChange"] }, { kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: PopoverDirective, selector: "[uxPopover]", inputs: ["uxPopover", "popoverTitle", "popoverDisabled", "popoverClass", "popoverRole", "popoverContext", "popoverDelay", "showTriggers", "hideTriggers"], exportAs: ["ux-popover"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchDateComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-date', standalone: false, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<div class=\"input-group date m-nil\">\n <span class=\"input-group-addon\" tabindex=\"1\" (click)=\"popover.show()\">\n <ux-icon name=\"calendar\"></ux-icon>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n [attr.aria-label]=\"dateInputAriaLabel\"\n [placeholder]=\"placeholder\"\n #popover=\"ux-popover\"\n [ngModel]=\"value | date: 'dd MMMM yyyy'\"\n [uxPopover]=\"popoverTemplate\"\n placement=\"bottom\"\n popoverClass=\"date-time-picker-popover\"\n [focusIf]=\"focus\"\n />\n</div>\n\n<ng-template #popoverTemplate>\n <ux-date-time-picker [(date)]=\"value\" [showTime]=\"false\"></ux-date-time-picker>\n</ng-template>\n" }]
}] });
class SearchSelectComponent extends BaseSearchComponent {
constructor() {
super(...arguments);
this.type = 'select';
}
/**
* Provide defaults for undefined properties
*/
get label() {
return this.config.label;
}
get options() {
return this.config.options || [];
}
get multiple() {
return this.config.multiple || false;
}
get placeholder() {
return this.config.placeholder || 'Select item';
}
get dropDirection() {
return this.config.dropDirection || 'down';
}
get allowNull() {
return this.config.allowNull || false;
}
get disabled() {
return this.config.disabled || false;
}
get maxHeight() {
return this.config.maxHeight || '250px';
}
get pageSize() {
return this.config.pageSize || 20;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SearchSelectComponent, isStandalone: false, selector: "ux-search-select", usesInheritance: true, ngImport: i0, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<ux-select\n [(value)]=\"value\"\n [options]=\"options\"\n [multiple]=\"multiple\"\n [placeholder]=\"placeholder\"\n [dropDirection]=\"dropDirection\"\n [pageSize]=\"pageSize\"\n [allowNull]=\"allowNull\"\n [disabled]=\"disabled\"\n [maxHeight]=\"maxHeight\"\n [key]=\"config.key\"\n [display]=\"config.display\"\n [loadingTemplate]=\"config.loadingTemplate\"\n [optionTemplate]=\"config.optionTemplate\"\n [noOptionsTemplate]=\"config.noOptionsTemplate\"\n [focusIf]=\"focus\"\n>\n</ux-select>\n", dependencies: [{ kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "component", type: SelectComponent, selector: "ux-select, ux-combobox, ux-dropdown", inputs: ["id", "value", "input", "dropdownOpen", "options", "display", "key", "allowNull", "ariaLabel", "ariaLabelledby", "listboxAriaLabel", "disabled", "dropDirection", "maxHeight", "multiple", "pageSize", "placeholder", "tagTemplate", "optionsHeadingTemplate", "recentOptionsHeadingTemplate", "autocomplete", "loadingTemplate", "noOptionsTemplate", "readonlyInput", "clearButton", "clearButtonAriaLabel", "autoCloseDropdown", "optionTemplate", "recentOptions", "recentOptionsMaxCount", "required", "filterDebounceTime"], outputs: ["valueChange", "inputChange", "dropdownOpenChange", "recentOptionsChange"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchSelectComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-select', standalone: false, template: "@if (label) {\n <label class=\"form-label\">{{ label }}</label>\n}\n\n<ux-select\n [(value)]=\"value\"\n [options]=\"options\"\n [multiple]=\"multiple\"\n [placeholder]=\"placeholder\"\n [dropDirection]=\"dropDirection\"\n [pageSize]=\"pageSize\"\n [allowNull]=\"allowNull\"\n [disabled]=\"disabled\"\n [maxHeight]=\"maxHeight\"\n [key]=\"config.key\"\n [display]=\"config.display\"\n [loadingTemplate]=\"config.loadingTemplate\"\n [optionTemplate]=\"config.optionTemplate\"\n [noOptionsTemplate]=\"config.noOptionsTemplate\"\n [focusIf]=\"focus\"\n>\n</ux-select>\n" }]
}] });
class SearchTextComponent extends BaseSearchComponent {
constructor() {
super(...arguments);
this.type = 'text';
}
get label() {
return this.config.label;
}
get placeholder() {
return this.config.placeholder || 'Enter text';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchTextComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SearchTextComponent, isStandalone: false, selector: "ux-search-text", usesInheritance: true, ngImport: i0, template: "@if (label) {\n <label class=\"form-label\" [attr.for]=\"id\">{{ label }}</label>\n}\n<input\n type=\"text\"\n [attr.id]=\"id\"\n class=\"form-control\"\n [placeholder]=\"placeholder\"\n [(ngModel)]=\"value\"\n [focusIf]=\"focus\"\n/>\n", dependencies: [{ kind: "directive", type: FocusIfDirective, selector: "[focusIf]", inputs: ["focusIfDelay", "focusIfScroll", "focusIf"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchTextComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-search-text', standalone: false, template: "@if (label) {\n <label class=\"form-label\" [attr.for]=\"id\">{{ label }}</label>\n}\n<input\n type=\"text\"\n [attr.id]=\"id\"\n class=\"form-control\"\n [placeholder]=\"placeholder\"\n [(ngModel)]=\"value\"\n [focusIf]=\"focus\"\n/>\n" }]
}] });
class SearchBuilderModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderModule, declarations: [SearchBuilderComponent,
SearchBuilderGroupComponent,
SearchTextComponent,
SearchDateComponent,
SearchDateRangeComponent,
SearchBuilderOutletDirective,
SearchSelectComponent,
BaseSearchComponent], imports: [AccessibilityModule,
CommonModule,
DateTimePickerModule,
FocusIfModule,
FormsModule,
IconModule,
PopoverModule,
SelectModule], exports: [SearchBuilderComponent, SearchBuilderGroupComponent, BaseSearchComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderModule, imports: [AccessibilityModule,
CommonModule,
DateTimePickerModule,
FocusIfModule,
FormsModule,
IconModule,
PopoverModule,
SelectModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SearchBuilderModule, decorators: [{
type: NgModule,
args: [{
imports: [
AccessibilityModule,
CommonModule,
DateTimePickerModule,
FocusIfModule,
FormsModule,
IconModule,
PopoverModule,
SelectModule,
],
exports: [SearchBuilderComponent, SearchBuilderGroupComponent, BaseSearchComponent],
declarations: [
SearchBuilderComponent,
SearchBuilderGroupComponent,
SearchTextComponent,
SearchDateComponent,
SearchDateRangeComponent,
SearchBuilderOutletDirective,
SearchSelectComponent,
BaseSearchComponent,
],
}]
}] });
class SelectionStrategy {
constructor(selectionService) {
this.selectionService = selectionService;
}
setSelectionService(selectionService) {
this.selectionService = selectionService;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
mousedown(event, data) { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
click(event, data) { }
// eslint-disable-next-line @typescript-eslint/no-empty-function
keydown(event, data) { }
/**
* Select the item - default behavior
*/
select(...data) {
this.selectionService.select(...data);
}
/**
* Replace the current selection with the list of items specified
*/
selectOnly(...data) {
this.selectionService.selectOnly(...data);
}
/**
* Toggle the item's selected state - default behavior
*/
toggle(...data) {
this.selectionService.toggle(...data);
}
/**
* Deselect the item - default behavior
*/
deselect(...data) {
this.selectionService.deselect(...data);
}
/**
* Select all items - default behavior
*/
selectAll() {
this.select(...this.selectionService.dataset);
}
/**
* Deselect all items - default behavior
*/
deselectAll() {
// call deselect on all items in the dataset
this.selectionService.deselectAll();
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
destroy() { }
}
class RowSelectionStrategy extends SelectionStrategy {
constructor() {
super(...arguments);
// store the most recently selected row
this._selection = { start: null, end: null };
}
/**
* By default on shift click the browser will highlight
* text. This looks bad and we don't want this to occur
*/
mousedown(event) {
event.preventDefault();
}
/**
* When a row is clicked we want to handle selection
*/
click(event, data) {
// determine which modifier keys are pressed
const { ctrlKey, shiftKey } = event;
// if the shift key is pressed we want to perform a multiple selection
if (shiftKey) {
return this.multipleSelect(data);
}
// if the control key is pressed we want to perform an additive toggle selection
if (ctrlKey) {
return this.toggle(data);
}
// perform a single selection where all other rows are deselected
this.singleSelect(data);
}
/**
* To support full keyboard control we need to support the following:
* 1. Arrow keys to navigate up and down
* 2. Spacebar to toggle selection
* 3. Shift + Arrow keys to multiple select
* 4. Ctrl + Arrow keys to allow retained selection and navigation
*/
keydown(event, data) {
switch (event.which) {
case UP_ARROW:
case DOWN_ARROW:
event.preventDefault();
this.navigate(event, data);
break;
case SPACE:
event.preventDefault();
this.selectionService.strategy.toggle(data);
// also activate the item
this.selectionService.activate(data);
break;
}
}
/**
* Override the standard toggle function to store or clear the
* most recently selected item
*/
toggle(data) {
super.toggle(data);
// store or clear the selection
this.selectionService.isSelected(data) ? this.setSelectionStart(data) : this.clearSelection();
}
/**
* Clear all other selected items and select only
* the most recently selected item
*/
singleSelect(data) {
// deselect all other rows if neither modifier key is pressed
this.deselectAll();
// select the current row
this.select(data);
// store the current item as the selection start
this.setSelectionStart(data);
}
/**
* Handle multiple selection:
* 1. If no start item selected - select it
* 2. If a start item has been selected - select all in between
* 3. If a start and end item have been selected clear the range and then select the new range
*/
multipleSelect(data) {
// if no selection currently exists then perform initial selection
if (!this._selection.start) {
// select the row
this.select(data);
// store the starting point
return this.setSelectionStart(data);
}
// if a multiple selection already took place - clear the previous selection
if (this._selection.start && this._selection.end) {
this.deselect(...this.getSelectedItems());
}
// set the new selection end point
this.setSelectionEnd(data);
// select all the items in the range
this.select(...this.getSelectedItems());
}
/**
* Set the selection start point. If there was previously a
* selection end point then clear it as this is a new selection
*/
setSelectionStart(data) {
this._selection.start = data;
this._selection.end = null;
// activate the item
this.selectionService.activate(data);
}
/**
* Set the selection end point
*/
setSelectionEnd(data) {
this._selection.end = data;
// activate the item
this.selectionService.activate(data);
}
/**
* Clear both start and end selection points
*/
clearSelection(deactivate = true) {
// reset the selected item
this._selection = { start: null, end: null };
// remove the current active item
if (deactivate) {
this.selectionService.deactivate();
}
}
/**
* Determine all the items affected by the current selection.
* Note that the end point may be above the start point so
* we need to account for this.
*/
getSelectedItems() {
// get the latest dataset
const { dataset } = this.selectionService;
// get the indexes of the start and end point
const startIdx = dataset.indexOf(this._selection.start);
const endIdx = dataset.indexOf(this._selection.end);
// get the region of the array that is selected - note the endIdx may be before the startIdx so account for this
return dataset.slice(Math.min(startIdx, endIdx), Math.max(startIdx, endIdx) + 1);
}
/**
* Activate the sibling item when arrow keys are pressed
*/
navigate(event, data) {
// determine which modifier keys are pressed
const { ctrlKey, shiftKey } = event;
// if no modifier keys are pressed then deselect all and clear the selection
if (!ctrlKey && !shiftKey) {
this.deselectAll();
this.clearSelection(false);
}
// activate the sibling - if the up arrow is pressed then navigate to the previous sibling
const sibling = this.selectionService.activateSibling(event.which === UP_ARROW);
// if the shift key is pressed then we also want to toggle the state if the item
if (shiftKey && sibling) {
// if there is no current selection start then select the current row
if (!this._selection.start) {
this.multipleSelect(data);
}
this.multipleSelect(sibling);
}
}
}
class RowAltSelectionStrategy extends RowSelectionStrategy {
keydown(event, data) {
switch (event.which) {
case UP_ARROW:
case DOWN_ARROW:
event.preventDefault();
this.handleCursorKey(event, data);
break;
case SPACE:
event.preventDefault();
this.selectionService.strategy.toggle(data);
break;
}
}
/**
* Select the sibling item when arrow keys are pressed
*/
handleCursorKey(event, data) {
// determine which modifier keys are pressed
const { ctrlKey, shiftKey } = event;
// if no modifier keys are pressed then deselect all and clear the selection
if (!ctrlKey && !shiftKey) {
this.deselectAll();
this.clearSelection(false);
}
if (ctrlKey) {
this.selectionService.activateSibling(event.which === UP_ARROW);
}
else {
const sibling = this.selectionService.getSibling(event.which === UP_ARROW);
this.multipleSelect(sibling ? sibling : data);
}
}
}
class SimpleSelectionStrategy extends SelectionStrategy {
/**
* When the item is clicked simply toggle the current selected state
*/
click(_event, data) {
this.toggle(data);
}
/**
* Add basic keyboard support for navigating
* and selecting/deselecting items
*/
keydown(event, data) {
switch (event.which) {
case UP_ARROW:
event.preventDefault();
this.selectionService.activateSibling(true);
return;
case DOWN_ARROW:
event.preventDefault();
this.selectionService.activateSibling(false);
return;
case SPACE:
event.preventDefault();
return this.toggle(data);
}
}
/**
* Override the standard toggle function to always activate the item
*/
toggle(data) {
super.toggle(data);
this.selectionService.activate(data);
}
}
class SelectionService {
constructor() {
/** The active selection strategy that defines how selections can be made */
this.strategy = new SimpleSelectionStrategy(this);
/** Define if selections can be performed on any items */
this.isEnabled = true;
/** Define if the mouse can be used to perform selections */
this.isClickEnabled = true;
/** Define if the keyboard can be used to perform selections */
this.isKeyboardEnabled = true;
/** Define the currently focused item */
this.focus$ = new BehaviorSubject(null);
/** Define the currently active item */
this.active$ = new BehaviorSubject(null);
/** Store the current list of selected items as an array */
this.selection$ = new BehaviorSubject([]);
/** Store the current set of selectable items */
this._dataset = [];
/** Store the selection strategy that should be destroyed */
this._strategyToDestroy = this.strategy;
/** Store the current selection in a set */
this._selection = new Set();
/** Store the current disabled items in a set */
this._disabled = new Set();
}
/** Store the current set of selectable items and ensure an item can be focused */
set dataset(dataset) {
this._dataset = dataset;
if (this._dataset.indexOf(this._active) === -1) {
this.setFirstItemFocusable();
}
}
/** Get the current set of selectable items */
get dataset() {
return this._dataset;
}
ngOnDestroy() {
// destroy the active strategy
if (this._strategyToDestroy) {
this._strategyToDestroy.destroy();
}
// complete all observables
this.focus$.complete();
this.active$.complete();
this.selection$.complete();
}
/**
* If the item is not currently selected then add it
* to the list of selected items
*/
select(...selections) {
// filter out any disabled items
selections = selections.filter(item => !this._disabled.has(item));
// add each selection to the set
selections.forEach(selection => this._selection.add(selection));
// propagate the changes
this.selectionHasMutated();
}
/**
* Deselect all currently selected items and replace with a new selection
*/
selectOnly(...selection) {
// filter out any disabled items
selection = selection.filter(item => !this._disabled.has(item));
// remove all currently selected items
this._selection.clear();
// select only the specified item
selection.forEach(item => this._selection.add(item));
// emit the changes
this.selectionHasMutated();
}
/**
* Remove an item from the list of selected items
*/
deselect(...selections) {
// remove each item from the set
selections.forEach(selection => this._selection.delete(selection));
// propagate the changes
this.selectionHasMutated();
}
/**
* Remove all items from the list of selected items
*/
deselectAll() {
// remove all items in the array
this.deselect(...this._dataset);
// clear the set in case any items have been removed from the DOM but are still selected
this._selection.clear();
}
/**
* Toggle the selected state of any specified items
*/
toggle(...selections) {
selections.forEach(selection => this.isSelected(selection) ? this.deselect(selection) : this.select(selection));
}
/**
* Determine whether or not a specific item is currently selected
*/
isSelected(data) {
return this._selection.has(data);
}
/**
* Return an observable specifically for notifying the subscriber
* only when the selection state of a specific object has changed
*/
getSelectionState(data) {
return this.selection$.pipe(map(() => this.isSelected(data)), distinctUntilChanged());
}
/**
* Define how selections should be performed.
* This allows us to use an strategy pattern to handle the various keyboard
* and mouse interactions while keeping each mode separated and
* easily extensible if we want to add more modes in future!
*/
setStrategy(mode) {
if (this._strategyToDestroy) {
// Destroy previous strategy if it was created internally
this._strategyToDestroy.destroy();
this._strategyToDestroy = null;
}
if (mode instanceof SelectionStrategy) {
// Custom strategy - pass in the service instance
this.strategy = mode;
this.strategy.setSelectionService(this);
}
else {
switch (mode.toLowerCase().trim()) {
case 'simple':
this.strategy = this._strategyToDestroy = new SimpleSelectionStrategy(this);
break;
case 'row':
this.strategy = this._strategyToDestroy = new RowSelectionStrategy(this);
break;
case 'row-alt':
this.strategy = this._strategyToDestroy = new RowAltSelectionStrategy(this);
break;
default:
throw new Error(`The selection mode '${mode}' does not exist. Valid modes are 'simple', 'row', or 'row-alt'.`);
}
}
}
/**
* Set the current active item
*/
activate(data) {
this._active = data;
this.active$.next(this._active);
}
/**
* Deactive all items
*/
deactivate() {
this._active = null;
this.active$.next(this._active);
}
/**
* Return the next or previous sibling of the current active item.
* @param previous If true, the previous sibling will be returned.
*/
getSibling(previous = false) {
// check if there is a current active item
if (!this._active) {
return;
}
// get the index of the current item
const idx = this.dataset.indexOf(this._active);
const target = this.dataset[previous ? idx - 1 : idx + 1];
return target;
}
/**
* Activate the sibling of the current active item.
* If previous is set to true the previous sibling will be activated
* rather than the next sibling. This function will also return the
* data of the newly activated sibling
*/
activateSibling(previous = false) {
const target = this.getSibling(previous);
// check if the target exists
if (target) {
this.activate(target);
}
return target;
}
setDisabled(disabled) {
// store the current disabled state
this.isEnabled = !disabled;
// clear any stateful data
this._active = null;
this.active$.next(this._active);
this._selection.clear();
// emit the selection change information
this.selectionHasMutated();
}
/** Store the disabled state of an item */
setItemDisabled(item, isDisabled) {
// update the internal list of disabled items
if (isDisabled && !this._disabled.has(item)) {
this._disabled.add(item);
}
else if (!isDisabled) {
this._disabled.delete(item);
}
}
selectionHasMutated() {
this.selection$.next(Array.from(this._selection));
}
setFirstItemFocusable() {
if (this._dataset.length > 0) {
this.focus$.next(this._dataset[0]);
this._active = this._dataset[0];
}
else {
this._active = null;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionService, decorators: [{
type: Injectable
}] });
class SelectListItemComponent {
set selected(isSelected) {
isSelected ? this._selection.select(this.data) : this._selection.deselect(this.data);
}
get selected() {
return this._selection.isSelected(this.data);
}
constructor() {
this._selection = inject(SelectionService);
this.elementRef = inject(ElementRef);
this.focusIndicatorService = inject(FocusIndicatorService);
this.tabindex = -1;
/** Unsubscribe from all subscriptions on destroy */
this._onDestroy = new Subject();
// create the focus indicator
this._focusIndicator = this.focusIndicatorService.monitor(this.elementRef.nativeElement);
this._selection.active$
.pipe(takeUntil(this._onDestroy), filter(data => data === this.data))
.subscribe(active => {
this._selection.focus$.next(active);
this.elementRef.nativeElement.focus();
});
// make this item tabbable or not based on the focused element
this._selection.focus$
.pipe(takeUntil(this._onDestroy), tick())
.subscribe(focused => (this.tabindex = focused === this.data ? 0 : -1));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this._focusIndicator.destroy();
}
onMouseDown(event) {
this._selection.strategy.mousedown(event, this.data);
}
onClick(event) {
this._selection.strategy.click(event, this.data);
}
onKeydown(event) {
this._selection.strategy.keydown(event, this.data);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: SelectListItemComponent, isStandalone: false, selector: "ux-select-list-item", inputs: { data: "data" }, host: { attributes: { "role": "listitem" }, listeners: { "mousedown": "onMouseDown($event)", "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "tabindex": "this.tabindex", "class.selected": "this.selected", "attr.aria-selected": "this.selected" } }, ngImport: i0, template: "<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-select-list-item', host: {
role: 'listitem',
}, standalone: false, template: "<ng-content></ng-content>\n" }]
}], ctorParameters: () => [], propDecorators: { data: [{
type: Input
}], tabindex: [{
type: HostBinding,
args: ['tabindex']
}], selected: [{
type: HostBinding,
args: ['class.selected']
}, {
type: HostBinding,
args: ['attr.aria-selected']
}], onMouseDown: [{
type: HostListener,
args: ['mousedown', ['$event']]
}], onClick: [{
type: HostListener,
args: ['click', ['$event']]
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class SelectionItemDirective {
/** Defines whether or not this item is currently selected. */
set selected(selected) {
selected ? this.select() : this.deselect();
}
get selected() {
return this._selected;
}
/** Determine whether or not this item can be selected */
set uxSelectionDisabled(isDisabled) {
// if this item was selected then deselect it
if (this._selected && isDisabled) {
this.deselect();
}
// inform the selection service of the disabled state
this._selectionService.setItemDisabled(this.uxSelectionItem, isDisabled);
// store the current disabled state
this._isDisabled = isDisabled;
}
/** Whether aria-selected is added to the host element */
set addAriaAttributes(value) {
this._addAriaAttributes = coerceBooleanProperty(value);
}
get addAriaAttributes() {
return this._addAriaAttributes;
}
get attrTabIndex() {
return this.tabindex !== null ? this.tabindex : this._managedTabIndex;
}
constructor() {
this.focusIndicatorService = inject(FocusIndicatorService);
this._selectionService = inject(SelectionService);
this._elementRef = inject(ElementRef);
this._managedFocusContainerService = inject(ManagedFocusContainerService);
this._changeDetector = inject(ChangeDetectorRef);
/** Defines the tab index of the row */
this.tabindex = null;
/** Defines whether or not this item is currently selected. */
this.selectedChange = new EventEmitter();
/** Store whether this item is the focusable item */
this.active = false;
/** Store the focused state of the element */
this.isFocused = false;
/** Store the current selected state */
this._selected = false;
/** Store the disabled state */
this._isDisabled = false;
/** Store the tab indexed if using the managed focus container */
this._managedTabIndex = -1;
/** Determine if there is a pending state change as we debounce before emitting */
this._hasPendingStateChange = false;
/** Store value for _addAriaAttributes */
this._addAriaAttributes = true;
/** Automatically unsubscribe when the component is destroyed */
this._onDestroy = new Subject();
this._focusIndicator = this.focusIndicatorService.monitor(this._elementRef.nativeElement);
}
ngOnInit() {
// if there is no associated data then throw an error
if (!this.uxSelectionItem) {
throw new Error('The uxSelectionItem directive must have data associated with it.');
}
// subscribe to changes to the active state
this._selectionService.active$
.pipe(takeUntil(this._onDestroy), map(active => active === this.uxSelectionItem))
.subscribe(active => {
// store the focus state
this.active = active;
// if it is active then focus the element
if (active === true) {
this._selectionService.focus$.next(this.uxSelectionItem);
this._elementRef.nativeElement.focus();
}
});
// Subscribe to changes to the focus target
// This is mostly the same as active$, except that it has an initial value of the first item in the collection.
this._selectionService.focus$.pipe(takeUntil(this._onDestroy)).subscribe(focusTarget => {
this._managedTabIndex = focusTarget === this.uxSelectionItem ? 0 : -1;
});
// Watch for focus within the container element and manage tabindex of descendants
this._managedFocusContainerService.register(this._elementRef.nativeElement, this);
// Listen for changes to the focus state and apply the appropriate class
this._focusIndicator.origin$
.pipe(map(origin => origin === 'keyboard'), takeUntil(this._onDestroy))
.subscribe(isFocused => {
this.isFocused = isFocused;
this._changeDetector.markForCheck();
});
}
ngOnChanges(changes) {
if (changes.uxSelectionItem) {
this.updateSelectionItem();
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this._focusIndicator.destroy();
this._managedFocusContainerService.unregister(this._elementRef.nativeElement, this);
}
click(event) {
if (!this._isDisabled &&
!this._hasPendingStateChange &&
this._selectionService.isEnabled &&
this._selectionService.isClickEnabled) {
this._selectionService.strategy.click(event, this.uxSelectionItem);
}
}
mousedown(event) {
if (!this._isDisabled &&
this._selectionService.isEnabled &&
this._selectionService.isClickEnabled) {
this._selectionService.strategy.mousedown(event, this.uxSelectionItem);
}
}
keydown(event) {
// if the space key (selection key) is pressed and we are disabled then we should block
// the event from propagating. However if this is a key such as arrow presses then we do
// still want this to propagate to allow keyboard navigation for accessibility purposes.
const isDisabled = this._isDisabled && event.keyCode === SPACE;
if (!isDisabled &&
this._selectionService.isEnabled &&
this._selectionService.isKeyboardEnabled) {
this._selectionService.strategy.keydown(event, this.uxSelectionItem);
}
}
focus() {
// If tabbed to from outside the component, activate.
if (this._selectionService.active$.getValue() !== this.uxSelectionItem) {
this._selectionService.activate(this.uxSelectionItem);
}
}
/**
* Select this item using the current strategy
*/
select() {
if (!this._isDisabled && this._selectionService.isEnabled) {
this._selectionService.strategy.select(this.uxSelectionItem);
}
}
/**
* Deselect this item using the current strategy
*/
deselect() {
if (!this._isDisabled && this._selectionService.isEnabled) {
this._selectionService.strategy.deselect(this.uxSelectionItem);
}
}
updateSelectionItem() {
if (this._selectionStateSubscription) {
this._selectionStateSubscription.unsubscribe();
}
// subscribe to selection changes on this item (don't emit the initial value)
this._selectionStateSubscription = this._selectionService
.getSelectionState(this.uxSelectionItem)
.pipe(skip(1), tap(() => (this._hasPendingStateChange = true)), debounceTime(0), takeUntil(this._onDestroy))
.subscribe(selected => {
this._hasPendingStateChange = false;
if (this._selected === selected) {
return;
}
// store the selected state
this._selected = selected;
// emit the selected state
this.selectedChange.emit(selected);
this._changeDetector.markForCheck();
});
this._selected = this._selectionService.isSelected(this.uxSelectionItem);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SelectionItemDirective, isStandalone: false, selector: "[uxSelectionItem]", inputs: { uxSelectionItem: "uxSelectionItem", selected: "selected", tabindex: "tabindex", uxSelectionDisabled: "uxSelectionDisabled", addAriaAttributes: "addAriaAttributes" }, outputs: { selectedChange: "selectedChange" }, host: { listeners: { "click": "click($event)", "mousedown": "mousedown($event)", "keydown": "keydown($event)", "focus": "focus()" }, properties: { "attr.aria-selected": "addAriaAttributes ? selected : null", "class.ux-selection-selected": "this.selected", "class.ux-selection-focused": "this.isFocused", "attr.tabindex": "this.attrTabIndex" } }, exportAs: ["ux-selection-item"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxSelectionItem]',
exportAs: 'ux-selection-item',
host: {
'[attr.aria-selected]': 'addAriaAttributes ? selected : null',
},
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxSelectionItem: [{
type: Input
}], selected: [{
type: Input
}, {
type: HostBinding,
args: ['class.ux-selection-selected']
}], tabindex: [{
type: Input
}], uxSelectionDisabled: [{
type: Input
}], addAriaAttributes: [{
type: Input
}], selectedChange: [{
type: Output
}], isFocused: [{
type: HostBinding,
args: ['class.ux-selection-focused']
}], attrTabIndex: [{
type: HostBinding,
args: ['attr.tabindex']
}], click: [{
type: HostListener,
args: ['click', ['$event']]
}], mousedown: [{
type: HostListener,
args: ['mousedown', ['$event']]
}], keydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}], focus: [{
type: HostListener,
args: ['focus']
}] } });
class SelectionDirective {
/** Defines the items that should be selected. */
set uxSelection(items) {
this._lastSelection = items;
this._selectionService.selectOnly(...items);
}
/** Can be used to enabled/disable selection behavior. */
set disabled(disabled) {
this._selectionService.setDisabled(disabled);
}
/**
* Defines the selection behavior. Alternatively, custom selection behavior can be defined by defining a
* class which extends SelectionStrategy, and providing an instance of the custom class to this property.
* See below for details of the SelectionStrategy class.
*/
set mode(mode) {
this._selectionService.setStrategy(mode);
}
/**
* Can be used to enable/disable click selection on items. This can be used to manually control the selection of an item,
* for example, binding the selection state to a checkbox.
*/
set clickSelection(isClickEnabled) {
this._selectionService.isClickEnabled = isClickEnabled;
}
/** Can be used to enable/disable keyboard navigation on items. Use this if you wish to provide custom keyboard controls for selection. */
set keyboardSelection(isKeyboardEnabled) {
this._selectionService.isKeyboardEnabled = isKeyboardEnabled;
}
/**
* The full set of selection items.
* Only needed if the full set of `uxSelectionItem`s is not available, e.g. within a virtual scroll container.
*/
set selectionItems(value) {
this._hasExplicitDataset = !!value;
if (value) {
this._selectionService.dataset = value;
}
}
constructor() {
this._selectionService = inject(SelectionService);
this._cdRef = inject(ChangeDetectorRef);
/** The tabstop of the selection outer element */
this.tabindex = null;
/** This event will be triggered when there is a change to the selected items. It will contain an array of the currently selected items. */
this.uxSelectionChange = new EventEmitter();
/** Unsubscribe from all observables on component destroy */
this._onDestroy = new Subject();
/** Store the previous selection so we don't emit more than we have to */
this._lastSelection = [];
/** Whether a value has been provided to the `selectionItems` input. */
this._hasExplicitDataset = false;
this._selectionService.selection$
.pipe(debounceTime(0), takeUntil(this._onDestroy))
.subscribe(items => {
if (this.isSelectionChanged(items)) {
this.uxSelectionChange.emit(items);
}
// store the most recent selection
this._lastSelection = [...items];
});
}
ngAfterContentInit() {
// provide the initial list of selection items
this.update();
// if the list changes then inform the service
this.items.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => this.update());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Update the dataset to reflect the latest selection items
*/
update() {
// Capture the set of data items from the ContentChildren, unless an explicit value has been provided.
if (!this._hasExplicitDataset) {
this._selectionService.dataset = this.items.map(item => item.uxSelectionItem);
}
// Make sure that a tab target has been defined so that the component can be tabbed to.
if (this._selectionService.focus$.getValue() === null &&
this._selectionService.dataset.length > 0) {
this._selectionService.focus$.next(this._selectionService.dataset[0]);
}
// The above could trigger a change in the computed tabindex for selection items
this._cdRef.detectChanges();
}
/**
* Select all the items in the list
*/
selectAll() {
if (this._selectionService.isEnabled) {
this._selectionService.strategy.selectAll();
}
}
/**
* Deselect all currently selected items
*/
deselectAll() {
if (this._selectionService.isEnabled) {
this._selectionService.strategy.deselectAll();
}
}
/**
* Determine if the previous selection is the same as the current selection
*/
isSelectionChanged(selection) {
// fast, efficient check, if length is different they must have changed
if ((!this._lastSelection && selection) || this._lastSelection.length !== selection.length) {
return true;
}
// if both arrays have 0 items then they have not changed
if (this._lastSelection.length === 0 && selection.length === 0) {
return false;
}
// otherwise do a check on each item
return !this._lastSelection.every(item => selection.indexOf(item) !== -1);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SelectionDirective, isStandalone: false, selector: "[uxSelection]", inputs: { uxSelection: "uxSelection", disabled: "disabled", mode: "mode", clickSelection: "clickSelection", keyboardSelection: "keyboardSelection", selectionItems: "selectionItems", tabindex: "tabindex" }, outputs: { uxSelectionChange: "uxSelectionChange" }, host: { properties: { "attr.tabindex": "this.tabindex" } }, providers: [SelectionService], queries: [{ propertyName: "items", predicate: SelectionItemDirective }], exportAs: ["ux-selection"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxSelection]',
exportAs: 'ux-selection',
providers: [SelectionService],
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxSelection: [{
type: Input
}], disabled: [{
type: Input
}], mode: [{
type: Input
}], clickSelection: [{
type: Input
}], keyboardSelection: [{
type: Input
}], selectionItems: [{
type: Input
}], tabindex: [{
type: Input
}, {
type: HostBinding,
args: ['attr.tabindex']
}], uxSelectionChange: [{
type: Output
}], items: [{
type: ContentChildren,
args: [SelectionItemDirective]
}] } });
class SelectionModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SelectionModule, declarations: [SelectionDirective, SelectionItemDirective], imports: [CommonModule], exports: [SelectionDirective, SelectionItemDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectionModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule],
declarations: [SelectionDirective, SelectionItemDirective],
exports: [SelectionDirective, SelectionItemDirective],
}]
}] });
class MultipleSelectListStrategy extends SelectionStrategy {
/** Prevent the browser from highlighting text on shift click */
mousedown(event) {
event.preventDefault();
}
click(event, data) {
// activate the clicked item
this.selectionService.activate(data);
// if the shift key is pressed we want to perform a multiple selection
if (event.shiftKey) {
return this.multipleSelect(data);
}
// otherwise perform a single toggle selection
if (this.selectionService.isSelected(data)) {
this.deselect(data);
this._lastSelection = null;
}
else {
this.select(data);
this._lastSelection = data;
}
}
keydown(event, data) {
switch (event.which) {
case UP_ARROW: {
event.preventDefault();
const sibling = this.selectionService.activateSibling(true);
if (event.shiftKey) {
this.select(data, sibling);
this._lastSelection = sibling;
}
break;
}
case DOWN_ARROW: {
event.preventDefault();
const sibling = this.selectionService.activateSibling(false);
if (event.shiftKey) {
this.select(data, sibling);
this._lastSelection = sibling;
}
break;
}
case SPACE:
case ENTER:
event.preventDefault();
this.toggle(data);
this._lastSelection = this.selectionService.isSelected(data) ? data : null;
break;
}
}
multipleSelect(data) {
// if there is no start item selected
if (!this._lastSelection) {
this.select(data);
this._lastSelection = data;
return;
}
// if there already is a start item then find the items in the range
this.select(...this.getSelectedItems(this._lastSelection, data));
// store the selection end point
this._lastSelection = data;
}
getSelectedItems(start, end) {
// get the latest dataset
const { dataset } = this.selectionService;
// get the indexes of the start and end point
const startIdx = dataset.indexOf(start);
const endIdx = dataset.indexOf(end);
// get the region of the array that is selected - note the endIdx may be before the startIdx so account for this
return dataset.slice(Math.min(startIdx, endIdx), Math.max(startIdx, endIdx) + 1);
}
}
class SingleSelectListStrategy extends SelectionStrategy {
click(_event, data) {
// activate the clicked item
this.selectionService.activate(data);
// toggle the selected state of the item
if (!this.selectionService.isSelected(data)) {
this.selectOnly(data);
}
else {
this.deselect(data);
}
}
keydown(event, data) {
switch (event.which) {
case UP_ARROW: {
event.preventDefault();
this.selectionService.activateSibling(true);
break;
}
case DOWN_ARROW: {
event.preventDefault();
this.selectionService.activateSibling(false);
break;
}
case SPACE:
case ENTER:
event.preventDefault();
this.click(null, data);
break;
}
}
}
class SelectListComponent {
/** Determine if we allow multiple items to be selected */
set multiple(multiple) {
this._selection.strategy.deselectAll();
this._selection.setStrategy(multiple ? new MultipleSelectListStrategy() : new SingleSelectListStrategy());
}
/** Set the selected items */
set selected(selected) {
// if the selection entered is the same as the current selection then do nothing
if (this._selection.selection$.value === selected) {
return;
}
// if selected is an array and has not items and there are no items currently selected also do nothing
if (Array.isArray(selected) &&
selected.length === 0 &&
this._selection.selection$.value.length === 0) {
return;
}
// select only the specified items
if (Array.isArray(selected)) {
this._selection.selectOnly(...selected);
}
else {
this._selection.selectOnly(selected);
}
}
constructor() {
this._selection = inject(SelectionService);
/** Emit when the selection changes */
this.selectedChange = new EventEmitter();
/** Automatically unsubscribe all observables */
this._onDestroy = new Subject();
// set the selection strategy to single by default
this._selection.setStrategy(new SingleSelectListStrategy());
// emit the selection changes when they occur
this._selection.selection$
.pipe(takeUntil(this._onDestroy))
.subscribe(selection => this.selectedChange.emit(selection));
}
ngAfterContentInit() {
// supply the initial item set
this._selection.dataset = this.items.map(item => item.data);
// if the item set changes update the list
this.items.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(() => (this._selection.dataset = this.items.map(item => item.data)));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: SelectListComponent, isStandalone: false, selector: "ux-select-list", inputs: { multiple: "multiple", selected: "selected" }, outputs: { selectedChange: "selectedChange" }, host: { attributes: { "role": "list" } }, providers: [SelectionService], queries: [{ propertyName: "items", predicate: SelectListItemComponent }], ngImport: i0, template: "<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-select-list', providers: [SelectionService], host: {
role: 'list',
}, standalone: false, template: "<ng-content></ng-content>\n" }]
}], ctorParameters: () => [], propDecorators: { multiple: [{
type: Input
}], selected: [{
type: Input
}], selectedChange: [{
type: Output
}], items: [{
type: ContentChildren,
args: [SelectListItemComponent]
}] } });
class SelectListModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SelectListModule, declarations: [SelectListComponent, SelectListItemComponent], imports: [AccessibilityModule], exports: [SelectListComponent, SelectListItemComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListModule, imports: [AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SelectListModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule],
declarations: [SelectListComponent, SelectListItemComponent],
exports: [SelectListComponent, SelectListItemComponent],
}]
}] });
class SidePanelCloseDirective {
constructor() {
this._service = inject(SidePanelService);
this._focusOrigin = inject(FocusIndicatorOriginService);
}
onClick(event) {
// determine the correct origin for the trigger event
this._focusOrigin.setOrigin(isKeyboardTrigger(event) ? 'keyboard' : 'mouse');
// close the side panel menu
this._service.close();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelCloseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SidePanelCloseDirective, isStandalone: false, selector: "[uxSidePanelClose]", host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelCloseDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxSidePanelClose]',
standalone: false,
}]
}], propDecorators: { onClick: [{
type: HostListener,
args: ['click', ['$event']]
}] } });
const EXPORTS$1 = [SidePanelComponent, SidePanelCloseDirective];
class SidePanelModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SidePanelModule, declarations: [SidePanelComponent, SidePanelCloseDirective], imports: [AccessibilityModule, CommonModule, A11yModule, FocusIfModule], exports: [SidePanelComponent, SidePanelCloseDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelModule, imports: [AccessibilityModule, CommonModule, A11yModule, FocusIfModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SidePanelModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, A11yModule, FocusIfModule],
exports: EXPORTS$1,
declarations: EXPORTS$1,
}]
}] });
class SparkComponent {
constructor() {
this._colorService = inject(ColorService);
this.values = [];
this.barHeight = 10;
this._theme = 'primary';
this._barColor = [];
}
set theme(value) {
this._theme = this._colorService.resolveColorName(value);
}
get theme() {
return this._theme;
}
set trackColor(value) {
this._trackColor = this._colorService.resolve(value);
}
get trackColor() {
return this._trackColor;
}
set barColor(value) {
if (Array.isArray(value)) {
this._barColor = value.map(color => this._colorService.resolve(color));
}
else {
this._barColor = [this._colorService.resolve(value)];
}
}
get barColor() {
return this._barColor;
}
set value(value) {
// ensure 'value' is an array at this point
const values = Array.isArray(value) ? value : [value];
// get the total value of all lines
const total = Math.max(values.reduce((previous, current) => previous + current, 0), 100);
// figure out the percentages for each spark line
this.values = values.map(val => (val / total) * 100);
}
get value() {
return this.values;
}
/**
* Get the aria label for the spark chart
*/
getAriaLabel() {
if (!Array.isArray(this.ariaLabel)) {
return this.ariaLabel || this.tooltip;
}
else {
return this.ariaLabel.join(', ');
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SparkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: SparkComponent, isStandalone: false, selector: "ux-spark", inputs: { barHeight: "barHeight", inlineLabel: "inlineLabel", topLeftLabel: "topLeftLabel", topRightLabel: "topRightLabel", bottomLeftLabel: "bottomLeftLabel", bottomRightLabel: "bottomRightLabel", tooltip: "tooltip", ariaLabel: ["aria-label", "ariaLabel"], ariaDescription: ["aria-description", "ariaDescription"], theme: "theme", trackColor: "trackColor", barColor: "barColor", value: "value" }, ngImport: i0, template: "<!-- Inline Spark Chart -->\n@if (inlineLabel) {\n <div class=\"ux-spark-inline-label-container\">\n <div class=\"ux-spark-inline-label-left\" [innerHtml]=\"inlineLabel\"></div>\n <div class=\"ux-spark-line\">\n @if (topLeftLabel || topRightLabel) {\n <div class=\"ux-spark-top-container\">\n @if (topLeftLabel) {\n <div class=\"ux-spark-label-top-left\" [innerHtml]=\"topLeftLabel\"></div>\n }\n @if (topRightLabel) {\n <div class=\"ux-spark-label-top-right\" [innerHtml]=\"topRightLabel\"></div>\n }\n </div>\n }\n <div\n role=\"progressbar\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-description]=\"ariaDescription\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-valuenow]=\"values.length === 1 ? values[0] : undefined\"\n class=\"ux-spark ux-inline ux-spark-theme-{{ theme }}\"\n [style.height.px]=\"barHeight\"\n [style.backgroundColor]=\"trackColor\"\n [uxTooltip]=\"tooltip\"\n >\n @for (line of values; track line; let idx = $index) {\n <div\n class=\"ux-spark-bar\"\n [style.width.%]=\"line\"\n [style.backgroundColor]=\"barColor[idx]\"\n ></div>\n }\n </div>\n @if (bottomLeftLabel || bottomRightLabel) {\n <div class=\"ux-spark-bottom-container\">\n @if (bottomLeftLabel) {\n <div class=\"ux-spark-label-bottom-left\" [innerHtml]=\"bottomLeftLabel\"></div>\n }\n @if (bottomRightLabel) {\n <div class=\"ux-spark-label-bottom-right\" [innerHtml]=\"bottomRightLabel\"></div>\n }\n </div>\n }\n </div>\n </div>\n}\n\n<!-- End Inline Spark Chart -->\n\n<!-- Non Inline Spark Chart -->\n@if (!inlineLabel) {\n <div>\n @if (topLeftLabel || topRightLabel) {\n <div class=\"ux-spark-top-container\">\n @if (topLeftLabel) {\n <div class=\"ux-spark-label-top-left\" [innerHtml]=\"topLeftLabel\"></div>\n }\n @if (topRightLabel) {\n <div class=\"ux-spark-label-top-right\" [innerHtml]=\"topRightLabel\"></div>\n }\n </div>\n }\n <div\n role=\"progressbar\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-description]=\"ariaDescription\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-valuenow]=\"values.length === 1 ? values[0] : undefined\"\n class=\"ux-spark ux-spark-theme-{{ theme }}\"\n [class.ux-spark-multi-value]=\"values.length > 1\"\n [style.height.px]=\"barHeight\"\n [style.backgroundColor]=\"trackColor\"\n [uxTooltip]=\"tooltip\"\n >\n @for (line of values; track line; let idx = $index) {\n <div\n class=\"ux-spark-bar\"\n [style.width.%]=\"line\"\n [style.backgroundColor]=\"barColor[idx]\"\n ></div>\n }\n </div>\n @if (bottomLeftLabel || bottomRightLabel) {\n <div class=\"ux-spark-bottom-container\">\n @if (bottomLeftLabel) {\n <div class=\"ux-spark-label-bottom-left\" [innerHtml]=\"bottomLeftLabel\"></div>\n }\n @if (bottomRightLabel) {\n <div class=\"ux-spark-label-bottom-right\" [innerHtml]=\"bottomRightLabel\"></div>\n }\n </div>\n }\n </div>\n}\n\n<!-- End Non Inline Spark Chart -->\n", dependencies: [{ kind: "directive", type: TooltipDirective, selector: "[uxTooltip]", inputs: ["uxTooltip", "tooltipDisabled", "tooltipClass", "tooltipRole", "tooltipContext", "tooltipDelay", "isOpen", "placement", "fallbackPlacement", "alignment", "showTriggers", "hideTriggers"], outputs: ["shown", "hidden", "isOpenChange"], exportAs: ["ux-tooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SparkComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-spark', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Inline Spark Chart -->\n@if (inlineLabel) {\n <div class=\"ux-spark-inline-label-container\">\n <div class=\"ux-spark-inline-label-left\" [innerHtml]=\"inlineLabel\"></div>\n <div class=\"ux-spark-line\">\n @if (topLeftLabel || topRightLabel) {\n <div class=\"ux-spark-top-container\">\n @if (topLeftLabel) {\n <div class=\"ux-spark-label-top-left\" [innerHtml]=\"topLeftLabel\"></div>\n }\n @if (topRightLabel) {\n <div class=\"ux-spark-label-top-right\" [innerHtml]=\"topRightLabel\"></div>\n }\n </div>\n }\n <div\n role=\"progressbar\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-description]=\"ariaDescription\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-valuenow]=\"values.length === 1 ? values[0] : undefined\"\n class=\"ux-spark ux-inline ux-spark-theme-{{ theme }}\"\n [style.height.px]=\"barHeight\"\n [style.backgroundColor]=\"trackColor\"\n [uxTooltip]=\"tooltip\"\n >\n @for (line of values; track line; let idx = $index) {\n <div\n class=\"ux-spark-bar\"\n [style.width.%]=\"line\"\n [style.backgroundColor]=\"barColor[idx]\"\n ></div>\n }\n </div>\n @if (bottomLeftLabel || bottomRightLabel) {\n <div class=\"ux-spark-bottom-container\">\n @if (bottomLeftLabel) {\n <div class=\"ux-spark-label-bottom-left\" [innerHtml]=\"bottomLeftLabel\"></div>\n }\n @if (bottomRightLabel) {\n <div class=\"ux-spark-label-bottom-right\" [innerHtml]=\"bottomRightLabel\"></div>\n }\n </div>\n }\n </div>\n </div>\n}\n\n<!-- End Inline Spark Chart -->\n\n<!-- Non Inline Spark Chart -->\n@if (!inlineLabel) {\n <div>\n @if (topLeftLabel || topRightLabel) {\n <div class=\"ux-spark-top-container\">\n @if (topLeftLabel) {\n <div class=\"ux-spark-label-top-left\" [innerHtml]=\"topLeftLabel\"></div>\n }\n @if (topRightLabel) {\n <div class=\"ux-spark-label-top-right\" [innerHtml]=\"topRightLabel\"></div>\n }\n </div>\n }\n <div\n role=\"progressbar\"\n [attr.aria-label]=\"getAriaLabel()\"\n [attr.aria-description]=\"ariaDescription\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n [attr.aria-valuenow]=\"values.length === 1 ? values[0] : undefined\"\n class=\"ux-spark ux-spark-theme-{{ theme }}\"\n [class.ux-spark-multi-value]=\"values.length > 1\"\n [style.height.px]=\"barHeight\"\n [style.backgroundColor]=\"trackColor\"\n [uxTooltip]=\"tooltip\"\n >\n @for (line of values; track line; let idx = $index) {\n <div\n class=\"ux-spark-bar\"\n [style.width.%]=\"line\"\n [style.backgroundColor]=\"barColor[idx]\"\n ></div>\n }\n </div>\n @if (bottomLeftLabel || bottomRightLabel) {\n <div class=\"ux-spark-bottom-container\">\n @if (bottomLeftLabel) {\n <div class=\"ux-spark-label-bottom-left\" [innerHtml]=\"bottomLeftLabel\"></div>\n }\n @if (bottomRightLabel) {\n <div class=\"ux-spark-label-bottom-right\" [innerHtml]=\"bottomRightLabel\"></div>\n }\n </div>\n }\n </div>\n}\n\n<!-- End Non Inline Spark Chart -->\n" }]
}], propDecorators: { barHeight: [{
type: Input
}], inlineLabel: [{
type: Input
}], topLeftLabel: [{
type: Input
}], topRightLabel: [{
type: Input
}], bottomLeftLabel: [{
type: Input
}], bottomRightLabel: [{
type: Input
}], tooltip: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaDescription: [{
type: Input,
args: ['aria-description']
}], theme: [{
type: Input
}], trackColor: [{
type: Input
}], barColor: [{
type: Input
}], value: [{
type: Input
}] } });
class SparkModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SparkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SparkModule, declarations: [SparkComponent], imports: [CommonModule, ColorServiceModule, TooltipModule], exports: [SparkComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SparkModule, imports: [CommonModule, ColorServiceModule, TooltipModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SparkModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, ColorServiceModule, TooltipModule],
exports: [SparkComponent],
declarations: [SparkComponent],
}]
}] });
function isColumnPickerGroupItem(column) {
return column.name !== undefined;
}
class ColumnPickerService {
getDeselectedColumnsInPresentationOrder(deselected, sort) {
let columns;
if (sort) {
const normalizedColumns = this.normalizeColumns(deselected);
normalizedColumns.sort(sort);
columns = this.denormalizeColumns(normalizedColumns, deselected);
}
else {
const grouped = deselected.filter(column => isColumnPickerGroupItem(column) && column.group !== undefined);
columns = [...grouped, ...deselected.filter(column => grouped.indexOf(column) === -1)];
}
return columns;
}
createTreeData(columns) {
const treeData = [];
const groupedColumns = columns.filter(column => isColumnPickerGroupItem(column) && column.group !== undefined);
columns.forEach((column) => {
if (groupedColumns.indexOf(column) !== -1 && isColumnPickerGroupItem(column)) {
const groupNode = this.createOrFindGroupNode(column, treeData);
groupNode.children.push(column.name);
this.createColumnTreeNode(column, treeData, 1);
}
else if (!isColumnPickerGroupItem(column)) {
this.createColumnTreeNode(column, treeData, 0);
}
});
return treeData;
}
normalizeColumns(columns) {
return columns.map(column => ({
name: isColumnPickerGroupItem(column) ? column.name : column,
group: isColumnPickerGroupItem(column) ? column.group : undefined,
}));
}
denormalizeColumns(normalizedColumns, originalColumns) {
return normalizedColumns.map(normalized => {
const original = originalColumns.find(originalColumn => {
if (isColumnPickerGroupItem(originalColumn)) {
return (originalColumn.group === normalized.group && originalColumn.name === normalized.name);
}
return false;
});
// Anything not found in the original array must have been normalized from a string
return original ? original : normalized.name;
});
}
createOrFindGroupNode(column, treeData) {
let groupNode = treeData.find(node => node.name === column.group && node.expandable);
if (!groupNode) {
groupNode = this.createGroupTreeNode(column, treeData);
}
return groupNode;
}
createGroupTreeNode(column, treeData) {
treeData.push({
name: column.group,
level: 0,
expandable: true,
isExpanded: this.isGroupTreeNodeExpanded(column.group),
column,
children: [],
});
return treeData[treeData.length - 1];
}
createColumnTreeNode(column, treeData, level) {
treeData.push({
name: isColumnPickerGroupItem(column) ? column.name : column,
level,
expandable: false,
column,
});
}
isGroupTreeNodeExpanded(groupName) {
const groupTreeNode = this.groups.find(group => group.name === groupName);
return groupTreeNode ? groupTreeNode.expanded : false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnPickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnPickerService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnPickerService, decorators: [{
type: Injectable
}] });
let uniqueId$1 = 0;
class ColumnPickerComponent {
constructor() {
this._columnPicker = inject(ColumnPickerService);
/** Access the LiveAnnounce to provide accessibility on reordering */
this._liveAnnouncer = inject(LiveAnnouncer);
/** We are using OnPush change detection so we must manually trigger CD */
this._changeDetectorRef = inject(ChangeDetectorRef);
/** Sets the id of the column picker. */
this.id = `ux-number-picker-${uniqueId$1++}`;
/** Define a list of all selected columns. */
this.selected = [];
/** Define a list of columns that are always selected. The columns cannot be moved or reordered. */
this.locked = [];
/** Define a list of columns that are not selected or locked. All columns must have unique names, including columns in different groups. */
this.deselected = [];
/** Define a function to get the aria label of reorderable items in the selected column. */
this.selectedAriaLabel = this.getDefaultSelectedAriaLabel;
/** Define a function to get the aria label of a group in the deselected list. */
this.deselectedGroupAriaLabel = this.getDefaultDeselectedGroupAriaLabel;
/** Define a function that returns a column move announcement. */
this.columnMovedAnnouncement = this.getColumnMovedAnnouncement;
/** Define the aria label for the add column button */
this.addColumnAriaLabel = 'Add selected item';
/** Define the aria label for the remove column button */
this.removeColumnAriaLabel = 'Remove selected item';
/** Define the aria label for the add all column button */
this.addAllColumnsAriaLabel = 'Add all items';
/** Define the aria label for the remove all column button */
this.removeAllColumnsAriaLabel = 'Remove all items';
/** Emits when the selected items change or the order of the selected items change. */
this.selectedChange = new EventEmitter();
/** Emits when the deselected items change. */
this.deselectedChange = new EventEmitter();
/** The Nested tree control used for the deselect tree */
this._treeControl = new FlatTreeControl(node => node.level, node => node.expandable);
/** The remaining selectable columns in the deselected list */
this._availableDeselectedColumns = 0;
/** An array of items that are currently selected in the left column. */
this._deselectedSelection = [];
/** An array of items that are currently selected in the right column. */
this._selectedSelection = [];
/** Cache selection during reordering */
this._selection = [];
/** Shallow copy of selection for reordering directive */
this._storedSelection = [];
}
/** Define settings for the grouped deselected items. */
set groups(groups) {
this._columnPicker.groups = groups;
}
get groups() {
return this._columnPicker.groups;
}
ngOnChanges(changes) {
// recreate tree when deselected changes
if (changes.deselected &&
changes.deselected.currentValue !== changes.deselected.previousValue) {
this.rebuildDeselectTree();
}
}
/** Parse data into suitable format for the FlatTreeComponent to understand and initialize deselect tree */
rebuildDeselectTree() {
const columns = this._columnPicker.getDeselectedColumnsInPresentationOrder(this.deselected.slice(), this.sort);
const treeData = this._columnPicker.createTreeData(columns);
this._treeData = treeData;
this._treeDataSource = new ArrayDataSource(treeData);
// set initial count for deselected values
this.updateAvailableDeselectedColumns();
}
/** A function that can be called to add columns. If no columns are passed to the function, the items that are selected in the left column will be added. */
addColumns(columns = this._deselectedSelection) {
const deselectedSelection = columns.filter(column => this.selected.indexOf(column) === -1);
// add each item to the selected columns list
this.selected = [...this.selected, ...deselectedSelection];
this.deselected = this.deselected.filter(column => deselectedSelection.indexOf(column) === -1);
// emit the selection changes
this.selectedChange.emit(this.selected);
this.deselectedChange.emit(this.deselected);
// store the available deselected items
this.updateAvailableDeselectedColumns();
// clear the current selection
this._deselectedSelection = [];
}
/** A function that can be called to remove columns. If no columns are passed to the function, the items that are selected in the right column will be removed. */
removeColumns(columns = this._selectedSelection) {
// remove each item from the selected columns list
this.selected = this.selected.filter(column => columns.indexOf(column) === -1);
this.deselected = [...this.deselected, ...columns];
// emit the selection changes
this.selectedChange.emit(this.selected);
this.deselectedChange.emit(this.deselected);
// store the available deselected items
this.updateAvailableDeselectedColumns();
// clear the current selection
this._selectedSelection = [];
}
/** A function that can be called to add all columns. */
addAllColumns() {
this.addColumns(this.deselected);
}
/** A function that can be called to remove all columns. */
removeAllColumns() {
this.removeColumns(this.selected);
}
/** Ensure we don't select while dragging */
storeSelection() {
this._selection = [...this._selectedSelection];
this._storedSelection = [...this.selected];
}
/** Restore the selection once dragging ends */
restoreSelection() {
this._selectedSelection = [...this._selection];
}
/** Update when reordering has occurred */
onReorder() {
this.selectedChange.emit(this.selected);
}
/** Get an aria label for deselected list groups */
getDefaultDeselectedGroupAriaLabel(column) {
return `Toggle ${column}`;
}
/** Get an aria label for reorderable items */
getDefaultSelectedAriaLabel(column) {
return `${column}. Press Alt up and alt down to reorder.`;
}
/** Get the announcement to read when a selected column is moved */
getColumnMovedAnnouncement(column, delta) {
return `${column} column moved ${delta > 0 ? 'down' : 'up'}`;
}
/** Perform a reorder with the keyboard */
move(column, delta) {
// perform the move
const index = this.selected.indexOf(column);
this.swap(index, index + delta);
// Announce the move if the order has changed
if (this.selected.indexOf(column) !== index) {
this._liveAnnouncer.announce(`Column moved ${delta > 0 ? 'down' : 'up'}`);
}
// emit the changes
this.selectedChange.emit(this.selected);
// perform change detection
this._changeDetectorRef.detectChanges();
// after the UI has updated focus the element again (ngFor creates new DOM elements)
setTimeout(() => {
const columnIndex = this.selected.indexOf(column);
const target = this.selectedElements.toArray()[columnIndex];
if (target) {
// focus the element
target.nativeElement.focus();
}
});
}
/** Provide a trackBy function for the reorderable options */
selectedTrackBy(index, column) {
return index + getColumnPickerGroupItemName(column);
}
/** Swap two elements in the selected columns array */
swap(source, target) {
// perform boundary checks
if (target < 0 || target > this.selected.length - 1) {
return;
}
// create a copy of the array to manipulate
const selected = [...this.selected];
// swap the array elements
[selected[target], selected[source]] = [selected[source], selected[target]];
// update the original array
this.selected = [...selected];
}
/** Get the column name based on type */
_getColumnName(item) {
return isColumnPickerGroupItem(item) ? item.name : item;
}
/** Check if tree group has visible children */
_nodeHasChildren(_, node) {
return node.expandable;
}
_nodeHasAvailableChildren(node) {
return node.children.filter(column => this.selected.indexOf(column) === -1).length > 0;
}
/** Check to see if current item should display in deselect tree */
_shouldRenderNode(node) {
const parent = this.getTreeParent(node);
return ((!parent || parent.isExpanded) &&
!this.selected.find(column => this._getColumnName(column) === node.name));
}
/** Work backwards from the index of the current node to find the parent node */
getTreeParent(node) {
const nodeIndex = this._treeData.indexOf(node);
if (node.level > 0) {
for (let i = nodeIndex - 1; i >= 0; i--) {
if (this._treeData[i].level === 0) {
return this._treeData[i];
}
}
}
return null;
}
/** Store the current count of nodes that are available for selection from the deselected list */
updateAvailableDeselectedColumns() {
this._availableDeselectedColumns = this.deselected.length;
}
/** Update the order of the items when reordering has changed */
onReorderChange(model) {
this.selected = [...model];
this.onReorder();
}
/** Get the action context, ensuring that functions have a pre-bound context */
_getActionContext() {
return {
addSelection: this._deselectedSelection,
removeSelection: this._selectedSelection,
addColumns: this.addColumns.bind(this),
removeColumns: this.removeColumns.bind(this),
addAllColumns: this.addAllColumns.bind(this),
removeAllColumns: this.removeAllColumns.bind(this),
};
}
/** Change the expanded state of a node */
_setNodeExpanded(node, isExpanded) {
node.isExpanded = isExpanded;
// the first change detection cycle will hide the elements but we need to trigger
// a second change detection cycle on the next tick to ensure the ContentChildren
// QueryList gets updated in the uxTabbableList directive
requestAnimationFrame(() => this._changeDetectorRef.detectChanges());
}
_isNodeSelected(name) {
const filtered = this._deselectedSelection.filter(selection => isColumnPickerGroupItem(selection) ? selection.name === name : selection === name);
return filtered.length > 0;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ColumnPickerComponent, isStandalone: false, selector: "ux-column-picker", inputs: { id: "id", selected: "selected", locked: "locked", deselected: "deselected", selectedTitleTemplate: "selectedTitleTemplate", deselectedTitleTemplate: "deselectedTitleTemplate", deselectedTemplate: "deselectedTemplate", selectedTemplate: "selectedTemplate", lockedTemplate: "lockedTemplate", actionsTemplate: "actionsTemplate", selectedAriaLabel: "selectedAriaLabel", deselectedGroupAriaLabel: "deselectedGroupAriaLabel", columnMovedAnnouncement: "columnMovedAnnouncement", groups: "groups", sort: "sort", addColumnAriaLabel: "addColumnAriaLabel", removeColumnAriaLabel: "removeColumnAriaLabel", addAllColumnsAriaLabel: "addAllColumnsAriaLabel", removeAllColumnsAriaLabel: "removeAllColumnsAriaLabel" }, outputs: { selectedChange: "selectedChange", deselectedChange: "deselectedChange" }, providers: [ColumnPickerService], viewQueries: [{ propertyName: "selectedElements", predicate: ["selectedColumn"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"column-picker-column\">\n <div class=\"column-picker-stats\" [id]=\"id + '-stats-left'\">\n @if (!deselectedTitleTemplate) {\n {{ _deselectedSelection.length }} of {{ _availableDeselectedColumns }} selected\n }\n\n @if (deselectedTitleTemplate) {\n <ng-container [ngTemplateOutlet]=\"deselectedTitleTemplate\"> </ng-container>\n }\n </div>\n\n <cdk-tree\n class=\"column-picker-list\"\n [dataSource]=\"_treeDataSource\"\n [treeControl]=\"_treeControl\"\n [(uxSelection)]=\"_deselectedSelection\"\n [attr.aria-labelledby]=\"id + '-stats-left'\"\n tabindex=\"-1\"\n uxTabbableList\n >\n <!-- Create item for not expandable node -->\n <cdk-tree-node\n *cdkTreeNodeDef=\"let node\"\n [attr.aria-hidden]=\"selected && selected.indexOf(node.name) > -1\"\n [attr.aria-selected]=\"_isNodeSelected(node.name)\"\n >\n @if (_shouldRenderNode(node)) {\n <div\n uxTabbableListItem\n [uxSelectionItem]=\"node.column\"\n addAriaAttributes=\"false\"\n class=\"column-picker-list-item\"\n [ngClass]=\"'column-picker-tree-node-level-' + node.level\"\n >\n @if (!deselectedTemplate) {\n {{ node.name }}\n }\n @if (deselectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"deselectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node.name }\"\n >\n </ng-container>\n }\n </div>\n }\n </cdk-tree-node>\n\n <!-- Create item for expandable node -->\n <cdk-tree-node\n *cdkTreeNodeDef=\"let node; when: _nodeHasChildren\"\n [attr.aria-expanded]=\"node.isExpanded\"\n >\n @if (_nodeHasAvailableChildren(node)) {\n <div class=\"column-picker-tree-group-node\">\n <button\n uxTabbableListItem\n (click)=\"_setNodeExpanded(node, !node.isExpanded)\"\n (keydown.arrowright)=\"_setNodeExpanded(node, true)\"\n (keydown.arrowleft)=\"_setNodeExpanded(node, false)\"\n [style.visibility]=\"node.expandable ? 'visible' : 'hidden'\"\n [attr.aria-label]=\"deselectedGroupAriaLabel(node.name, node.isExpanded)\"\n class=\"column-picker-group-toggle-btn\"\n >\n <ux-icon [name]=\"node.isExpanded ? 'chevron-down' : 'chevron-right'\"></ux-icon>\n @if (!deselectedTemplate) {\n {{ node.name }}\n }\n @if (deselectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"deselectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node.name }\"\n >\n </ng-container>\n }\n </button>\n </div>\n }\n </cdk-tree-node>\n </cdk-tree>\n</div>\n\n<div class=\"column-picker-actions-column\">\n <!-- Show the default action buttons -->\n @if (!actionsTemplate) {\n <button\n class=\"btn button-primary btn-block\"\n [disabled]=\"_deselectedSelection.length === 0\"\n [attr.aria-label]=\"addColumnAriaLabel\"\n (click)=\"addColumns()\"\n >\n <ux-icon name=\"chevron-right\"></ux-icon>\n </button>\n <button\n class=\"btn button-primary btn-block m-b-md\"\n [disabled]=\"_selectedSelection.length === 0\"\n [attr.aria-label]=\"removeColumnAriaLabel\"\n (click)=\"removeColumns()\"\n >\n <ux-icon name=\"chevron-left\"></ux-icon>\n </button>\n <button\n class=\"btn button-secondary btn-block\"\n [disabled]=\"_availableDeselectedColumns === 0\"\n [attr.aria-label]=\"addAllColumnsAriaLabel\"\n (click)=\"addAllColumns()\"\n >\n <ux-icon name=\"chevron-right-double\"></ux-icon>\n </button>\n <button\n class=\"btn button-secondary btn-block\"\n [disabled]=\"selected.length === 0\"\n [attr.aria-label]=\"removeAllColumnsAriaLabel\"\n (click)=\"removeAllColumns()\"\n >\n <ux-icon name=\"chevron-left-double\"></ux-icon>\n </button>\n }\n\n <!-- Allow custom actions template -->\n @if (actionsTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"actionsTemplate\"\n [ngTemplateOutletContext]=\"_getActionContext()\"\n >\n </ng-container>\n }\n</div>\n\n<div class=\"column-picker-column\">\n <div class=\"column-picker-stats\" [id]=\"id + '-stats-right'\">\n @if (!selectedTitleTemplate) {\n {{ selected.length + locked.length }} columns added\n }\n\n @if (selectedTitleTemplate) {\n <ng-container [ngTemplateOutlet]=\"selectedTitleTemplate\"> </ng-container>\n }\n </div>\n\n <div class=\"column-picker-list\" role=\"listbox\" [attr.aria-labelledby]=\"id + '-stats-right'\">\n @for (column of locked; track column) {\n <div class=\"column-picker-list-item column-picker-list-item-locked\">\n @if (!lockedTemplate) {\n {{ column }} <ux-icon name=\"lock\"></ux-icon>\n }\n @if (lockedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"lockedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: column }\"\n >\n </ng-container>\n }\n </div>\n }\n\n <div\n [(uxSelection)]=\"_selectedSelection\"\n uxReorderable\n [reorderableModel]=\"_storedSelection\"\n (reorderableModelChange)=\"onReorderChange($event)\"\n (reorderStart)=\"storeSelection()\"\n (reorderEnd)=\"restoreSelection()\"\n >\n @for (column of selected; track selectedTrackBy(index, column); let index = $index) {\n <div\n #selectedColumn\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n class=\"column-picker-list-item column-picker-list-item-selected\"\n [uxSelectionItem]=\"column\"\n [uxReorderableModel]=\"column\"\n [attr.aria-label]=\"selectedAriaLabel(column, selected.indexOf(column))\"\n (keydown.alt.arrowup)=\"move(column, -1)\"\n (keydown.alt.arrowdown)=\"move(column, 1)\"\n role=\"option\"\n >\n @if (!selectedTemplate) {\n <ux-icon uxReorderableHandle name=\"drag\" class=\"drag-handle-icon\"></ux-icon>\n {{ _getColumnName(column) }}\n }\n @if (selectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"selectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: column }\"\n >\n </ng-container>\n }\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: DefaultFocusIndicatorDirective, selector: ".btn:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor]), a[href]:not([uxFocusIndicator]):not([uxMenuNavigationToggle]):not([uxMenuTriggerFor])" }, { kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: TabbableListDirective, selector: "[uxTabbableList]", inputs: ["direction", "wrap", "focusOnShow", "returnFocus", "hierarchy", "allowAltModifier", "allowCtrlModifier", "allowBoundaryKeys"], exportAs: ["ux-tabbable-list"] }, { kind: "directive", type: TabbableListItemDirective, selector: "[uxTabbableListItem]", inputs: ["parent", "rank", "disabled", "expanded", "key"], outputs: ["expandedChange", "activated"], exportAs: ["ux-tabbable-list-item"] }, { kind: "directive", type: i5.CdkTreeNodeDef, selector: "[cdkTreeNodeDef]", inputs: ["cdkTreeNodeDefWhen"] }, { kind: "component", type: i5.CdkTree, selector: "cdk-tree", inputs: ["dataSource", "treeControl", "levelAccessor", "childrenAccessor", "trackBy", "expansionKey"], exportAs: ["cdkTree"] }, { kind: "directive", type: i5.CdkTreeNode, selector: "cdk-tree-node", inputs: ["role", "isExpandable", "isExpanded", "isDisabled", "cdkTreeNodeTypeaheadLabel"], outputs: ["activation", "expandedChange"], exportAs: ["cdkTreeNode"] }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }, { kind: "directive", type: ReorderableDirective, selector: "[uxReorderable]", inputs: ["reorderableModel", "reorderableGroup", "reorderingDisabled"], outputs: ["reorderableModelChange", "reorderStart", "reorderCancel", "reorderEnd"] }, { kind: "directive", type: ReorderableHandleDirective, selector: "[uxReorderableHandle]" }, { kind: "directive", type: ReorderableModelDirective, selector: "[uxReorderableModel]", inputs: ["uxReorderableModel"] }, { kind: "directive", type: SelectionDirective, selector: "[uxSelection]", inputs: ["uxSelection", "disabled", "mode", "clickSelection", "keyboardSelection", "selectionItems", "tabindex"], outputs: ["uxSelectionChange"], exportAs: ["ux-selection"] }, { kind: "directive", type: SelectionItemDirective, selector: "[uxSelectionItem]", inputs: ["uxSelectionItem", "selected", "tabindex", "uxSelectionDisabled", "addAriaAttributes"], outputs: ["selectedChange"], exportAs: ["ux-selection-item"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ColumnPickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-column-picker', changeDetection: ChangeDetectionStrategy.OnPush, providers: [ColumnPickerService], standalone: false, template: "<div class=\"column-picker-column\">\n <div class=\"column-picker-stats\" [id]=\"id + '-stats-left'\">\n @if (!deselectedTitleTemplate) {\n {{ _deselectedSelection.length }} of {{ _availableDeselectedColumns }} selected\n }\n\n @if (deselectedTitleTemplate) {\n <ng-container [ngTemplateOutlet]=\"deselectedTitleTemplate\"> </ng-container>\n }\n </div>\n\n <cdk-tree\n class=\"column-picker-list\"\n [dataSource]=\"_treeDataSource\"\n [treeControl]=\"_treeControl\"\n [(uxSelection)]=\"_deselectedSelection\"\n [attr.aria-labelledby]=\"id + '-stats-left'\"\n tabindex=\"-1\"\n uxTabbableList\n >\n <!-- Create item for not expandable node -->\n <cdk-tree-node\n *cdkTreeNodeDef=\"let node\"\n [attr.aria-hidden]=\"selected && selected.indexOf(node.name) > -1\"\n [attr.aria-selected]=\"_isNodeSelected(node.name)\"\n >\n @if (_shouldRenderNode(node)) {\n <div\n uxTabbableListItem\n [uxSelectionItem]=\"node.column\"\n addAriaAttributes=\"false\"\n class=\"column-picker-list-item\"\n [ngClass]=\"'column-picker-tree-node-level-' + node.level\"\n >\n @if (!deselectedTemplate) {\n {{ node.name }}\n }\n @if (deselectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"deselectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node.name }\"\n >\n </ng-container>\n }\n </div>\n }\n </cdk-tree-node>\n\n <!-- Create item for expandable node -->\n <cdk-tree-node\n *cdkTreeNodeDef=\"let node; when: _nodeHasChildren\"\n [attr.aria-expanded]=\"node.isExpanded\"\n >\n @if (_nodeHasAvailableChildren(node)) {\n <div class=\"column-picker-tree-group-node\">\n <button\n uxTabbableListItem\n (click)=\"_setNodeExpanded(node, !node.isExpanded)\"\n (keydown.arrowright)=\"_setNodeExpanded(node, true)\"\n (keydown.arrowleft)=\"_setNodeExpanded(node, false)\"\n [style.visibility]=\"node.expandable ? 'visible' : 'hidden'\"\n [attr.aria-label]=\"deselectedGroupAriaLabel(node.name, node.isExpanded)\"\n class=\"column-picker-group-toggle-btn\"\n >\n <ux-icon [name]=\"node.isExpanded ? 'chevron-down' : 'chevron-right'\"></ux-icon>\n @if (!deselectedTemplate) {\n {{ node.name }}\n }\n @if (deselectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"deselectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: node.name }\"\n >\n </ng-container>\n }\n </button>\n </div>\n }\n </cdk-tree-node>\n </cdk-tree>\n</div>\n\n<div class=\"column-picker-actions-column\">\n <!-- Show the default action buttons -->\n @if (!actionsTemplate) {\n <button\n class=\"btn button-primary btn-block\"\n [disabled]=\"_deselectedSelection.length === 0\"\n [attr.aria-label]=\"addColumnAriaLabel\"\n (click)=\"addColumns()\"\n >\n <ux-icon name=\"chevron-right\"></ux-icon>\n </button>\n <button\n class=\"btn button-primary btn-block m-b-md\"\n [disabled]=\"_selectedSelection.length === 0\"\n [attr.aria-label]=\"removeColumnAriaLabel\"\n (click)=\"removeColumns()\"\n >\n <ux-icon name=\"chevron-left\"></ux-icon>\n </button>\n <button\n class=\"btn button-secondary btn-block\"\n [disabled]=\"_availableDeselectedColumns === 0\"\n [attr.aria-label]=\"addAllColumnsAriaLabel\"\n (click)=\"addAllColumns()\"\n >\n <ux-icon name=\"chevron-right-double\"></ux-icon>\n </button>\n <button\n class=\"btn button-secondary btn-block\"\n [disabled]=\"selected.length === 0\"\n [attr.aria-label]=\"removeAllColumnsAriaLabel\"\n (click)=\"removeAllColumns()\"\n >\n <ux-icon name=\"chevron-left-double\"></ux-icon>\n </button>\n }\n\n <!-- Allow custom actions template -->\n @if (actionsTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"actionsTemplate\"\n [ngTemplateOutletContext]=\"_getActionContext()\"\n >\n </ng-container>\n }\n</div>\n\n<div class=\"column-picker-column\">\n <div class=\"column-picker-stats\" [id]=\"id + '-stats-right'\">\n @if (!selectedTitleTemplate) {\n {{ selected.length + locked.length }} columns added\n }\n\n @if (selectedTitleTemplate) {\n <ng-container [ngTemplateOutlet]=\"selectedTitleTemplate\"> </ng-container>\n }\n </div>\n\n <div class=\"column-picker-list\" role=\"listbox\" [attr.aria-labelledby]=\"id + '-stats-right'\">\n @for (column of locked; track column) {\n <div class=\"column-picker-list-item column-picker-list-item-locked\">\n @if (!lockedTemplate) {\n {{ column }} <ux-icon name=\"lock\"></ux-icon>\n }\n @if (lockedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"lockedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: column }\"\n >\n </ng-container>\n }\n </div>\n }\n\n <div\n [(uxSelection)]=\"_selectedSelection\"\n uxReorderable\n [reorderableModel]=\"_storedSelection\"\n (reorderableModelChange)=\"onReorderChange($event)\"\n (reorderStart)=\"storeSelection()\"\n (reorderEnd)=\"restoreSelection()\"\n >\n @for (column of selected; track selectedTrackBy(index, column); let index = $index) {\n <div\n #selectedColumn\n uxFocusIndicator\n [programmaticFocusIndicator]=\"true\"\n class=\"column-picker-list-item column-picker-list-item-selected\"\n [uxSelectionItem]=\"column\"\n [uxReorderableModel]=\"column\"\n [attr.aria-label]=\"selectedAriaLabel(column, selected.indexOf(column))\"\n (keydown.alt.arrowup)=\"move(column, -1)\"\n (keydown.alt.arrowdown)=\"move(column, 1)\"\n role=\"option\"\n >\n @if (!selectedTemplate) {\n <ux-icon uxReorderableHandle name=\"drag\" class=\"drag-handle-icon\"></ux-icon>\n {{ _getColumnName(column) }}\n }\n @if (selectedTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"selectedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: column }\"\n >\n </ng-container>\n }\n </div>\n }\n </div>\n </div>\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}], selected: [{
type: Input
}], locked: [{
type: Input
}], deselected: [{
type: Input
}], selectedTitleTemplate: [{
type: Input
}], deselectedTitleTemplate: [{
type: Input
}], deselectedTemplate: [{
type: Input
}], selectedTemplate: [{
type: Input
}], lockedTemplate: [{
type: Input
}], actionsTemplate: [{
type: Input
}], selectedAriaLabel: [{
type: Input
}], deselectedGroupAriaLabel: [{
type: Input
}], columnMovedAnnouncement: [{
type: Input
}], groups: [{
type: Input
}], sort: [{
type: Input
}], addColumnAriaLabel: [{
type: Input
}], removeColumnAriaLabel: [{
type: Input
}], addAllColumnsAriaLabel: [{
type: Input
}], removeAllColumnsAriaLabel: [{
type: Input
}], selectedChange: [{
type: Output
}], deselectedChange: [{
type: Output
}], selectedElements: [{
type: ViewChildren,
args: ['selectedColumn']
}] } });
function getColumnPickerGroupItemName(column) {
return isColumnPickerGroupItem(column) ? column.name : column;
}
class BaseResizableTableService {
constructor() {
/** Emit an event whenever a column is resized */
this.onResize$ = new Subject();
/** Store the current width of the table */
this.tableWidth = 0;
/** Determine if we are currently resizing */
this.isResizing$ = new BehaviorSubject(false);
/** Indicate when the columns are ready */
this.isInitialised$ = new BehaviorSubject(false);
/** Store the percentage widths of each column */
this.columns = [];
}
/** Cleanup when service is disposed */
ngOnDestroy() {
this.onResize$.complete();
}
/** Update the resizing state */
setResizing(isResizing) {
this.isResizing$.next(isResizing);
}
/** Get the width of a column in a specific unit */
getColumnWidth(index, unit, columns = this.columns) {
switch (unit) {
case ColumnUnit$2.Percentage:
return columns[index];
case ColumnUnit$2.Pixel:
return (this.tableWidth / 100) * columns[index];
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseResizableTableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseResizableTableService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseResizableTableService, decorators: [{
type: Injectable
}] });
var ColumnUnit$2;
(function (ColumnUnit) {
ColumnUnit[ColumnUnit["Pixel"] = 0] = "Pixel";
ColumnUnit[ColumnUnit["Percentage"] = 1] = "Percentage";
})(ColumnUnit$2 || (ColumnUnit$2 = {}));
var ResizableTableType;
(function (ResizableTableType) {
ResizableTableType[ResizableTableType["Standard"] = 0] = "Standard";
ResizableTableType[ResizableTableType["Expand"] = 1] = "Expand";
})(ResizableTableType || (ResizableTableType = {}));
const RESIZABLE_TABLE_SERVICE_TOKEN = new InjectionToken('RESIZABLE_TABLE_SERVICE_TOKEN');
class ResizableTableService extends BaseResizableTableService {
constructor() {
super(...arguments);
/** Define the type of resizing we should use */
this.type = ResizableTableType.Standard;
}
/** Store the size of each column */
setColumns(columns) {
// store the current columns
this._columns = columns;
// store the sizes
this.columns = columns.map(column => (column.getNaturalWidth() / this.tableWidth) * 100);
// check if there is any overflow
this.columns = this.ensureNoOverflow(this.columns);
// ensure all the columns fit
this._columns.forEach((column, idx) => {
if (!column.disabled) {
this.resizeColumn(idx, 0, false);
}
});
// indicate we are now initialised
if (this.isInitialised$.value === false) {
this.isInitialised$.next(true);
}
}
/** Set all resizable columns to the same width */
setUniformWidths() {
// set any disabled columns to their specified width
this.columns = this._columns.map(column => column.disabled ? (column.getNaturalWidth() / this.tableWidth) * 100 : 0);
// check to see if we've reached 100% of the table width
const totalWidth = this.columns.reduce((partial, columnWidth) => partial + columnWidth);
if (totalWidth > 100) {
// remove overflow
this.columns = this.ensureNoOverflow(this.columns);
}
else {
// get the list of resizable columns
const resizableColumns = this._columns.toArray().filter(column => !column.disabled);
// work out what we need to add to each column to make up the full width
const newWidth = (100 - totalWidth) / resizableColumns.length;
// set the non-disabled columns to the new width
this.columns = this._columns.map((column, idx) => column.disabled ? this.columns[idx] : newWidth);
}
// do the resizing
this._columns.forEach((column, idx) => {
if (!column.disabled) {
this.resizeColumn(idx, 0, false);
}
});
}
ensureNoOverflow(columns) {
// get the total width
const total = columns.reduce((width, column) => width + column);
// if we have no overflow then we don't need to do anything
if (total <= 100) {
return columns;
}
// if there is overflow identify which columns can be resized
const variableColumns = this._columns.filter(column => !column.disabled &&
this.getColumnWidth(column.getCellIndex(), ColumnUnit$1.Pixel, columns) > column.minWidth);
// if there are no columns that can be resized then stop here
if (variableColumns.length === 0) {
return columns;
}
// determine the total width of the variable columns
const totalWidth = this._columns.reduce((width, column) => width + this.getColumnWidth(column.getCellIndex(), ColumnUnit$1.Pixel, columns), 0);
// determine to the width of all the variable columns
const variableColumnsWidth = variableColumns.reduce((width, column) => width + this.getColumnWidth(column.getCellIndex(), ColumnUnit$1.Pixel, columns), 0);
// determine how much the columns are currently too large (ignoring fixed columns)
const targetWidth = this.tableWidth - (totalWidth - variableColumnsWidth);
// determine how much we need to reduce a column by
const difference = variableColumnsWidth - targetWidth;
// find the column with the largest size
const target = variableColumns.reduce((widest, column) => {
const columnWidth = this.getColumnWidth(column.getCellIndex(), ColumnUnit$1.Pixel, columns);
const widestWidth = this.getColumnWidth(widest.getCellIndex(), ColumnUnit$1.Pixel, columns);
return columnWidth > widestWidth ? column : widest;
});
// perform the resize
columns = this.setColumnWidth(target.getCellIndex(), this.getColumnWidth(target.getCellIndex(), ColumnUnit$1.Pixel, columns) - difference, ColumnUnit$1.Pixel, columns);
// check if we are still over the limit (allow some variance for javascript double precision)
if (columns.reduce((width, column) => width + column) > 100.01) {
return this.ensureNoOverflow(columns);
}
return columns;
}
/** Allow setting the column size in any unit */
setColumnWidth(index, value, unit, columns = this.columns) {
// create a new array so we keep the instance array immutable
const sizes = [...columns];
switch (unit) {
case ColumnUnit$1.Percentage:
sizes[index] = value;
break;
case ColumnUnit$1.Pixel:
sizes[index] = (value / this.tableWidth) * 100;
break;
}
// update the instance variable
return sizes;
}
/** Resize a column by a specific pixel amount */
resizeColumn(index, delta, isDragging = true) {
// get the sibling column that will also be resized
const sibling = this.getSiblingColumn(index);
// if there is no sibling that can be resized then stop here
if (!sibling) {
return;
}
// create a new array for the sizes
let columns = [...this.columns];
// resize the column to the desired size
columns = this.setColumnWidth(index, Math.round(this.getColumnWidth(index, ColumnUnit$1.Pixel) + delta), ColumnUnit$1.Pixel, columns);
columns = this.setColumnWidth(sibling, Math.round(this.getColumnWidth(sibling, ColumnUnit$1.Pixel) - delta), ColumnUnit$1.Pixel, columns);
// if the move is not possible then stop here
if (!this.isWidthValid(index, this.getColumnWidth(index, ColumnUnit$1.Pixel, columns)) ||
!this.isWidthValid(sibling, this.getColumnWidth(sibling, ColumnUnit$1.Pixel, columns))) {
return;
}
// check that we add up to exactly 100%
const total = columns.reduce((count, column) => column + count, 0);
// if the columns to not add to 100 ensure we make them
if (total !== 100) {
// get the column with a variable width
const target = this.getVariableColumn(100 - total);
if (target && !isDragging) {
columns[this._columns.toArray().indexOf(target)] += 100 - total;
}
else {
columns[index] += 100 - total;
}
}
// store the new sizes
this.columns = columns;
// emit the resize event for each column
this.onResize$.next();
}
getVariableColumn(delta) {
// get all variable width columns that are not disabled
const variableColumns = this._columns.filter(column => !column.isFixedWidth && !column.disabled);
// find one that is greater than its min width by enough
return variableColumns
.reverse()
.find(column => this.getColumnWidth(column.getCellIndex(), ColumnUnit$1.Pixel) >= column.minWidth + delta);
}
getColumn(index) {
return this._columns ? this._columns.toArray()[index] : null;
}
getColumnDisabled(index) {
return this.getColumn(index) ? this.getColumn(index).disabled : false;
}
/** Determine whether a column is above or below its minimum width */
isWidthValid(index, width) {
// get the column at a given position
const column = this.getColumnInstance(index);
// determine if the specified width is greater than the min width
return column && width >= column.minWidth;
}
/** Get the next column in the sequence of columns */
getSiblingColumn(index) {
// find the first sibling that is not disabled
for (let idx = index + 1; idx < this.columns.length; idx++) {
const sibling = this.getColumnInstance(idx);
if (!sibling || !sibling.disabled) {
return idx;
}
}
return null;
}
/** Get the column class from our query list */
getColumnInstance(index) {
return this._columns ? this._columns.toArray()[index] : null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableService, decorators: [{
type: Injectable
}] });
var ColumnUnit$1;
(function (ColumnUnit) {
ColumnUnit[ColumnUnit["Pixel"] = 0] = "Pixel";
ColumnUnit[ColumnUnit["Percentage"] = 1] = "Percentage";
})(ColumnUnit$1 || (ColumnUnit$1 = {}));
class ResizableTableCellComponent {
constructor() {
this._table = inject(RESIZABLE_TABLE_SERVICE_TOKEN);
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
/** Unsubscribe from all subscriptions on destroy */
this._onDestroy = new Subject();
}
ngOnInit() {
this._minWidth = parseFloat(getComputedStyle(this._elementRef.nativeElement).minWidth);
// if the table has already been initialised then we should set the initial size
if (this._table.isInitialised$.value) {
this.setColumnWidth();
this.setColumnFlex();
}
// update the sizes when columns are resized
combineLatest([this._table.onResize$, this._table.isResizing$])
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.setColumnWidth();
this.setColumnFlex();
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Get the column index this cell is part of */
getCellIndex() {
return this._elementRef.nativeElement.cellIndex;
}
/** Set the width of the column */
setColumnWidth() {
const width = this._table.isResizing$.value || this._table.getColumnDisabled(this.getCellIndex())
? `${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel)}px`
: `${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Percentage)}%`;
if (this._table.type === ResizableTableType.Expand) {
const minWidth = Math.max(this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel), this._minWidth);
this._renderer.setStyle(this._elementRef.nativeElement, 'min-width', `${minWidth}px`);
}
this._renderer.setStyle(this._elementRef.nativeElement, 'width', width);
}
/** Set the flex value of the column */
setColumnFlex() {
// if we are resizing then always return 'none' to allow free movement
if (this._table.isResizing$.value || this._table.getColumnDisabled(this.getCellIndex())) {
this._renderer.setStyle(this._elementRef.nativeElement, 'flex', 'none');
return;
}
const flex = this._table.isInitialised$.value
? `0 1 ${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Percentage)}%`
: '';
this._renderer.setStyle(this._elementRef.nativeElement, 'flex', flex);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: ResizableTableCellComponent, isStandalone: false, selector: "[uxResizableTableCell]", ngImport: i0, template: "<div class=\"ux-resizable-table-cell-content\">\n <ng-content></ng-content>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableCellComponent, decorators: [{
type: Component,
args: [{ selector: '[uxResizableTableCell]', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"ux-resizable-table-cell-content\">\n <ng-content></ng-content>\n</div>\n" }]
}] });
class ResizableTableColumnComponent {
constructor() {
this._table = inject(RESIZABLE_TABLE_SERVICE_TOKEN);
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
/** Show/Hide column resizable handle */
this._handleVisible = true;
/** Disabled the column resizing */
this.disabled = false;
/** Emit the current column width */
this.widthChange = new EventEmitter();
/** Determine if this column is a variable width column */
this.isFixedWidth = false;
/** Emit when all observables should be unsubscribed */
this._onDestroy = new Subject();
}
get handleVisible() {
return this._handleVisible;
}
set handleVisible(value) {
this._handleVisible = coerceBooleanProperty(value);
}
/** Define the width of a column */
set width(width) {
// there may be cases where columns are created with an `*ngFor` and a width
// may be specified on *some* columns and not others. This this setter will
// still be called whenever the value is empty and this will mark this column
// as having a fixed width, even though it doesn't. So we should only proceed
// whenever there is an actual numeric value passed in.
if (width === null || width === undefined) {
return;
}
// ensure width is a valid number
this._width = coerceNumberProperty(width);
// note that this column has a fixed width
this.isFixedWidth = true;
// if we have not initialised then set the element width
if (!this._table.isInitialised$.value) {
this._renderer.setStyle(this._elementRef.nativeElement, 'width', `${this._width}px`);
}
else {
// if it is initialised then resize the column
const currentWidth = this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel);
// resize the column by the difference in size
if (isNaN(currentWidth)) {
this._table.resizeColumn(this.getCellIndex(), this._width, false);
}
else {
this._table.resizeColumn(this.getCellIndex(), this._width - currentWidth, false);
}
}
}
get width() {
return this._width;
}
/** Get the minimum width allowed by the column */
get minWidth() {
// determine the minimum width of the column based on its computed CSS value
const computed = parseFloat(getComputedStyle(this._elementRef.nativeElement).minWidth);
// if it is disabled use its current width - otherwise use its CSS min width if it is valid
return this.disabled
? this._elementRef.nativeElement.offsetWidth
: isNaN(computed)
? 0
: computed;
}
ngAfterViewInit() {
// initially emit the size when we have initialised
this._table.isInitialised$
.pipe(takeUntil(this._onDestroy), filter(isInitialised => isInitialised))
.subscribe(() => {
// get the current min-width
this._minWidth = parseFloat(getComputedStyle(this._elementRef.nativeElement).minWidth);
const width = this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel);
if (!isNaN(width)) {
this.widthChange.emit(width);
}
});
// ensure the correct width gets emitted on column size change
this._table.onResize$.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.setColumnWidth();
this.setColumnFlex();
// get the current table width
const width = this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel);
// check if the width actually changed - otherwise don't emit
if (!isNaN(width) &&
(this._width === undefined ||
Math.max(width, this._width) - Math.min(width, this._width) >= 1)) {
this.widthChange.emit(width);
}
});
}
/** Cleanup when component is destroyed */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Get the natural pixel width of the column */
getNaturalWidth() {
return this._width || this._elementRef.nativeElement.offsetWidth;
}
/** When the dragging starts */
onDragStart(event) {
// determine the mouse position within the handle
this._offset = event.clientX - event.target.getBoundingClientRect().left;
}
/** When the mouse is moved */
onDragMove(event, handle) {
// get the current mouse position
const mouseX = event.pageX - pageXOffset;
// position of the drag handle
const { left } = handle.getBoundingClientRect();
// determine how much the mouse has moved since the last update
const delta = mouseX - (left + this._offset);
// perform resizing
this._table.resizeColumn(this.getCellIndex(), delta);
// set the resizing state
this._table.setResizing(true);
}
/** When the dragging ends */
onDragEnd() {
this._table.setResizing(false);
}
/** Shrink the column when the left arrow key is pressed */
onMoveLeft() {
this._table.resizeColumn(this.getCellIndex(), -10);
}
/** Grow the column when the right arrow key is pressed */
onMoveRight() {
this._table.resizeColumn(this.getCellIndex(), 10);
}
/** Get the column index this cell is part of */
getCellIndex() {
return this._elementRef.nativeElement.cellIndex;
}
/** The percentage width of the column */
setColumnWidth() {
if (this.disabled && this._width !== undefined) {
this._renderer.setStyle(this._elementRef.nativeElement, 'width', `${this._width}px`);
this._renderer.setStyle(this._elementRef.nativeElement, 'max-width', `${this._width}px`);
return;
}
if (!this._table.isInitialised$.value) {
return;
}
const width = this._table.isResizing$.value
? `${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel)}px`
: `${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Percentage)}%`;
this._renderer.setStyle(this._elementRef.nativeElement, 'width', width);
this._renderer.setStyle(this._elementRef.nativeElement, 'max-width', null);
}
/** The flex width of the column */
setColumnFlex() {
let flex;
// if we are resizing then always return 'none' to allow free movement
if (this._table.isResizing$.value || this.disabled) {
this._renderer.setStyle(this._elementRef.nativeElement, 'flex', 'none');
}
if (this._table.type === ResizableTableType.Expand) {
flex = this._table.isInitialised$.value
? `0 0 ${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Pixel)}px`
: '';
}
else {
flex = this._table.isInitialised$.value
? `0 1 ${this._table.getColumnWidth(this.getCellIndex(), ColumnUnit$1.Percentage)}%`
: '';
}
this._renderer.setStyle(this._elementRef.nativeElement, 'flex', flex);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: ResizableTableColumnComponent, isStandalone: false, selector: "[uxResizableTableColumn]", inputs: { disabled: "disabled", handleVisible: "handleVisible", width: "width" }, outputs: { widthChange: "widthChange" }, host: { properties: { "class.ux-resizable-table-hide-handle": "!handleVisible", "class.ux-resizable-table-column-disabled": "this.disabled" }, classAttribute: "ux-resizable-table-column" }, ngImport: i0, template: "<div class=\"ux-resizable-table-column-content\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"ux-resizable-table-column-actions\">\n <ng-content select=\"ux-column-sorting\"></ng-content>\n</div>\n\n@if (!disabled && handleVisible) {\n <div\n #handle\n uxDrag\n uxFocusIndicator\n tabindex=\"0\"\n role=\"separator\"\n aria-label=\"Column resize handle. Use arrow keys to change the column width.\"\n class=\"ux-resizable-table-column-handle\"\n [attr.aria-valuemin]=\"minWidth\"\n [attr.aria-valuenow]=\"width\"\n (onDragStart)=\"onDragStart($event)\"\n (onDrag)=\"onDragMove($event, handle)\"\n (onDragEnd)=\"onDragEnd()\"\n (keydown.ArrowLeft)=\"onMoveLeft()\"\n (keydown.ArrowRight)=\"onMoveRight()\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"ux-resizable-table-column-handle-icon\"></div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: DragDirective, selector: "[uxDrag]", inputs: ["clone", "group", "model", "draggable"], outputs: ["onDragStart", "onDrag", "onDragScroll", "onDragEnd", "onDrop", "onDropEnter", "onDropLeave"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableColumnComponent, decorators: [{
type: Component,
args: [{ selector: '[uxResizableTableColumn]', changeDetection: ChangeDetectionStrategy.OnPush, host: {
class: 'ux-resizable-table-column',
'[class.ux-resizable-table-hide-handle]': '!handleVisible',
}, standalone: false, template: "<div class=\"ux-resizable-table-column-content\">\n <ng-content></ng-content>\n</div>\n\n<div class=\"ux-resizable-table-column-actions\">\n <ng-content select=\"ux-column-sorting\"></ng-content>\n</div>\n\n@if (!disabled && handleVisible) {\n <div\n #handle\n uxDrag\n uxFocusIndicator\n tabindex=\"0\"\n role=\"separator\"\n aria-label=\"Column resize handle. Use arrow keys to change the column width.\"\n class=\"ux-resizable-table-column-handle\"\n [attr.aria-valuemin]=\"minWidth\"\n [attr.aria-valuenow]=\"width\"\n (onDragStart)=\"onDragStart($event)\"\n (onDrag)=\"onDragMove($event, handle)\"\n (onDragEnd)=\"onDragEnd()\"\n (keydown.ArrowLeft)=\"onMoveLeft()\"\n (keydown.ArrowRight)=\"onMoveRight()\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"ux-resizable-table-column-handle-icon\"></div>\n </div>\n}\n" }]
}], propDecorators: { disabled: [{
type: Input
}, {
type: HostBinding,
args: ['class.ux-resizable-table-column-disabled']
}], handleVisible: [{
type: Input
}], width: [{
type: Input
}], widthChange: [{
type: Output
}] } });
class BaseResizableTableDirective {
constructor() {
this._table = inject(RESIZABLE_TABLE_SERVICE_TOKEN);
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this.resize = inject(ResizeService);
/** Unsubscribe from the observables */
this._onDestroy = new Subject();
/** Store the initialised state of the table */
this._initialised = false;
// watch for the table being resized
this.resize
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
// store the latest table size
this._table.tableWidth = this.getScrollWidth();
// run the initial logic if the table is fully visible
this.onTableReady();
});
}
/** Cleanup after the component is destroyed */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Set all resizable columns to the same width */
setUniformWidths() {
this._table.setUniformWidths();
}
/** Get the smallest tbody width taking into account scrollbars (uxFixedHeaderTable) */
getScrollWidth() {
return Array.from(this._elementRef.nativeElement.tBodies).reduce((width, tbody) => Math.min(width, tbody.scrollWidth), this._elementRef.nativeElement.offsetWidth);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseResizableTableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: BaseResizableTableDirective, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaseResizableTableDirective, decorators: [{
type: Directive
}], ctorParameters: () => [] });
class ResizableExpandingTableService extends BaseResizableTableService {
constructor() {
super(...arguments);
/** Define the type of resizing we should use */
this.type = ResizableTableType.Expand;
}
/** Store the size of each column */
setColumns(columns) {
// store the current columns
this._columns = columns;
// store the sizes
this.columns = columns.map(column => (column.getNaturalWidth() / this.tableWidth) * 100);
// ensure all the columns fit
this._columns.forEach((column, idx) => {
if (!column.disabled) {
this.columns = this.setColumnWidth(idx, this.columns[idx], ColumnUnit.Percentage, this.columns);
}
});
// indicate we are now initialised
if (this.isInitialised$.value === false) {
this.isInitialised$.next(true);
}
}
/** Set all resizable columns to the same width */
setUniformWidths() {
// set any disabled columns to their specified width
this.columns = this._columns.map(column => column.disabled ? (column.getNaturalWidth() / this.tableWidth) * 100 : 0);
// check to see if we've reached 100% of the table width
const totalWidth = this.columns.reduce((partial, columnWidth) => partial + columnWidth);
if (totalWidth > 98) {
// remove overflow
this.columns = this.ensureNoOverflow(this.columns);
}
else {
// get the list of resizable columns
const resizableColumns = this._columns.toArray().filter(column => !column.disabled);
// work out what we need to add to each column to make up the full width
const newWidth = (98 - totalWidth) / resizableColumns.length;
// set the non-disabled columns to the new width
this.columns = this._columns.map((column, idx) => column.disabled ? this.columns[idx] : newWidth);
}
// do the resizing
this._columns.forEach((column, idx) => {
if (!column.disabled) {
this.resizeColumn(idx, 0);
}
});
}
ensureNoOverflow(columns) {
// get the total width
const total = columns.reduce((width, column) => width + column);
// if we have no overflow then we don't need to do anything
if (total <= 100) {
return columns;
}
// if there is overflow identify which columns can be resized
const variableColumns = this._columns.filter(column => !column.disabled &&
this.getColumnWidth(column.getCellIndex(), ColumnUnit.Pixel, columns) > column.minWidth);
// if there are no columns that can be resized then stop here
if (variableColumns.length === 0) {
return columns;
}
// determine the total width of the variable columns
const totalWidth = this._columns.reduce((width, column) => width + this.getColumnWidth(column.getCellIndex(), ColumnUnit.Pixel, columns), 0);
// determine to the width of all the variable columns
const variableColumnsWidth = variableColumns.reduce((width, column) => width + this.getColumnWidth(column.getCellIndex(), ColumnUnit.Pixel, columns), 0);
// determine how much the columns are currently too large (ignoring fixed columns)
const targetWidth = this.tableWidth - (totalWidth - variableColumnsWidth);
// determine how much we need to reduce a column by
const difference = variableColumnsWidth - targetWidth;
// find the column with the largest size
const target = variableColumns.reduce((widest, column) => {
const columnWidth = this.getColumnWidth(column.getCellIndex(), ColumnUnit.Pixel, columns);
const widestWidth = this.getColumnWidth(widest.getCellIndex(), ColumnUnit.Pixel, columns);
return columnWidth > widestWidth ? column : widest;
});
// perform the resize
columns = this.setColumnWidth(target.getCellIndex(), this.getColumnWidth(target.getCellIndex(), ColumnUnit.Pixel, columns) - difference, ColumnUnit.Pixel, columns);
// check if we are still over the limit (allow some variance for javascript double precision)
if (columns.reduce((width, column) => width + column) > 100.01) {
return this.ensureNoOverflow(columns);
}
return columns;
}
/** Allow setting the column size in any unit */
setColumnWidth(index, value, unit, columns = this.columns) {
// create a new array so we keep the instance array immutable
const sizes = [...columns];
switch (unit) {
case ColumnUnit.Percentage:
sizes[index] = value;
break;
case ColumnUnit.Pixel:
sizes[index] = (value / this.tableWidth) * 100;
break;
}
// update the instance variable
return sizes;
}
/** Resize a column by a specific pixel amount */
resizeColumn(index, delta) {
// get the sibling column that will also be resized
const sibling = this.getSiblingColumn(index);
// create a new array for the sizes
let columns = [...this.columns];
// resize the column to the desired size
columns = this.setColumnWidth(index, Math.round(this.getColumnWidth(index, ColumnUnit.Pixel) + delta), ColumnUnit.Pixel);
columns = this.setColumnWidth(sibling, Math.round(this.getColumnWidth(sibling, ColumnUnit.Pixel)), ColumnUnit.Pixel, columns);
// if the move is not possible then stop here
if (!this.isWidthValid(index, this.getColumnWidth(index, ColumnUnit.Pixel, columns))) {
return;
}
// store the new sizes
this.columns = columns;
// emit the resize event for each column
this.onResize$.next();
}
/** Get the next column in the sequence of columns */
getSiblingColumn(index) {
// find the first sibling that is not disabled
for (let idx = index + 1; idx < this.columns.length; idx++) {
const sibling = this.getColumn(idx);
if (!sibling || !sibling.disabled) {
return idx;
}
}
return null;
}
/** Return true if this column is above the minimum width */
isWidthValid(index, width) {
// get the column at a given position
const column = this.getColumn(index);
// determine if the specified width is greater than the min width
return column && width >= column.minWidth;
}
getColumn(index) {
return this._columns ? this._columns.toArray()[index] : null;
}
getColumnDisabled(index) {
return this.getColumn(index) ? this.getColumn(index).disabled : false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableExpandingTableService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableExpandingTableService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableExpandingTableService, decorators: [{
type: Injectable
}] });
var ColumnUnit;
(function (ColumnUnit) {
ColumnUnit[ColumnUnit["Pixel"] = 0] = "Pixel";
ColumnUnit[ColumnUnit["Percentage"] = 1] = "Percentage";
})(ColumnUnit || (ColumnUnit = {}));
class ResizableExpandingTableDirective extends BaseResizableTableDirective {
constructor() {
super();
this._platformId = inject(PLATFORM_ID);
/** Has horizontal overflow */
this._overflowX = false;
}
ngAfterViewInit() {
if (isPlatformBrowser(this._platformId)) {
const tableHeaders = this._elementRef.nativeElement.querySelectorAll('thead > tr');
for (const body of Array.from(this._elementRef.nativeElement.tBodies)) {
fromEvent(body, 'scroll')
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
Array.from(tableHeaders).forEach(thead => this._renderer.setStyle(thead, 'margin-left', `-${body.scrollLeft}px`));
});
}
/** checks if the table is resizing and allows for a class to be added for when moving from
overflow to no overflow */
merge(this._table.onResize$, this.columns.changes)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this._overflowX =
this._elementRef.nativeElement.tBodies[0].scrollWidth >
this._elementRef.nativeElement.tBodies[0].offsetWidth;
});
}
}
/**
* If this is being used within a modal the table width may initially be zero. This can cause some issues when it does actually appear
* visibily on screen. We should only setup the table once we actually have a width/
*/
onTableReady() {
// if we have already initialised or the table width is currently 0 then do nothing
if (this._initialised || this.getScrollWidth() === 0) {
// if the table has been initialized but the width is now 0
// for example, due to the element being hidden (eg. in a collapsed accordion)
// we would need to re-run this logic whenever the width is back over 0
// to do this we can mark the table as not having been initialized
if (this._initialised && this.getScrollWidth() === 0) {
this._initialised = false;
}
return;
}
// ensure we initially set the table width
this._table.tableWidth = this.getScrollWidth();
// set the columns - prevent expression changed error
Promise.resolve().then(() => {
// initially set the columns
this._table.setColumns(this.columns);
// force relayout to occur to ensure the UI is consistent with the internal state
this.updateLayout();
});
// watch for any future changes to the columns
this.columns.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(() => Promise.resolve().then(() => this._table.setColumns(this.columns)));
this._initialised = true;
}
/** Force the layout to recalculate */
updateLayout() {
Promise.resolve().then(() => this.columns.forEach((_column, index) => this._table.resizeColumn(index, 0)));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableExpandingTableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ResizableExpandingTableDirective, isStandalone: false, selector: "[uxResizableExpandingTable]", host: { properties: { "class.ux-resizable-expanding-table-overflow": "_overflowX" }, classAttribute: "ux-resizable-expanding-table" }, providers: [
{
provide: RESIZABLE_TABLE_SERVICE_TOKEN,
useClass: ResizableExpandingTableService,
},
], queries: [{ propertyName: "columns", predicate: ResizableTableColumnComponent, descendants: true }], exportAs: ["ux-resizable-expanding-table"], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableExpandingTableDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxResizableExpandingTable]',
exportAs: 'ux-resizable-expanding-table',
providers: [
{
provide: RESIZABLE_TABLE_SERVICE_TOKEN,
useClass: ResizableExpandingTableService,
},
],
host: {
class: 'ux-resizable-expanding-table',
'[class.ux-resizable-expanding-table-overflow]': '_overflowX',
},
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { columns: [{
type: ContentChildren,
args: [ResizableTableColumnComponent, { descendants: true }]
}] } });
class ResizableTableDirective extends BaseResizableTableDirective {
constructor() {
super();
// we should hide any horizontal overflow when we are resizing
this._table.isResizing$.pipe(takeUntil(this._onDestroy)).subscribe(this.setOverflow.bind(this));
}
/**
* If this is being used within a modal the table width may initially be zero. This can cause some issues when it does actually appear
* visibily on screen. We should only setup the table once we actually have a width/
*/
onTableReady() {
// if we have already initialised or the table width is currently 0 then do nothing
if (this._initialised || this.getScrollWidth() === 0) {
return;
}
// ensure we initially set the table width
this._table.tableWidth = this.getScrollWidth();
// set the columns - prevent expression changed error
Promise.resolve().then(() => {
// initially set the columns
this._table.setColumns(this.columns);
// force relayout to occur to ensure the UI is consistent with the internal state
this.updateLayout();
});
// watch for any future changes to the columns
this.columns.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(() => Promise.resolve().then(() => this._table.setColumns(this.columns)));
this._initialised = true;
}
/** Force the layout to recalculate */
updateLayout() {
Promise.resolve().then(() => this.columns.forEach((_column, index) => this._table.resizeColumn(index, 0)));
}
/**
* We should hide any horizontal overflow whenever we are resizing, this is because when we are dragging a column
* we must set the column widths in pixel values as percentages cause some jankiness when moving them. However pixel
* values are less precise and can in some cases cause overflow, so we should hide overflow when we are resizing
*/
setOverflow(isResizing) {
Array.from(this._elementRef.nativeElement.tBodies).forEach(tbody => this._renderer.setStyle(tbody, 'overflow-x', isResizing ? 'hidden' : null));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ResizableTableDirective, isStandalone: false, selector: "[uxResizableTable]", host: { classAttribute: "ux-resizable-table" }, providers: [
{
provide: RESIZABLE_TABLE_SERVICE_TOKEN,
useClass: ResizableTableService,
},
], queries: [{ propertyName: "columns", predicate: ResizableTableColumnComponent, descendants: true }], exportAs: ["ux-resizable-table"], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResizableTableDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxResizableTable]',
exportAs: 'ux-resizable-table',
providers: [
{
provide: RESIZABLE_TABLE_SERVICE_TOKEN,
useClass: ResizableTableService,
},
],
host: {
class: 'ux-resizable-table',
},
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { columns: [{
type: ContentChildren,
args: [ResizableTableColumnComponent, { descendants: true }]
}] } });
class TableModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TableModule, declarations: [ColumnPickerComponent,
ResizableTableDirective,
ResizableExpandingTableDirective,
ResizableTableColumnComponent,
ResizableTableCellComponent], imports: [A11yModule,
AccessibilityModule,
CdkTreeModule,
CommonModule,
DragModule,
IconModule,
ResizeModule,
ReorderableModule,
SelectionModule], exports: [ColumnPickerComponent,
ResizableTableDirective,
ResizableExpandingTableDirective,
ResizableTableColumnComponent,
ResizableTableCellComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TableModule, imports: [A11yModule,
AccessibilityModule,
CdkTreeModule,
CommonModule,
DragModule,
IconModule,
ResizeModule,
ReorderableModule,
SelectionModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TableModule, decorators: [{
type: NgModule,
args: [{
imports: [
A11yModule,
AccessibilityModule,
CdkTreeModule,
CommonModule,
DragModule,
IconModule,
ResizeModule,
ReorderableModule,
SelectionModule,
],
declarations: [
ColumnPickerComponent,
ResizableTableDirective,
ResizableExpandingTableDirective,
ResizableTableColumnComponent,
ResizableTableCellComponent,
],
exports: [
ColumnPickerComponent,
ResizableTableDirective,
ResizableExpandingTableDirective,
ResizableTableColumnComponent,
ResizableTableCellComponent,
],
}]
}] });
let uniqueId = 0;
class TimelineEventComponent {
constructor() {
/** Define the id for the event */
this.id = `ux-timeline-event-${uniqueId++}`;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TimelineEventComponent, isStandalone: false, selector: "ux-timeline-event", inputs: { id: "id", badgeColor: "badgeColor", badgeTitle: "badgeTitle" }, ngImport: i0, template: "<div class=\"timeline-badge\" [ngClass]=\"badgeColor\" [attr.aria-describedby]=\"id\">\n <span>{{ badgeTitle }}</span>\n</div>\n\n<div class=\"timeline-panel\" [id]=\"id\">\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineEventComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-timeline-event', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"timeline-badge\" [ngClass]=\"badgeColor\" [attr.aria-describedby]=\"id\">\n <span>{{ badgeTitle }}</span>\n</div>\n\n<div class=\"timeline-panel\" [id]=\"id\">\n <ng-content></ng-content>\n</div>\n" }]
}], propDecorators: { id: [{
type: Input
}], badgeColor: [{
type: Input
}], badgeTitle: [{
type: Input
}] } });
class TimelineComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TimelineComponent, isStandalone: false, selector: "ux-timeline", ngImport: i0, template: "<div class=\"timeline\">\n <div class=\"timeline-connector\"></div>\n <ux-icon class=\"timeline-arrow\" name=\"chevron-down\"></ux-icon>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-timeline', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"timeline\">\n <div class=\"timeline-connector\"></div>\n <ux-icon class=\"timeline-arrow\" name=\"chevron-down\"></ux-icon>\n <ng-content></ng-content>\n</div>\n" }]
}] });
class TimelineModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TimelineModule, declarations: [TimelineComponent, TimelineEventComponent], imports: [CommonModule, IconModule], exports: [TimelineComponent, TimelineEventComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineModule, imports: [CommonModule, IconModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, IconModule],
exports: [TimelineComponent, TimelineEventComponent],
declarations: [TimelineComponent, TimelineEventComponent],
}]
}] });
const TOGGLESWITCH_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ToggleSwitchComponent),
multi: true,
};
let uniqueToggleSwitchId = 0;
class ToggleSwitchComponent {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
/** Provide a default unique id value for the toggle switch */
this._toggleSwitchId = `ux-toggleswitch-${++uniqueToggleSwitchId}`;
/** Specify a unique id for the element. */
this.id = this._toggleSwitchId;
/** Binding for the state of the switch; `true` for "on" and `false` for "off." */
this.value = false;
/** Specify a tabindex. */
this.tabindex = 0;
/** If set to `false` the switch will not be updated when clicking on it, can be used if something else is updating the state of the switch. */
this.clickable = true;
/** If this value is set to `true` then the toggle switch will be disabled. */
this.disabled = false;
/** Specify an aria label for the input element */
this.ariaLabel = '';
/** Specify an aria labelledby property for the input element */
this.ariaLabelledby = null;
/** Emits when `value` has been changed. */
this.valueChange = new EventEmitter();
/** Determine if the underlying input component has been focused with the keyboard */
this._focused = false;
/** Used to inform Angular forms that the component has been touched */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
/** Used to inform Angular forms that the component value has changed */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
}
toggle() {
if (!this.disabled && this.clickable) {
this.value = !this.value;
// emit the value
this.valueChange.emit(this.value);
// update the value if used within a form control
this.onChangeCallback(this.value);
// mark the component as touched
this.onTouchedCallback();
}
}
writeValue(value) {
this.value = !!value;
this._changeDetector.markForCheck();
}
registerOnChange(fn) {
this.onChangeCallback = fn;
}
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
/** Focus the input element */
focus(origin) {
this._focusIndicator.focus(origin);
}
setInputTabIndex(tabindex) {
this.tabindex = tabindex;
this._changeDetector.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: ToggleSwitchComponent, isStandalone: false, selector: "ux-toggleswitch", inputs: { id: "id", name: "name", value: "value", tabindex: "tabindex", clickable: "clickable", disabled: "disabled", ariaLabel: ["aria-label", "ariaLabel"], ariaLabelledby: ["aria-labelledby", "ariaLabelledby"], required: "required" }, outputs: { valueChange: "valueChange" }, providers: [
TOGGLESWITCH_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: ToggleSwitchComponent,
},
], viewQueries: [{ propertyName: "_focusIndicator", first: true, predicate: FocusIndicatorDirective, descendants: true }], ngImport: i0, template: "<label\n [attr.for]=\"(id || _toggleSwitchId) + '-input'\"\n class=\"ux-toggleswitch\"\n [class.ux-toggleswitch-checked]=\"value\"\n [class.ux-toggleswitch-disabled]=\"disabled\"\n [class.ux-toggleswitch-focused]=\"_focused\"\n>\n <input\n #input\n class=\"ux-toggleswitch-input\"\n uxFocusIndicator\n type=\"checkbox\"\n [id]=\"(id || _toggleSwitchId) + '-input'\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [attr.name]=\"name\"\n [tabindex]=\"tabindex\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-checked]=\"value\"\n (indicator)=\"_focused = $event\"\n (change)=\"toggle()\"\n (click)=\"$event.stopPropagation()\"\n />\n\n <div class=\"ux-toggleswitch-container\">\n <div class=\"ux-toggleswitch-bg\"></div>\n <div class=\"ux-toggleswitch-nub\"></div>\n </div>\n\n <span class=\"ux-toggleswitch-label\">\n <ng-content></ng-content>\n </span>\n</label>\n", dependencies: [{ kind: "directive", type: FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-toggleswitch', providers: [
TOGGLESWITCH_VALUE_ACCESSOR,
{
provide: FocusableItemToken,
useExisting: ToggleSwitchComponent,
},
], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<label\n [attr.for]=\"(id || _toggleSwitchId) + '-input'\"\n class=\"ux-toggleswitch\"\n [class.ux-toggleswitch-checked]=\"value\"\n [class.ux-toggleswitch-disabled]=\"disabled\"\n [class.ux-toggleswitch-focused]=\"_focused\"\n>\n <input\n #input\n class=\"ux-toggleswitch-input\"\n uxFocusIndicator\n type=\"checkbox\"\n [id]=\"(id || _toggleSwitchId) + '-input'\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [attr.name]=\"name\"\n [tabindex]=\"tabindex\"\n [attr.aria-label]=\"ariaLabel\"\n [attr.aria-labelledby]=\"ariaLabelledby\"\n [attr.aria-checked]=\"value\"\n (indicator)=\"_focused = $event\"\n (change)=\"toggle()\"\n (click)=\"$event.stopPropagation()\"\n />\n\n <div class=\"ux-toggleswitch-container\">\n <div class=\"ux-toggleswitch-bg\"></div>\n <div class=\"ux-toggleswitch-nub\"></div>\n </div>\n\n <span class=\"ux-toggleswitch-label\">\n <ng-content></ng-content>\n </span>\n</label>\n" }]
}], propDecorators: { id: [{
type: Input
}], name: [{
type: Input
}], value: [{
type: Input
}], tabindex: [{
type: Input
}], clickable: [{
type: Input
}], disabled: [{
type: Input
}], ariaLabel: [{
type: Input,
args: ['aria-label']
}], ariaLabelledby: [{
type: Input,
args: ['aria-labelledby']
}], required: [{
type: Input
}], valueChange: [{
type: Output
}], _focusIndicator: [{
type: ViewChild,
args: [FocusIndicatorDirective]
}] } });
class ToggleSwitchModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchModule, declarations: [ToggleSwitchComponent], imports: [AccessibilityModule, FormsModule], exports: [ToggleSwitchComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchModule, imports: [AccessibilityModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToggleSwitchModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, FormsModule],
exports: [ToggleSwitchComponent],
declarations: [ToggleSwitchComponent],
}]
}] });
class ToolbarSearchButtonDirective {
constructor() {
this._elementRef = inject(ElementRef);
/** Emit whenever the button is clicked */
this.clicked = new EventEmitter();
}
/** Get the width of the button element */
get width() {
return this._elementRef.nativeElement.offsetWidth;
}
clickHandler() {
this.clicked.emit();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ToolbarSearchButtonDirective, isStandalone: false, selector: "[uxToolbarSearchButton]", outputs: { clicked: "clicked" }, host: { listeners: { "click": "clickHandler()" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchButtonDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxToolbarSearchButton]',
standalone: false,
}]
}], propDecorators: { clicked: [{
type: Output
}], clickHandler: [{
type: HostListener,
args: ['click']
}] } });
/* eslint-disable @angular-eslint/no-output-native */
const TOOLBAR_SEARCH_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ToolbarSearchFieldDirective),
multi: true,
};
class ToolbarSearchFieldDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._changeDetector = inject(ChangeDetectorRef);
/** Emit whenever the escape key is pressed */
this.cancel = new EventEmitter();
/** Emit whenever the enter key is pressed */
this.submitted = new EventEmitter();
/** For use with the Forms and ReactiveForms */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onTouchedCallback = () => { };
/** Call this function with the latest value to update ngModel or formControl name */
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.onChangeCallback = () => { };
}
/** Get the current value of the input control */
get text() {
return this._elementRef.nativeElement.value;
}
focus() {
// mark the control as dirty
this.onTouchedCallback();
// focus the input control after a delay to ensure the element is present
requestAnimationFrame(() => this._elementRef.nativeElement.focus());
}
blur() {
// blur the input control after a delay to ensure the element is present
requestAnimationFrame(() => this._elementRef.nativeElement.blur());
}
/** Clear the input, if we have an ngModel reset its value otherwise just set the input value to empty */
clear() {
this.setValue('');
}
onEnter() {
this.submitted.emit(this.text);
}
onEscape() {
this._elementRef.nativeElement.blur();
this.cancel.emit();
}
onInput() {
this.setValue(this.text);
}
/** Update the input value based on ngModel or formControl */
writeValue(value) {
this.setValue(value);
this._changeDetector.markForCheck();
}
/** Register a function to update form control */
registerOnChange(fn) {
this.onChangeCallback = fn;
}
/** Register a function to mark form control as touched */
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
/** Update the value in all required places */
setValue(value) {
// ngModel/form control can set the default value to null or undefined, which can show in the input. Replace with empty string
if (!value) {
value = '';
}
// update the form value if there is one in use
this.onChangeCallback(value);
// update the content of the input control
this._elementRef.nativeElement.value = value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchFieldDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: ToolbarSearchFieldDirective, isStandalone: false, selector: "[uxToolbarSearchField]", outputs: { cancel: "cancel", submitted: "submitted" }, host: { listeners: { "keydown.enter": "onEnter()", "keydown.escape": "onEscape()", "input": "onInput()" } }, providers: [TOOLBAR_SEARCH_VALUE_ACCESSOR], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchFieldDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxToolbarSearchField]',
providers: [TOOLBAR_SEARCH_VALUE_ACCESSOR],
standalone: false,
}]
}], propDecorators: { cancel: [{
type: Output
}], submitted: [{
type: Output
}], onEnter: [{
type: HostListener,
args: ['keydown.enter']
}], onEscape: [{
type: HostListener,
args: ['keydown.escape']
}], onInput: [{
type: HostListener,
args: ['input']
}] } });
class ToolbarSearchComponent {
constructor() {
this._platformId = inject(PLATFORM_ID);
this._elementRef = inject(ElementRef);
this._colorService = inject(ColorService);
this._renderer = inject(Renderer2);
/** The direction in which the search box will expand. If the search button is aligned to the right edge of the container, specify left. */
this.direction = 'right';
/** Whether the color scheme is inverted. For use when the component is hosted on a dark background, e.g. the masthead. */
this.inverse = false;
/** Indicate whether or not the search field should always be expanded */
this.alwaysExpanded = false;
/** Emitted when the expanded state changes */
this.expandedChange = new EventEmitter();
/**
* Emitted when a search query has been submitted, either by pressing enter when the search field has focus, or by clicking the search button
* when the search field contains text. The event contains the search text.
*/
// eslint-disable-next-line @angular-eslint/no-output-native
this.search = new EventEmitter();
/** Store the CSS position value as this may change to absolute */
this._position = 'relative';
/** Store the active background color */
this._backgroundColor = 'transparent';
/** Store the expanded state */
this._expanded = false;
/** Unsubscribe from all subscriptions on component destroy */
this._onDestroy = new Subject();
}
/** Whether the input field is visible. Use this to collapse or expand the control in response to other events. */
set expanded(value) {
this._expanded = value;
this.expandedChange.emit(this.expanded);
if (this.expanded) {
// Set focus on the input when expanded
this.field.focus();
}
else {
// Clear text when contracted
this.field.clear();
// Remove focus (works around an IE issue where the caret remains visible)
this.field.blur();
}
}
get expanded() {
return this.alwaysExpanded || this._expanded;
}
/*
* The background color of the component. Color names from the Color Palette can be used here.
* Specify this when a transparent background would cause display issues, such as background items showing through the search field.
*/
set background(value) {
this._backgroundColor = this._colorService.resolve(value) || 'transparent';
}
/** Return the correct animation based on the expanded state */
get _expandedAnimation() {
return {
value: this.expanded ? 'expanded' : 'collapsed',
params: {
initialWidth: this.button.width + 'px',
},
};
}
ngAfterContentInit() {
// Subscribe to the submitted event on the input field, triggering the search event
this.field.submitted
.pipe(takeUntil(this._onDestroy))
.subscribe((text) => this.search.emit(text));
// Subscribe to cancel events coming from the input field
this.field.cancel.pipe(takeUntil(this._onDestroy)).subscribe(() => (this.expanded = false));
// Subscribe to the button click event
this.button.clicked.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.expanded && this.field.text
? this.search.emit(this.field.text)
: (this.expanded = !this.expanded);
});
// Create placeholder element to avoid changing layout when switching to position: absolute
// If the platform is a server we dont want to do this as we can't access getComputedStyle
if (!isPlatformServer(this._platformId)) {
this.createPlaceholder();
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
/**
* We programmatically created the placeholder node so Angular is not aware of its existence
* so we must manually destroy it otherwise the reference will be retained.
* Note, the `destroyNode` function may be null or undefined as mentioned in the
* Angular API docs (https://angular.io/api/core/Renderer2#destroyNode) so
* we must check that the function is available before attempting to call it
*/
if (this._placeholder && this._renderer && this._renderer.destroyNode) {
this._renderer.destroyNode(this._placeholder);
}
}
animationStart(event) {
if (event.toState === 'expanded') {
this._position = 'absolute';
this.setPlaceholderVisible(true);
}
}
animationDone(event) {
if (event.toState === 'collapsed') {
this._position = 'relative';
this.setPlaceholderVisible(false);
}
}
/** Programmatically create a placeholder element */
createPlaceholder() {
// Create invisible div with the same dimensions
this._placeholder = this._renderer.createElement('div');
this._renderer.setStyle(this._placeholder, 'display', 'none');
this._renderer.setStyle(this._placeholder, 'width', this.button.width + 'px');
this._renderer.setStyle(this._placeholder, 'visibility', 'hidden');
this.setPlaceholderHeight();
// Add as a sibling
this._renderer.insertBefore(this._elementRef.nativeElement.parentNode, this._placeholder, this._elementRef.nativeElement);
}
/** Update the display state of the placeholder node */
setPlaceholderVisible(isVisible) {
if (!this._placeholder) {
return;
}
// Recalculate the height since the layout might not be complete when initially created.
if (isVisible) {
this.setPlaceholderHeight();
}
this._renderer.setStyle(this._placeholder, 'display', isVisible ? 'inline-block' : 'none');
}
/** Set the placeholder height to match the height of this component. */
setPlaceholderHeight() {
const { height } = getComputedStyle(this._elementRef.nativeElement);
this._renderer.setStyle(this._placeholder, 'height', height);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: ToolbarSearchComponent, isStandalone: false, selector: "ux-toolbar-search", inputs: { direction: "direction", inverse: "inverse", alwaysExpanded: "alwaysExpanded", expanded: "expanded", background: "background" }, outputs: { expandedChange: "expandedChange", search: "search" }, host: { listeners: { "@expanded.start": "animationStart($event)", "@expanded.done": "animationDone($event)" }, properties: { "class.expanded": "expanded", "class.left": "direction === \"left\"", "class.right": "direction === \"right\"", "class.inverse": "inverse", "style.position": "_position", "style.background-color": "_backgroundColor", "@expanded": "_expandedAnimation" } }, queries: [{ propertyName: "field", first: true, predicate: ToolbarSearchFieldDirective, descendants: true, static: true }, { propertyName: "button", first: true, predicate: ToolbarSearchButtonDirective, descendants: true }], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, animations: [
trigger('expanded', [
state('collapsed', style({ width: '{{initialWidth}}' }), {
params: { initialWidth: '30px' },
}),
state('expanded', style({ width: '100%' })),
transition('collapsed <=> expanded', [animate('0.3s ease-out')]),
]),
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchComponent, decorators: [{
type: Component,
args: [{
selector: 'ux-toolbar-search',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('expanded', [
state('collapsed', style({ width: '{{initialWidth}}' }), {
params: { initialWidth: '30px' },
}),
state('expanded', style({ width: '100%' })),
transition('collapsed <=> expanded', [animate('0.3s ease-out')]),
]),
],
host: {
'[class.expanded]': 'expanded',
'[class.left]': 'direction === "left"',
'[class.right]': 'direction === "right"',
'[class.inverse]': 'inverse',
'[style.position]': '_position',
'[style.background-color]': '_backgroundColor',
'[@expanded]': '_expandedAnimation',
},
standalone: false,
}]
}], propDecorators: { direction: [{
type: Input
}], inverse: [{
type: Input
}], alwaysExpanded: [{
type: Input
}], expanded: [{
type: Input
}], background: [{
type: Input
}], expandedChange: [{
type: Output
}], search: [{
type: Output
}], field: [{
type: ContentChild,
args: [ToolbarSearchFieldDirective, { static: true }]
}], button: [{
type: ContentChild,
args: [ToolbarSearchButtonDirective, { static: false }]
}], animationStart: [{
type: HostListener,
args: ['@expanded.start', ['$event']]
}], animationDone: [{
type: HostListener,
args: ['@expanded.done', ['$event']]
}] } });
const DECLARATIONS$3 = [
ToolbarSearchComponent,
ToolbarSearchFieldDirective,
ToolbarSearchButtonDirective,
];
class ToolbarSearchModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchModule, declarations: [ToolbarSearchComponent,
ToolbarSearchFieldDirective,
ToolbarSearchButtonDirective], imports: [CommonModule], exports: [ToolbarSearchComponent,
ToolbarSearchFieldDirective,
ToolbarSearchButtonDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToolbarSearchModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule],
exports: DECLARATIONS$3,
declarations: DECLARATIONS$3,
providers: [],
}]
}] });
class VirtualForService {
constructor() {
/** Store the size of each item */
this.itemSize = 0;
/** Emit the current dataset */
this.dataset = new ReplaySubject(1);
/** Emit the visible range */
this.range = new ReplaySubject(1);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForService, decorators: [{
type: Injectable
}] });
/**
* This implementation is inspired by the CDK virtual for:
* https://github.com/angular/material2/blob/master/src/cdk/scrolling/virtual-for-of.ts
* However the CDK requires a container component which limits use in places such
* as fixed header tables, so this is a more generic implementation that does not
* require a parent element but instead uses an attribute on the parent container instead
*/
class VirtualForDirective {
/** Store the list of items to display */
set uxVirtualForOf(dataset) {
// emit the latest dataset
this._virtualScroll.dataset.next(dataset);
// store a local version of the dataset
this._dataset = dataset;
// if this is an update and not the initial dataset then we should
// forcibly redraw the list of items. In cases where the length of
// the dataset change would trigger a re-renderer as the scroll position
// would change, however if we are performing sorting then it would not
// so we must ensure we update everytime the dataset changes.
if (this._renderedRange) {
this.onRangeChange();
}
}
constructor() {
/** A reference to the container element where we will insert elements. */
this._viewContainerRef = inject(ViewContainerRef);
/** The template for all items */
this._templateRef = inject(TemplateRef);
/** Gets the set of Angular differs for detecting changes. */
this._differs = inject(IterableDiffers);
/** Get the renderer to perform DOM manipulation */
this._renderer = inject(Renderer2);
this._changeDetector = inject(ChangeDetectorRef);
/** A service to share values between the container and child elements */
this._virtualScroll = inject(VirtualForService, {
optional: true,
});
/** Provide a trackBy function to optimize rendering */
this.uxVirtualForTrackBy = this.defaultTrackBy;
/** Indicate whether we need to perform a view update */
this._isDirty = false;
/** Store a cache of recently disposed views for reuse */
this._templateCache = [];
/** Limit the size of the cache as it can use a lot of memory */
this._cacheSize = 20;
/** Unsubscribe from all observables */
this._onDestroy = new Subject();
// While marked as optional, it isn't. We do this so we can provide a more helpful error message
if (!this._virtualScroll) {
throw new Error('The "uxVirtualFor" directive requires the "uxVirtualForContainer" directive to be added to the parent element.');
}
}
ngOnInit() {
// update the UI whenever the range changes
this._virtualScroll.range
.pipe(distinctUntilChanged(this.isRangeSame), takeUntil(this._onDestroy))
.subscribe(range => {
this._renderedRange = range;
this.onRangeChange();
this._changeDetector.detectChanges();
});
}
ngDoCheck() {
if (this._isDirty && this._differ) {
// check if there area any changes
const changes = this.getChanges();
if (changes) {
this.applyChanges(changes);
}
else {
this.updateContexts();
}
// now that we have rendered any change we should store this so we don't perform unneeded updates
this._isDirty = false;
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** If an itemSize is not specified we need to calculate it */
getHeight(context, length) {
// create a temporary view
const view = this.createView(0);
// set the implicit value to the item value
view.context.$implicit = context;
view.context.count = length;
view.context.even = true;
view.context.odd = false;
view.context.first = true;
view.context.last = length === 1;
// run change detection
view.detectChanges();
// get the size of the view
const height = view.rootNodes[0].offsetHeight;
// destroy the view
this._viewContainerRef.remove(0);
view.destroy();
return height;
}
/** Determine if the range has changed (performance optimization) */
isRangeSame(previous, current) {
return previous.start === current.start && previous.end === current.end;
}
onRangeChange() {
// store the visible range
this._renderedItems = this._dataset.slice(this._renderedRange.start, this._renderedRange.end);
// create the Angular differ if we haven't previously done so
if (!this._differ) {
this._differ = this._differs.find(this._renderedItems).create(this.uxVirtualForTrackBy);
}
// mark the view for re-render
this._isDirty = true;
}
/** Determine which items have changed */
getChanges() {
return this._differ.diff(this._renderedItems);
}
/** Insert, move and remove any items within the view */
applyChanges(changes) {
// Go through each changes and either add or rearrange accordingly
changes.forEachOperation((record, previousIndex, currentIndex) => {
// check if a new item was added
if (previousIndex === null) {
// create the new embedded view
const view = this.createView(currentIndex);
// set the implicit value to the item value
view.context.$implicit = record.item;
}
else if (currentIndex === null) {
// check if the item should be removed
const view = this._viewContainerRef.detach(currentIndex);
const index = this._viewContainerRef.indexOf(view);
// if there is space in the cache then store the detached view
if (this._templateCache.length < this._cacheSize) {
this._templateCache.push(view);
}
else {
index === -1 ? view.destroy() : this._viewContainerRef.remove(index);
}
}
else {
// the position of the item has changed
// get the view from its current position
const view = this._viewContainerRef.get(previousIndex);
// move it to the new position
this._viewContainerRef.move(view, currentIndex);
// update the implicit value (the rest will stay the same)
view.context.$implicit = record.item;
}
});
// Ensure the implicit value is correct for any items whose identity changed
changes.forEachIdentityChange((record) => {
const view = this._viewContainerRef.get(record.currentIndex);
if (view) {
view.context.$implicit = record.item;
}
});
this.updateContexts();
}
updateContexts() {
// update all the other context properties
for (let idx = 0; idx < this._viewContainerRef.length; idx++) {
// get the view at a given position
const view = this._viewContainerRef.get(idx);
// update the properties
view.context.index = this._renderedRange.start + idx;
view.context.count = this._dataset.length;
view.context.first = view.context.index === 0;
view.context.last = view.context.index === view.context.count - 1;
view.context.even = view.context.index % 2 === 0;
view.context.odd = !view.context.even;
// update the position in the DOM
view.rootNodes.forEach((node) => {
this._renderer.setStyle(node, 'position', 'absolute');
this._renderer.setStyle(node, 'width', '100%');
this._renderer.setStyle(node, 'top', '0');
this._renderer.setStyle(node, 'transform', `translateY(${view.context.index * this._virtualScroll.itemSize}px`);
});
view.detectChanges();
}
}
createView(index) {
// get a checked EmbeddedViewRef is there is one
const cachedTemplate = this._templateCache.pop();
if (cachedTemplate) {
// replace existing context with the defaults
cachedTemplate.context.$implicit = null;
cachedTemplate.context.index = -1;
cachedTemplate.context.count = -1;
cachedTemplate.context.first = false;
cachedTemplate.context.last = false;
cachedTemplate.context.even = false;
cachedTemplate.context.odd = false;
// insert the view
this._viewContainerRef.insert(cachedTemplate, index);
// return the cached EmbeddedViewRef
return cachedTemplate;
}
// otherwise create a new view and insert it
return this._viewContainerRef.createEmbeddedView(this._templateRef, {
$implicit: null,
index: -1,
count: -1,
first: false,
last: false,
even: false,
odd: false,
}, index);
}
defaultTrackBy(index) {
return index;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: VirtualForDirective, isStandalone: false, selector: "[uxVirtualFor][uxVirtualForOf]", inputs: { uxVirtualForOf: "uxVirtualForOf", uxVirtualForTrackBy: "uxVirtualForTrackBy" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxVirtualFor][uxVirtualForOf]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { uxVirtualForOf: [{
type: Input
}], uxVirtualForTrackBy: [{
type: Input
}] } });
class VirtualForContainerComponent {
constructor() {
/** Get the ElementRef of the container element */
this._elementRef = inject(ElementRef);
/** A service to share values between the container and child elements */
this._virtualScroll = inject(VirtualForService);
/** Handle key presses if there is a tabbable list */
this._tabbableList = inject(TabbableListService, { optional: true, self: true });
/** Keep a local reference of the dataset */
this._dataset = [];
/** Indicate if the component has finished initialising */
this._initialized = false;
/** Unsubscribe from all observables */
this._onDestroy = new Subject();
}
/** Define the height of each virtual item */
set itemSize(itemSize) {
this._virtualScroll.itemSize = itemSize;
if (this._initialized) {
requestAnimationFrame(() => {
this.updateContainer();
this.virtualFor.updateContexts();
});
}
}
get itemSize() {
return this._virtualScroll.itemSize;
}
/** Determine if this is a table */
get _isTable() {
return (this._elementRef.nativeElement.tagName === 'TABLE' ||
this._elementRef.nativeElement.tagName === 'TBODY');
}
/** Determine if this is a list */
get _isList() {
return (this._elementRef.nativeElement.tagName === 'OL' ||
this._elementRef.nativeElement.tagName === 'UL');
}
ngAfterViewInit() {
// subscribe to changes to the dataset
this._virtualScroll.dataset.pipe(takeUntil(this._onDestroy)).subscribe(dataset => {
// store the latest dataset
this._dataset = dataset;
// update the container properties
requestAnimationFrame(() => {
this.updateContainer();
// mark the component as ready
this._initialized = true;
});
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
updateContainer() {
if (this.itemSize === 0 && this._dataset.length > 0) {
this.itemSize = this.virtualFor.getHeight(this._dataset[0], this._dataset.length);
}
// calculate the total height of all the items
this._totalHeight = this._dataset.length * this.itemSize;
// get the actual height of the container element
const height = this.getContainerHeight();
// determine the number of items it takes to fill the container height (multiply by 2 to give us some buffer items)
const itemCount = Math.ceil((height / this.itemSize) * 2);
/** Determine the number of items we have as a top buffer */
const topBufferCount = Math.ceil((height / this.itemSize) * 0.5);
// get the scroll offset
const scrollOffset = this.getScrollOffset();
// determine the start index based on the scroll offset
const startIdx = Math.max(Math.floor(scrollOffset / this.itemSize) - Math.floor(topBufferCount), 0);
// determine the end index based on the start and the number of items to display
const endIdx = Math.min(startIdx + itemCount, this._dataset.length);
// update the range
this._range = { start: startIdx, end: endIdx };
// emit the new visible range
this._virtualScroll.range.next(this._range);
}
/** If cells are automatically getting their height detected you may want to update the size */
recalculateCellSize() {
this.itemSize = 0;
}
onKeydown(event, keyCode) {
if (!this._tabbableList) {
return;
}
switch (keyCode) {
case PAGE_UP:
this._tabbableList.focusKeyManager.setFirstItemActive();
event.preventDefault();
break;
case PAGE_DOWN:
this._tabbableList.focusKeyManager.setLastItemActive();
event.preventDefault();
break;
case HOME:
// ensure the QueryList doesn't do any updates until we have finished
this._tabbableList.shouldFocusOnChange = false;
// scroll to the top of the container
this._elementRef.nativeElement.scrollTop = 0;
// after the update the activate the first item
requestAnimationFrame(() => {
this._tabbableList.focusKeyManager.setFirstItemActive();
this._tabbableList.shouldFocusOnChange = true;
});
event.preventDefault();
break;
case END:
// ensure the QueryList doesn't do any updates until we have finished
this._tabbableList.shouldFocusOnChange = false;
// scroll to the bottom of the container
this._elementRef.nativeElement.scrollTop = this._elementRef.nativeElement.scrollHeight;
// after the update the activate the last item
requestAnimationFrame(() => {
this._tabbableList.focusKeyManager.setLastItemActive();
this._tabbableList.shouldFocusOnChange = true;
});
event.preventDefault();
break;
}
}
getScrollOffset() {
return this._elementRef.nativeElement.scrollTop;
}
getContainerHeight() {
return this._elementRef.nativeElement.clientHeight;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: VirtualForContainerComponent, isStandalone: false, selector: "[uxVirtualForContainer]", inputs: { itemSize: "itemSize" }, host: { listeners: { "scroll": "updateContainer()", "keydown": "onKeydown($event,$event.keyCode)" }, properties: { "style.position": "\"relative\"" } }, providers: [VirtualForService], queries: [{ propertyName: "virtualFor", first: true, predicate: VirtualForDirective, descendants: true }], ngImport: i0, template: "<!-- Display the appropriate top spacer -->\n@if (_isTable) {\n <tr\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></tr>\n}\n\n@if (_isList) {\n <li\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></li>\n}\n\n@if (!_isTable && !_isList) {\n <div\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></div>\n}\n\n<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualForContainerComponent, decorators: [{
type: Component,
args: [{ selector: '[uxVirtualForContainer]', providers: [VirtualForService], host: {
'[style.position]': '"relative"',
}, standalone: false, template: "<!-- Display the appropriate top spacer -->\n@if (_isTable) {\n <tr\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></tr>\n}\n\n@if (_isList) {\n <li\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></li>\n}\n\n@if (!_isTable && !_isList) {\n <div\n class=\"ux-virtual-scroll-spacer\"\n [style.height.px]=\"_totalHeight\"\n [style.padding.px]=\"0\"\n [style.margin.px]=\"0\"\n [style.border]=\"'none'\"\n ></div>\n}\n\n<ng-content></ng-content>\n" }]
}], propDecorators: { itemSize: [{
type: Input
}], virtualFor: [{
type: ContentChild,
args: [VirtualForDirective, { static: false }]
}], updateContainer: [{
type: HostListener,
args: ['scroll']
}], onKeydown: [{
type: HostListener,
args: ['keydown', ['$event', '$event.keyCode']]
}] } });
class VirtualScrollCellDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollCellDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: VirtualScrollCellDirective, isStandalone: false, selector: "[uxVirtualScrollCell]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollCellDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxVirtualScrollCell]',
standalone: false,
}]
}] });
class VirtualScrollLoadButtonDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollLoadButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: VirtualScrollLoadButtonDirective, isStandalone: false, selector: "[uxVirtualScrollLoadButton]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollLoadButtonDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxVirtualScrollLoadButton]',
standalone: false,
}]
}] });
class VirtualScrollLoadingDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollLoadingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: VirtualScrollLoadingDirective, isStandalone: false, selector: "[uxVirtualScrollLoading]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollLoadingDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxVirtualScrollLoading]',
standalone: false,
}]
}] });
class VirtualScrollComponent {
constructor() {
this.resizeService = inject(ResizeService);
this._elementRef = inject(ElementRef);
/** Provide the collection of items to display */
this.collection = Observable.create();
/** Indicate whether pages should be loaded on scroll or button click */
this.loadOnScroll = true;
/** Emit when we need to load another page */
this.loading = new EventEmitter();
this.cells = new BehaviorSubject([]);
this.scrollTop = 0;
this.isLoading = false;
this.pageNumber = 0;
this.data = [];
this.loadingComplete = false;
this._buffer = 5;
this._onDestroy = new Subject();
// watch for any future changes to size
this.resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(event => (this._height = event.height));
}
ngOnInit() {
if (!this.cellHeight) {
throw new Error('Virtual Scroll Component requires "cellHeight" property to be defined.');
}
// subscribe to the collection
this.setupObservable();
// load the first page of data
this.loadNextPage();
}
ngAfterContentInit() {
// re-render cells now that we can display any loading indicator or loading button
this.renderCells();
}
ngOnChanges(changes) {
if (changes.collection &&
changes.collection.currentValue !== changes.collection.previousValue &&
!changes.collection.isFirstChange()) {
this.setupObservable();
this.reset();
}
}
ngOnDestroy() {
this._subscription.unsubscribe();
this._onDestroy.next();
this._onDestroy.complete();
}
setupObservable() {
// if there is a current subscription, unsubscribe
if (this._subscription && this._subscription.unsubscribe) {
this._subscription.unsubscribe();
}
this._subscription = this.collection.subscribe(collection => {
this.data = [...this.data, ...collection];
this.renderCells();
this.isLoading = false;
}, null, () => {
this.loadingComplete = true;
});
}
renderCells() {
this.cells.next(this.getVisibleCells());
if (this.loadOnScroll && !this.isLoading && !this.loadingComplete) {
const remainingScroll = this._elementRef.nativeElement.scrollHeight -
(this._elementRef.nativeElement.scrollTop + this._elementRef.nativeElement.clientHeight);
// if the current cells take up less than the height of the component then load the next page
if (remainingScroll <= this._elementRef.nativeElement.clientHeight) {
this.loadNextPage();
}
}
}
getVisibleCells() {
// store the initial element height
if (!this._height) {
this._height = this._elementRef.nativeElement.offsetHeight;
}
// perform some calculations
const scrollTop = this._elementRef.nativeElement.scrollTop;
const startCell = Math.floor(scrollTop / this.cellHeight);
const endCell = Math.ceil(this._height / this.cellHeight);
// we want to add some buffer cells on both the top and bottom of the visible list
const startBuffer = Math.max(0, startCell - this._buffer);
const endBuffer = startCell + (startCell - startBuffer) + Math.min(this.data.length, endCell + this._buffer);
// update the scroll position
this.scrollTop =
scrollTop - (scrollTop % this.cellHeight) - (startCell - startBuffer) * this.cellHeight;
// return a sublist of items visible on the screen
const cells = this.data.slice(startBuffer, endBuffer);
// now map these cells to a virtual cell interface
return cells.map((cell, index) => ({ data: cell, index: startBuffer + index }));
}
getTotalHeight() {
return this.cellHeight * this.data.length;
}
loadNextPage() {
this.isLoading = true;
this.loading.next(this.pageNumber);
this.pageNumber++;
}
reset() {
// reset all values
this.scrollTop = 0;
this.data = [];
this._height = undefined;
this.pageNumber = 0;
this.loadingComplete = false;
// set scroll position
this._elementRef.nativeElement.scrollTop = 0;
// clear the current cells
this.renderCells();
// reload first page
this.loadNextPage();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: VirtualScrollComponent, isStandalone: false, selector: "ux-virtual-scroll", inputs: { collection: "collection", cellHeight: "cellHeight", loadOnScroll: "loadOnScroll" }, outputs: { loading: "loading" }, host: { listeners: { "scroll": "renderCells()" } }, queries: [{ propertyName: "cellTemplate", first: true, predicate: VirtualScrollCellDirective, descendants: true, read: TemplateRef }, { propertyName: "loadingIndicatorTemplate", first: true, predicate: VirtualScrollLoadingDirective, descendants: true, read: TemplateRef }, { propertyName: "loadButtonTemplate", first: true, predicate: VirtualScrollLoadButtonDirective, descendants: true, read: TemplateRef }], usesOnChanges: true, ngImport: i0, template: "<div class=\"virtual-scroll-content-height\" [style.height.px]=\"getTotalHeight()\"></div>\n<div class=\"virtual-scroll-content\" [style.transform]=\"'translateY(' + scrollTop + 'px)'\">\n <!-- Virtually Render Cells -->\n @for (cell of cells | async; track cell) {\n <ng-container\n *ngTemplateOutlet=\"cellTemplate; context: { cell: cell.data, index: cell.index }\"\n ></ng-container>\n }\n\n <!-- Loading Indicator -->\n @if (loadingIndicatorTemplate && isLoading) {\n <ng-container [ngTemplateOutlet]=\"loadingIndicatorTemplate\"></ng-container>\n }\n\n <!-- Loading Button -->\n @if (loadButtonTemplate && !loadOnScroll && !loadingComplete && !isLoading) {\n <div class=\"virtual-scroll-load-button\" (click)=\"loadNextPage()\">\n <ng-container *ngTemplateOutlet=\"loadButtonTemplate\"></ng-container>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-virtual-scroll', standalone: false, template: "<div class=\"virtual-scroll-content-height\" [style.height.px]=\"getTotalHeight()\"></div>\n<div class=\"virtual-scroll-content\" [style.transform]=\"'translateY(' + scrollTop + 'px)'\">\n <!-- Virtually Render Cells -->\n @for (cell of cells | async; track cell) {\n <ng-container\n *ngTemplateOutlet=\"cellTemplate; context: { cell: cell.data, index: cell.index }\"\n ></ng-container>\n }\n\n <!-- Loading Indicator -->\n @if (loadingIndicatorTemplate && isLoading) {\n <ng-container [ngTemplateOutlet]=\"loadingIndicatorTemplate\"></ng-container>\n }\n\n <!-- Loading Button -->\n @if (loadButtonTemplate && !loadOnScroll && !loadingComplete && !isLoading) {\n <div class=\"virtual-scroll-load-button\" (click)=\"loadNextPage()\">\n <ng-container *ngTemplateOutlet=\"loadButtonTemplate\"></ng-container>\n </div>\n }\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { collection: [{
type: Input
}], cellHeight: [{
type: Input
}], loadOnScroll: [{
type: Input
}], loading: [{
type: Output
}], cellTemplate: [{
type: ContentChild,
args: [VirtualScrollCellDirective, { read: TemplateRef, static: false }]
}], loadingIndicatorTemplate: [{
type: ContentChild,
args: [VirtualScrollLoadingDirective, { read: TemplateRef, static: false }]
}], loadButtonTemplate: [{
type: ContentChild,
args: [VirtualScrollLoadButtonDirective, { read: TemplateRef, static: false }]
}], renderCells: [{
type: HostListener,
args: ['scroll']
}] } });
const DECLARATIONS$2 = [
VirtualScrollComponent,
VirtualScrollLoadingDirective,
VirtualScrollLoadButtonDirective,
VirtualScrollCellDirective,
VirtualForContainerComponent,
VirtualForDirective,
];
class VirtualScrollModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollModule, declarations: [VirtualScrollComponent,
VirtualScrollLoadingDirective,
VirtualScrollLoadButtonDirective,
VirtualScrollCellDirective,
VirtualForContainerComponent,
VirtualForDirective], imports: [AccessibilityModule, CommonModule, ResizeModule], exports: [VirtualScrollComponent,
VirtualScrollLoadingDirective,
VirtualScrollLoadButtonDirective,
VirtualScrollCellDirective,
VirtualForContainerComponent,
VirtualForDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollModule, imports: [AccessibilityModule, CommonModule, ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: VirtualScrollModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule, CommonModule, ResizeModule],
exports: DECLARATIONS$2,
declarations: DECLARATIONS$2,
}]
}] });
class AutoGrowDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
// ensure this is a textarea or else throw error
if (this._elementRef.nativeElement.tagName.toLowerCase() !== 'textarea') {
throw new Error('uxAutoGrow directive can only be used on <textarea> elements.');
}
}
ngAfterViewInit() {
this.update();
}
update() {
// perform sizing
this._renderer.setStyle(this._elementRef.nativeElement, 'overflowY', 'hidden');
this._renderer.setStyle(this._elementRef.nativeElement, 'height', 'auto');
// get the new total height and element height
const { scrollHeight } = this._elementRef.nativeElement;
const { maxHeight } = getComputedStyle(this._elementRef.nativeElement);
// determine what the maximum allowed height is
const maximum = !isNaN(parseFloat(maxHeight)) ? parseFloat(maxHeight) : Infinity;
// if there is a max height specifed we want to show the scrollbars
if (maximum < scrollHeight) {
this._renderer.setStyle(this._elementRef.nativeElement, 'overflowY', 'auto');
this._renderer.setStyle(this._elementRef.nativeElement, 'height', maximum + 'px');
}
else {
this._renderer.setStyle(this._elementRef.nativeElement, 'height', scrollHeight + 'px');
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: AutoGrowDirective, isStandalone: false, selector: "[uxAutoGrow]", host: { listeners: { "input": "update()" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxAutoGrow]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { update: [{
type: HostListener,
args: ['input']
}] } });
class AutoGrowModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowModule, declarations: [AutoGrowDirective], exports: [AutoGrowDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AutoGrowModule, decorators: [{
type: NgModule,
args: [{
exports: [AutoGrowDirective],
declarations: [AutoGrowDirective],
}]
}] });
class BadgeDirective {
constructor() {
this._element = inject(ElementRef);
this._renderer = inject(Renderer2);
this._colorService = inject(ColorService);
this._contrastService = inject(ContrastService);
this._className = 'ux-badge';
this._darkColor = ThemeColor.parse('#000');
this._lightColor = ThemeColor.parse('#FFF');
this._badgeContent = null;
this._badgeColor = this._darkColor;
/**
* Set the badge vertical position in relation to the parent element
*/
this.badgeVerticalPosition = 'above';
/**
* Set the badge horizontal position in relation to the parent element
*/
this.badgeHorizontalPosition = 'after';
/**
* Set if the badge overlaps parent content or flows after parent
*/
this.badgeOverlap = false;
/**
* Badge size (based on CSS styles)
*/
this.badgeSize = 'medium';
/**
* Hide badge from view
*/
this.badgeHidden = false;
}
get badgeContent() {
return this._badgeContent;
}
set badgeContent(badge) {
if (typeof badge === 'number') {
this._badgeContent = badge;
this._isNumber = true;
}
else if (typeof badge === 'string' && badge.replace(/ /g, '').length > 0) {
const subject = badge.trim();
this._isNumber = /^\d+$/.test(subject);
this._badgeContent = subject;
}
else {
this._badgeContent = null;
}
}
/**
* Define the badge background color
*/
get badgeColor() {
return this._badgeColor.toRgba();
}
set badgeColor(color) {
this._badgeColor = this.parseThemeColor(color);
}
/**
* Define the badge border color - if unset there is no border
*/
get badgeBorderColor() {
return this._badgeBorderColor.toRgba();
}
set badgeBorderColor(color) {
this._badgeBorderColor = this.parseThemeColor(color);
}
ngAfterViewInit() {
this._badgeElement = this._renderer.createElement('span');
this._renderer.addClass(this._badgeElement, this._className);
this._renderer.setStyle(this._badgeElement, 'display', 'none');
this.setBadgeColor();
this.setBadgeBorderColor();
this.setBadgeSize();
this.setContent(this._badgeContent, this.badgeMaxValue);
this._renderer.appendChild(this._element.nativeElement, this._badgeElement);
this._renderer.removeStyle(this._badgeElement, 'display');
}
ngOnChanges(changes) {
// if the badge is visible set changed values
if (!this._badgeElement) {
return;
}
// set badge content and get display friendly version of text based on max length and type of val
if (changes.badgeContent || changes.badgeMaxValue) {
const finalText = (changes.badgeContent && changes.badgeContent.currentValue) || this.badgeContent || null;
const maxValue = (changes.badgeMaxValue && changes.badgeMaxValue.currentValue) || this.badgeMaxValue || null;
this.setContent(finalText, maxValue);
}
// set the badge color
if (changes.badgeColor &&
changes.badgeColor.currentValue !== changes.badgeColor.previousValue) {
this.setBadgeColor();
}
// set the badge border color
if (changes.badgeBorderColor &&
changes.badgeBorderColor.currentValue !== changes.badgeBorderColor.previousValue) {
this.setBadgeBorderColor();
}
// set badge size
if (changes.badgeSize && changes.badgeSize.currentValue !== changes.badgeSize.previousValue) {
this.setBadgeSize(changes.badgeSize.previousValue);
}
}
ngOnDestroy() {
if (this._renderer.destroyNode) {
this._renderer.destroyNode(this._badgeElement);
}
}
setContent(content, maxValue) {
if (content && maxValue && maxValue > 0) {
if (this._isNumber) {
const numericValue = typeof content === 'number' ? content : parseInt(content);
if (numericValue > maxValue) {
content = `${maxValue}+`;
}
}
else if (typeof content === 'string' && content.length > maxValue) {
content = `${content.substr(0, maxValue)}…`;
}
}
this._badgeDisplayContent = content;
this._badgeElement.textContent = this._badgeDisplayContent?.toString();
}
setBadgeColor() {
if (this._badgeColor) {
this._renderer.setStyle(this._badgeElement, 'background-color', this._badgeColor.toRgba());
}
else {
this._renderer.removeStyle(this._badgeElement, 'background-color');
}
this._renderer.setStyle(this._badgeElement, 'color', this.determineContentTextColor().toRgba());
}
setBadgeBorderColor() {
if (this._badgeBorderColor) {
this._renderer.setStyle(this._badgeElement, 'border-color', this._badgeBorderColor.toRgba());
}
else {
this._renderer.removeStyle(this._badgeElement, 'border-color');
}
this._renderer.setStyle(this._badgeElement, 'background-clip', this._badgeBorderColor ? 'padding-box' : 'border-box');
}
setBadgeSize(previousSize) {
if (previousSize) {
this._renderer.removeClass(this._badgeElement, `ux-badge-${previousSize}`);
}
this._renderer.addClass(this._badgeElement, `ux-badge-${this.badgeSize}`);
}
determineContentTextColor() {
return this._badgeColor
? ThemeColor.parse(this._contrastService
.getContrastColor(this._badgeColor, this._lightColor, this._darkColor)
.toRgba())
: this._lightColor;
}
parseThemeColor(color) {
if (!color) {
return null;
}
return this._colorService.colorExists(color)
? ThemeColor.parse(this._colorService.resolve(color))
: ThemeColor.parse(color);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BadgeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: BadgeDirective, isStandalone: false, selector: "[uxBadge]", inputs: { badgeContent: ["uxBadge", "badgeContent"], badgeColor: "badgeColor", badgeBorderColor: "badgeBorderColor", badgeVerticalPosition: "badgeVerticalPosition", badgeHorizontalPosition: "badgeHorizontalPosition", badgeOverlap: "badgeOverlap", badgeMaxValue: "badgeMaxValue", badgeSize: "badgeSize", badgeHidden: "badgeHidden" }, host: { properties: { "class.ux-badge-above": "badgeVerticalPosition === \"above\"", "class.ux-badge-below": "badgeVerticalPosition === \"below\"", "class.ux-badge-after": "badgeHorizontalPosition === \"after\"", "class.ux-badge-before": "badgeHorizontalPosition === \"before\"", "class.ux-badge-overlap": "this.badgeOverlap", "class.ux-badge-hidden": "this.badgeHidden" }, classAttribute: "ux-badge-container" }, exportAs: ["ux-badge"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BadgeDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxBadge]',
exportAs: 'ux-badge',
host: {
class: 'ux-badge-container',
'[class.ux-badge-above]': 'badgeVerticalPosition === "above"',
'[class.ux-badge-below]': 'badgeVerticalPosition === "below"',
'[class.ux-badge-after]': 'badgeHorizontalPosition === "after"',
'[class.ux-badge-before]': 'badgeHorizontalPosition === "before"',
},
standalone: false,
}]
}], propDecorators: { badgeContent: [{
type: Input,
args: ['uxBadge']
}], badgeColor: [{
type: Input
}], badgeBorderColor: [{
type: Input
}], badgeVerticalPosition: [{
type: Input
}], badgeHorizontalPosition: [{
type: Input
}], badgeOverlap: [{
type: HostBinding,
args: ['class.ux-badge-overlap']
}, {
type: Input
}], badgeMaxValue: [{
type: Input
}], badgeSize: [{
type: Input
}], badgeHidden: [{
type: HostBinding,
args: ['class.ux-badge-hidden']
}, {
type: Input
}] } });
class BadgeModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BadgeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: BadgeModule, declarations: [BadgeDirective], imports: [ColorServiceModule, AccessibilityModule], exports: [BadgeDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BadgeModule, imports: [ColorServiceModule, AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BadgeModule, decorators: [{
type: NgModule,
args: [{
imports: [ColorServiceModule, AccessibilityModule],
exports: [BadgeDirective],
declarations: [BadgeDirective],
}]
}] });
class FixedHeaderTableDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._resizeService = inject(ResizeService);
/** Emit when the table tries to load more data */
this.tablePaging = new EventEmitter();
/** Apply a class whenever the table has scrolled */
this._hasScrolled = false;
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
}
/** Allow dataset changes to trigger re-layout */
set dataset(_dataset) {
requestAnimationFrame(() => this.setLayout());
}
ngOnInit() {
// add class to the table
this._renderer.addClass(this._elementRef.nativeElement, 'ux-fixed-header-table');
// locate the important elements
this._tableHead = this._elementRef.nativeElement.querySelector('thead');
this._tableBody = this._elementRef.nativeElement.querySelector('tbody');
// bind to scroll events on the table body
this._renderer.listen(this._tableBody, 'scroll', this.onScroll.bind(this));
// resize the table header to account for scrollbar
this.setLayout();
// if a resize occurs perform a relayout (this can be useful when displaying tables in modals)
this._resizeService
.addResizeListener(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.setLayout());
// trigger the loading of the first page
this.tablePaging.emit();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/**
* Get the table element
* Primarily used by column width directive
*/
getTable() {
return this._elementRef.nativeElement;
}
/**
* Update the size of the table header to account for the scrollbar.
* This is important to keep the columns aligned
*/
setLayout() {
if (!this._tableBody || !this._tableHead) {
return;
}
// calculate the size of the scrollbar
const scrollbar = this._tableBody.offsetWidth - this._tableBody.clientWidth;
// add padding to the header to account for this
this._renderer.setStyle(this._tableHead, 'padding-right', scrollbar + 'px');
// set the desired height of the table body
this._renderer.setStyle(this._tableBody, 'height', typeof this.tableHeight === 'number' ? `${this.tableHeight}px` : this.tableHeight);
}
/**
* Handle scroll events
*/
onScroll() {
// determine if we are scrolled to the bottom and if so load the next page
const scrollTop = this._tableBody.scrollTop;
const scrollHeight = this._tableBody.scrollHeight - this._tableBody.offsetHeight;
const delta = Math.max(scrollTop, scrollHeight) - Math.min(scrollTop, scrollHeight);
// its possible for the difference to be a value < 1 when we are at the bottom. Account for this:
if (delta < 1) {
this.tablePaging.emit();
}
// update the class based on the scroll position
this._hasScrolled = scrollTop > 0;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FixedHeaderTableDirective, isStandalone: false, selector: "[uxFixedHeaderTable]", inputs: { dataset: "dataset", tableHeight: "tableHeight" }, outputs: { tablePaging: "tablePaging" }, host: { properties: { "class.ux-fixed-header-table-scrolled": "this._hasScrolled" } }, exportAs: ["ux-fixed-header-table"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFixedHeaderTable]',
exportAs: 'ux-fixed-header-table',
standalone: false,
}]
}], propDecorators: { dataset: [{
type: Input
}], tableHeight: [{
type: Input
}], tablePaging: [{
type: Output
}], _hasScrolled: [{
type: HostBinding,
args: ['class.ux-fixed-header-table-scrolled']
}] } });
class FixedHeaderTableModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableModule, declarations: [FixedHeaderTableDirective], imports: [ResizeModule], exports: [FixedHeaderTableDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableModule, imports: [ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FixedHeaderTableModule, decorators: [{
type: NgModule,
args: [{
imports: [ResizeModule],
exports: [FixedHeaderTableDirective],
declarations: [FixedHeaderTableDirective],
}]
}] });
class FloatLabelDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._renderer = inject(Renderer2);
this._autofillMonitor = inject(AutofillMonitor);
this.mode = 'focus';
this.raised = false;
this._focused = false;
this._eventHandles = [];
this._subscription = new Subscription();
}
set input(input) {
// remove any previous autofill subscriptions
if (this._input) {
this._autofillMonitor.stopMonitoring(this._input);
}
this._subscription.unsubscribe();
this._input = input;
// if the input is null then don't need to subscribe to autofillMonitor
if (!input) {
return;
}
// create a new autofillMonitor subscription
this._subscription = this._autofillMonitor.monitor(input).subscribe(event => {
if (!this.raised && event.isAutofilled) {
this.raised = true;
}
if (this.raised && !event.isAutofilled && !this.hasText()) {
this.raised = false;
}
});
}
get input() {
return this._input;
}
ngOnInit() {
this._eventHandles.push(this._renderer.listen(this.input, 'focus', this.inputFocus.bind(this)), this._renderer.listen(this.input, 'blur', this.inputBlur.bind(this)), this._renderer.listen(this.input, 'input', this.inputChange.bind(this)));
// Check initial input value
this.raised = this.hasText();
// Ensure that the `for` attribute is set
if (!this._elementRef.nativeElement.getAttribute('for') && this.input.getAttribute('id')) {
this._renderer.setAttribute(this._elementRef.nativeElement, 'for', this.input.getAttribute('id'));
}
}
ngOnChanges() {
if (!(this.mode === 'focus' && this._focused)) {
this.raised = this.hasText();
}
}
ngOnDestroy() {
// Unsubscribe event handles
this._eventHandles.forEach(eventHandle => eventHandle());
this._autofillMonitor.stopMonitoring(this._input);
this._subscription.unsubscribe();
}
hasText() {
if (this.value === undefined) {
return !!this.input.value;
}
return !!this.value;
}
inputFocus() {
if (this.mode === 'focus') {
this._focused = true;
this.raised = true;
}
}
inputBlur() {
if (this.mode === 'focus') {
this._focused = false;
this.raised = this.hasText();
}
}
inputChange() {
if (this.mode === 'input') {
this.raised = this.hasText();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FloatLabelDirective, isStandalone: false, selector: "[uxFloatLabel]", inputs: { input: ["uxFloatLabel", "input"], value: "value", mode: "mode" }, host: { properties: { "class.ux-float-label-raised": "this.raised" }, classAttribute: "ux-float-label" }, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxFloatLabel]',
host: {
class: 'ux-float-label',
},
standalone: false,
}]
}], propDecorators: { input: [{
type: Input,
args: ['uxFloatLabel']
}], value: [{
type: Input
}], mode: [{
type: Input
}], raised: [{
type: HostBinding,
args: ['class.ux-float-label-raised']
}] } });
class FloatLabelModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelModule, declarations: [FloatLabelDirective], exports: [FloatLabelDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FloatLabelModule, decorators: [{
type: NgModule,
args: [{
imports: [],
exports: [FloatLabelDirective],
declarations: [FloatLabelDirective],
providers: [],
}]
}] });
class HelpCenterService {
constructor() {
this.items = new BehaviorSubject([]);
}
registerItem(item) {
// get the current items
const items = this.items.getValue();
// add the new item to the list
items.push(item);
// update the observable
this.items.next(items);
}
unregisterItem(item) {
// get the current items
let items = this.items.getValue();
// remove the item being unregistered
items = items.filter(itm => itm !== item);
// update the observable
this.items.next(items);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class HelpCenterItemDirective {
constructor() {
this._helpCenterService = inject(HelpCenterService);
}
ngOnInit() {
// register the item in the service
this._helpCenterService.registerItem(this.uxHelpCenterItem);
}
ngOnDestroy() {
// remove this item when it is destroyed
this._helpCenterService.unregisterItem(this.uxHelpCenterItem);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: HelpCenterItemDirective, isStandalone: false, selector: "[uxHelpCenterItem]", inputs: { uxHelpCenterItem: "uxHelpCenterItem" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxHelpCenterItem]',
standalone: false,
}]
}], propDecorators: { uxHelpCenterItem: [{
type: Input
}] } });
class HelpCenterModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterModule, declarations: [HelpCenterItemDirective], exports: [HelpCenterItemDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HelpCenterModule, decorators: [{
type: NgModule,
args: [{
exports: [HelpCenterItemDirective],
declarations: [HelpCenterItemDirective],
}]
}] });
class HoverActionService {
constructor() {
this.active = new BehaviorSubject(false);
this._focused = false;
this._hovered = false;
this._actions = [];
}
register(action) {
this._actions.push(action);
}
unregister(action) {
this._actions = this._actions.filter(actn => actn !== action);
}
setFocusState(focus) {
this._focused = focus;
this.updateVisibility();
}
setHoverState(hover) {
this._hovered = hover;
this.updateVisibility();
}
updateVisibility() {
this.active.next(this._focused || this._hovered || this.actionHasFocus());
}
actionHasFocus() {
return !!this.getFocusedAction();
}
getFocusedAction() {
return this._actions.find(action => action.focused);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionService, decorators: [{
type: Injectable
}] });
class HoverActionContainerDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._managedFocusContainerService = inject(ManagedFocusContainerService);
this._hoverActionService = inject(HoverActionService);
this.tabindex = 0;
this.active = false;
this._onDestroy = new Subject();
}
ngOnInit() {
// Watch for focus within the container element and manage tabindex of descendants
this._managedFocusContainerService.register(this._elementRef.nativeElement, this);
// Track focus and update state for the child directives
this._managedFocusContainerService
.hasFocus(this._elementRef.nativeElement)
.pipe(takeUntil(this._onDestroy))
.subscribe(active => {
this.active = active;
this._hoverActionService.setFocusState(active);
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this._managedFocusContainerService.unregister(this._elementRef.nativeElement, this);
}
onHover() {
this._hoverActionService.setHoverState(true);
}
onLeave() {
this._hoverActionService.setHoverState(false);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionContainerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: HoverActionContainerDirective, isStandalone: false, selector: "[uxHoverActionContainer]", inputs: { tabindex: "tabindex" }, host: { listeners: { "mouseenter": "onHover()", "mouseleave": "onLeave()" }, properties: { "tabindex": "this.tabindex", "class.hover-action-container-active": "this.active" } }, providers: [HoverActionService], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionContainerDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxHoverActionContainer]',
providers: [HoverActionService],
standalone: false,
}]
}], propDecorators: { tabindex: [{
type: Input
}, {
type: HostBinding,
args: ['tabindex']
}], active: [{
type: HostBinding,
args: ['class.hover-action-container-active']
}], onHover: [{
type: HostListener,
args: ['mouseenter']
}], onLeave: [{
type: HostListener,
args: ['mouseleave']
}] } });
class HoverActionDirective {
constructor() {
this._elementRef = inject(ElementRef);
this._hoverActionService = inject(HoverActionService);
this.focusIndicatorService = inject(FocusIndicatorService);
this.tabindex = 0;
this.active = false;
this.focused = false;
this._onDestroy = new Subject();
// create the focus indicator
this._focusIndicator = this.focusIndicatorService.monitor(this._elementRef.nativeElement);
// register the action
this._hoverActionService.register(this);
// watch for changes to the activeness of the container
this._hoverActionService.active
.pipe(takeUntil(this._onDestroy))
.subscribe(active => (this.active = active));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this._hoverActionService.unregister(this);
this._focusIndicator.destroy();
}
focus() {
this._elementRef.nativeElement.focus();
}
onFocus() {
this.focused = true;
this._hoverActionService.updateVisibility();
}
onBlur() {
this.focused = false;
this._hoverActionService.updateVisibility();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: HoverActionDirective, isStandalone: false, selector: "[uxHoverAction]", inputs: { tabindex: "tabindex" }, host: { listeners: { "focus": "onFocus()", "blur": "onBlur()" }, properties: { "tabindex": "this.tabindex", "class.hover-action-active": "this.active", "class.hover-action-focused": "this.focused" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxHoverAction]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { tabindex: [{
type: Input
}, {
type: HostBinding,
args: ['tabindex']
}], active: [{
type: HostBinding,
args: ['class.hover-action-active']
}], focused: [{
type: HostBinding,
args: ['class.hover-action-focused']
}], onFocus: [{
type: HostListener,
args: ['focus']
}], onBlur: [{
type: HostListener,
args: ['blur']
}] } });
const DECLARATIONS$1 = [HoverActionDirective, HoverActionContainerDirective];
class HoverActionModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: HoverActionModule, declarations: [HoverActionDirective, HoverActionContainerDirective], imports: [AccessibilityModule], exports: [HoverActionDirective, HoverActionContainerDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionModule, imports: [AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HoverActionModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule],
exports: DECLARATIONS$1,
declarations: DECLARATIONS$1,
}]
}] });
class LayoutSwitcherItemDirective {
constructor() {
this._templateRef = inject(TemplateRef);
this._viewContainerRef = inject(ViewContainerRef);
}
getLayout() {
return this._templateRef;
}
getConfig() {
return this._config;
}
activate() {
this._embeddedView = this._viewContainerRef.createEmbeddedView(this._templateRef);
}
deactivate() {
const index = this._viewContainerRef.indexOf(this._embeddedView);
this._viewContainerRef.remove(index);
this._embeddedView = null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: LayoutSwitcherItemDirective, isStandalone: false, selector: "[uxLayoutSwitcherItem]", inputs: { _config: ["uxLayoutSwitcherItem", "_config"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxLayoutSwitcherItem]',
standalone: false,
}]
}], propDecorators: { _config: [{
type: Input,
args: ['uxLayoutSwitcherItem']
}] } });
class LayoutSwitcherDirective {
constructor() {
this.resizeService = inject(ResizeService);
this._elementRef = inject(ElementRef);
this._viewContainerRef = inject(ViewContainerRef);
// watch for changes to the container size
this.resizeService.addResizeListener(this._elementRef.nativeElement).subscribe(event => {
this._width = event.width;
// render the appropriate layout
this.updateActiveLayout();
});
}
ngOnChanges(changes) {
// if the active group has changed then render the appropriate layout
if (changes.group.currentValue !== changes.group.previousValue) {
this.updateActiveLayout();
}
}
getActiveLayout() {
// if there are currently no layouts then do nothing
if (!this._layouts) {
return null;
}
// otherwise find layouts that match the active group and that meet the constraints
return this._layouts
.filter(layout => this.group === layout.getConfig().group)
.find(layout => {
const minWidth = layout.getConfig().minWidth || 0;
const maxWidth = layout.getConfig().maxWidth || Infinity;
return this._width >= minWidth && this._width < maxWidth;
});
}
updateActiveLayout() {
// get the layout that should be shown
const layout = this.getActiveLayout();
// check if we are currently showing the layout
if (this._activeLayout === layout) {
return;
}
// remove the current layout
if (this._activeLayout) {
this._activeLayout.deactivate();
}
// store the new active layout
this._activeLayout = layout;
// if there is an active layout then activate
if (this._activeLayout) {
this._activeLayout.activate();
}
}
ngAfterContentInit() {
// store the initial current element width
this._width = this._elementRef.nativeElement.offsetWidth;
// render the appropriate layout - need a delay as Angular doesn't like changes like this in these lifecycle hooks
requestAnimationFrame(this.updateActiveLayout.bind(this));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: LayoutSwitcherDirective, isStandalone: false, selector: "[uxLayoutSwitcher]", inputs: { group: "group" }, queries: [{ propertyName: "_layouts", predicate: LayoutSwitcherItemDirective }], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxLayoutSwitcher]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { group: [{
type: Input
}], _layouts: [{
type: ContentChildren,
args: [LayoutSwitcherItemDirective]
}] } });
const DECLARATIONS = [LayoutSwitcherDirective, LayoutSwitcherItemDirective];
class LayoutSwitcherModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherModule, declarations: [LayoutSwitcherDirective, LayoutSwitcherItemDirective], imports: [ResizeModule], exports: [LayoutSwitcherDirective, LayoutSwitcherItemDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherModule, imports: [ResizeModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LayoutSwitcherModule, decorators: [{
type: NgModule,
args: [{
imports: [ResizeModule],
exports: DECLARATIONS,
declarations: DECLARATIONS,
providers: [],
}]
}] });
class MenuNavigationService {
constructor() {
/** Store a list of items that belong to this menu */
this.menuItems = [];
/** Store the current active menu item */
this.active$ = new BehaviorSubject(null);
}
/** Add an item to this menu */
register(menuItem) {
this.menuItems = [...this.menuItems, menuItem];
}
/** Remove an item from the list of menu items */
unregister(menuItem) {
this.menuItems = this.menuItems.filter(_menuItem => _menuItem !== menuItem);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationService, decorators: [{
type: Injectable
}] });
class MenuNavigationItemDirective {
constructor() {
this.focusIndicatorService = inject(FocusIndicatorService);
this._elementRef = inject(ElementRef);
this._menuNavigationService = inject(MenuNavigationService);
/** Emit when this menu is activated */
this.activated = new EventEmitter();
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
// register this item with the menu - this allows for nested menus as we each uxMenuNavigation will create its own service
this._menuNavigationService.register(this);
// create the focus indicator
this._focusIndicator = this.focusIndicatorService.monitor(this._elementRef.nativeElement, {
programmaticFocusIndicator: true,
checkChildren: false,
});
/** Subscribe to the current active index */
this._menuNavigationService.active$
.pipe(takeUntil(this._onDestroy), filter(item => item === this))
.subscribe(() => this.setActive());
}
ngOnDestroy() {
this._menuNavigationService.unregister(this);
this._onDestroy.unsubscribe();
this._focusIndicator.destroy();
}
setActive() {
this._elementRef.nativeElement.focus();
this.activated.emit();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuNavigationItemDirective, isStandalone: false, selector: "[uxMenuNavigationItem]", outputs: { activated: "activated" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuNavigationItem]',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { activated: [{
type: Output
}] } });
class MenuNavigationToggleDirective {
/** Define if the menu is open */
get menuOpen() {
return this._menuOpen;
}
set menuOpen(value) {
this._menuOpen = value;
this.menuOpenChange.emit(value);
}
constructor() {
this.elementRef = inject(ElementRef);
this.focusIndicatorService = inject(FocusIndicatorService);
/** Define the position the menu appears relative to the button */
this.menuPosition = 'bottom';
/** Emit when the menu open state changes */
this.menuOpenChange = new EventEmitter();
/** Emits whenever a key that opens the menu is pressed */
this.keyEnter = new EventEmitter();
this._focusIndicator = this.focusIndicatorService.monitor(this.elementRef.nativeElement);
}
ngOnDestroy() {
this._focusIndicator.destroy();
}
focus(origin) {
this._focusIndicator.focus(origin);
}
keydownHandler(event) {
if (this.isKeyMatch(event.which)) {
// Open the menu
this.menuOpen = true;
// Allow the menu to init, then send the event to give it focus
setTimeout(() => this.keyEnter.emit());
event.preventDefault();
event.stopPropagation();
}
}
isKeyMatch(key) {
switch (key) {
case ENTER:
case SPACE:
return true;
case UP_ARROW:
return this.menuPosition === 'top';
case DOWN_ARROW:
return this.menuPosition === 'bottom';
case LEFT_ARROW:
return this.menuPosition === 'left';
case RIGHT_ARROW:
return this.menuPosition === 'right';
}
return false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationToggleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuNavigationToggleDirective, isStandalone: false, selector: "[uxMenuNavigationToggle]", inputs: { menuOpen: "menuOpen", menuPosition: "menuPosition" }, outputs: { menuOpenChange: "menuOpenChange", keyEnter: "keyEnter" }, host: { listeners: { "keydown": "keydownHandler($event)" } }, exportAs: ["uxMenuNavigationToggle"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationToggleDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuNavigationToggle]',
exportAs: 'uxMenuNavigationToggle',
standalone: false,
}]
}], ctorParameters: () => [], propDecorators: { menuOpen: [{
type: Input
}], menuPosition: [{
type: Input
}], menuOpenChange: [{
type: Output
}], keyEnter: [{
type: Output
}], keydownHandler: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
class MenuNavigationDirective {
constructor() {
this._menuNavigationService = inject(MenuNavigationService);
/** Define the position of the toggle button relative to the menu */
this.toggleButtonPosition = 'top';
/** Emit when the menu is no longer focused */
this.navigatedOut = new EventEmitter();
/** Determine if the menu currently has focus */
this._isFocused = false;
/** Unsubscribe from all observables on destroy */
this._onDestroy = new Subject();
}
/** Get the index of the currently active item */
get activeIndex() {
return this.menuItems.indexOf(this._menuNavigationService.active$.value);
}
// get the list of menu items
get menuItems() {
return this._menuNavigationService.menuItems;
}
ngOnInit() {
if (this.toggleButton) {
this.toggleButton.keyEnter
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this.focusFirst());
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
focusFirst() {
this.moveFirst();
}
onFocusIn() {
this._isFocused = true;
}
onFocusOut() {
this._isFocused = false;
}
keydownHandler(event) {
// Only handle events when focus in within the list of menu items
if (this._isFocused === false) {
return;
}
let handled = false;
switch (event.which) {
case UP_ARROW:
this.movePrevious(event);
handled = true;
break;
case DOWN_ARROW:
this.moveNext(event);
handled = true;
break;
case LEFT_ARROW:
if (this.toggleButtonPosition === 'left') {
this.moveToToggleButton(event);
handled = true;
}
break;
case RIGHT_ARROW:
if (this.toggleButtonPosition === 'right') {
this.moveToToggleButton(event);
handled = true;
}
break;
case HOME:
this.moveFirst();
handled = true;
break;
case END:
this.moveLast();
handled = true;
break;
case ESCAPE:
this.moveToToggleButton(event);
handled = true;
break;
}
if (handled) {
event.preventDefault();
event.stopPropagation();
}
}
moveNext(event) {
// Do nothing if there's no active menu item registered
if (this.activeIndex < 0) {
return;
}
const nextIndex = this.activeIndex + 1;
if (nextIndex < this.menuItems.length) {
// Activate the next menu item
// (uxMenuNavigationItem subscribes to this and applies focus if it matches)
this._menuNavigationService.active$.next(this.menuItems[nextIndex]);
}
else {
// Check if focus went out of bounds in the direction of the origin toggle button
if (this.toggleButtonPosition === 'bottom') {
this.moveToToggleButton(event);
}
}
}
movePrevious(event) {
// Do nothing if there's no active menu item registered
if (this.activeIndex < 0) {
return;
}
const nextIndex = this.activeIndex - 1;
if (nextIndex >= 0) {
// Activate the previous menu item
// (uxMenuNavigationItem subscribes to this and applies focus if it matches)
this._menuNavigationService.active$.next(this.menuItems[nextIndex]);
}
else {
// Check if focus went out of bounds in the direction of the origin toggle button
if (this.toggleButtonPosition === 'top') {
this.moveToToggleButton(event);
}
}
}
moveFirst() {
if (this.menuItems.length > 0) {
this._menuNavigationService.active$.next(this.menuItems[0]);
}
}
moveLast() {
if (this.menuItems.length > 0) {
this._menuNavigationService.active$.next(this.menuItems[this.menuItems.length - 1]);
}
}
moveToToggleButton(event) {
if (this.toggleButton) {
this.toggleButton.focus('keyboard');
this.toggleButton.menuOpen = false;
}
this.navigatedOut.emit(event);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: MenuNavigationDirective, isStandalone: false, selector: "[uxMenuNavigation]", inputs: { toggleButton: "toggleButton", toggleButtonPosition: "toggleButtonPosition" }, outputs: { navigatedOut: "navigatedOut" }, host: { listeners: { "focusin": "onFocusIn()", "focusout": "onFocusOut()", "keydown": "keydownHandler($event)" } }, providers: [MenuNavigationService], exportAs: ["uxMenuNavigation"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxMenuNavigation]',
exportAs: 'uxMenuNavigation',
providers: [MenuNavigationService],
standalone: false,
}]
}], propDecorators: { toggleButton: [{
type: Input
}], toggleButtonPosition: [{
type: Input
}], navigatedOut: [{
type: Output
}], onFocusIn: [{
type: HostListener,
args: ['focusin']
}], onFocusOut: [{
type: HostListener,
args: ['focusout']
}], keydownHandler: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
const EXPORTS = [
MenuNavigationDirective,
MenuNavigationItemDirective,
MenuNavigationToggleDirective,
];
class MenuNavigationModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationModule, declarations: [MenuNavigationDirective,
MenuNavigationItemDirective,
MenuNavigationToggleDirective], imports: [AccessibilityModule], exports: [MenuNavigationDirective,
MenuNavigationItemDirective,
MenuNavigationToggleDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationModule, imports: [AccessibilityModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: MenuNavigationModule, decorators: [{
type: NgModule,
args: [{
imports: [AccessibilityModule],
exports: EXPORTS,
declarations: EXPORTS,
}]
}] });
class OverflowDirective {
constructor() {
this._elementRef = inject(ElementRef);
/** Allow overflow to be within a range before emitting */
this.tolerance = 0;
/** Emit when there is a change to the overflow state - horizontal or vertical */
this.uxOverflowObserver = new EventEmitter();
/** Emit when there is a change to overflow on the horizontal axis */
this.uxOverflowHorizontalObserver = new EventEmitter();
/** Emit when there is a change to overflow on the vertical axis */
this.uxOverflowVerticalObserver = new EventEmitter();
/** Store the overflow state on both axis */
this._state = { horizontalOverflow: false, verticalOverflow: false };
/** Unsubscribe from all the observables */
this._onDestroy = new Subject();
}
/** Set up the trigger if specified */
ngOnInit() {
if (this.trigger) {
this.trigger.pipe(takeUntil(this._onDestroy)).subscribe(() => this.checkForOverflow());
}
}
/** Perform an intial check for overflow */
ngAfterViewInit() {
requestAnimationFrame(() => this.checkForOverflow());
}
/** Unsubscribe from the trigger */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Programmatically trigger check for overflow */
checkForOverflow() {
const { offsetWidth, offsetHeight, scrollWidth, scrollHeight } = this._elementRef.nativeElement;
const horizontalOverflow = scrollWidth - offsetWidth > this.tolerance;
const verticalOverflow = scrollHeight - offsetHeight > this.tolerance;
if (horizontalOverflow !== this._state.horizontalOverflow) {
this.uxOverflowHorizontalObserver.emit(horizontalOverflow);
}
if (verticalOverflow !== this._state.verticalOverflow) {
this.uxOverflowVerticalObserver.emit(verticalOverflow);
}
if (horizontalOverflow !== this._state.horizontalOverflow ||
verticalOverflow !== this._state.verticalOverflow) {
this.uxOverflowObserver.emit(horizontalOverflow || verticalOverflow);
}
// store the state
this._state = { horizontalOverflow, verticalOverflow };
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverflowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: OverflowDirective, isStandalone: false, selector: "[uxOverflowObserver], [uxOverflowHorizontalObserver], [uxOverflowVerticalObserver]", inputs: { trigger: "trigger", tolerance: "tolerance" }, outputs: { uxOverflowObserver: "uxOverflowObserver", uxOverflowHorizontalObserver: "uxOverflowHorizontalObserver", uxOverflowVerticalObserver: "uxOverflowVerticalObserver" }, exportAs: ["ux-overflow-observer"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: OverflowDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxOverflowObserver], [uxOverflowHorizontalObserver], [uxOverflowVerticalObserver]',
exportAs: 'ux-overflow-observer',
standalone: false,
}]
}], propDecorators: { trigger: [{
type: Input
}], tolerance: [{
type: Input
}], uxOverflowObserver: [{
type: Output
}], uxOverflowHorizontalObserver: [{
type: Output
}], uxOverflowVerticalObserver: [{
type: Output
}] } });
class ObserversModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ObserversModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ObserversModule, declarations: [OverflowDirective], exports: [OverflowDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ObserversModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ObserversModule, decorators: [{
type: NgModule,
args: [{
exports: [OverflowDirective],
declarations: [OverflowDirective],
}]
}] });
class TreeGridState {
constructor(level, setSize, positionInSet) {
this.level = level;
this.setSize = setSize;
this.positionInSet = positionInSet;
this.loading$ = new BehaviorSubject(false);
}
}
class TreeGridService {
constructor() {
/** The raw table data */
this.data$ = new BehaviorSubject([]);
/** The flattened table data */
this.rows$ = new BehaviorSubject([]);
/** Ensure we destroy all observables correctly */
this._onDestroy = new Subject();
this.data$
.pipe(takeUntil(this._onDestroy))
.subscribe(data => this.rows$.next(this.getFlattenedTree(data)));
}
/** Unsubscribe from all observables */
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
/** Set the expanded state of a row */
async setExpanded(item, expanded) {
if (expanded) {
await this.getChildren(item);
this.insertChildren(item);
}
else {
this.removeChildren(item);
}
}
/** A function to flatten tree data */
getFlattenedTree(data, parent) {
// flatten the nodes at this level
return data.reduce((previous, item, index) => {
item.state = new TreeGridState(parent ? parent.state.level + 1 : 0, data.length, index + 1);
// Convert any child nodes
const children = item.children && item.expanded ? this.getFlattenedTree(item.children, item) : [];
// return the nodes in a flattened array
return [...previous, item, ...children];
}, []);
}
/** Load any children dynamically */
async getChildren(item) {
if (!item.children && this.loadChildren) {
item.state.loading$.next(true);
try {
item.children = await this.getNormalizedChildren(this.loadChildren(item));
}
finally {
item.state.loading$.next(false);
}
}
}
/** We want to support an array, a promise and an observable. This will return all types as a promise */
async getNormalizedChildren(response) {
// if it is already an observable do nothing
if (isObservable(response)) {
return await response.toPromise();
}
// if it is a promise wrap it as an observable
if (response instanceof Promise) {
return await response;
}
// if it is an array then make it an observable
return response;
}
/** Insert the children into the flattened tree at the correct location */
insertChildren(parent) {
if (!parent.children) {
return;
}
const rows = [...this.rows$.getValue()];
const index = rows.indexOf(parent);
if (index < 0) {
return;
}
// Skip duplicates - this could happen if an already expanded child has been inserted
const uniqueChildren = parent.children.filter(child => rows.indexOf(child) === -1);
const childRows = this.getFlattenedTree(uniqueChildren, parent);
rows.splice(index + 1, 0, ...childRows);
this.rows$.next(rows);
}
/** Remove all rows from the flattened tree */
removeChildren(parent) {
const rows = [...this.rows$.getValue()];
const index = rows.indexOf(parent);
if (index < 0) {
return;
}
while (index + 1 < rows.length && rows[index + 1].state.level > parent.state.level) {
rows.splice(index + 1, 1);
}
this.rows$.next(rows);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class TreeGridRowDirective {
constructor() {
this._treeGridService = inject(TreeGridService);
this.expandedChange = new EventEmitter();
this.loading = false;
this._expanded = false;
this._onDestroy = new Subject();
}
set expanded(value) {
const expanded = coerceBooleanProperty(value);
if (expanded !== this._expanded) {
this._expanded = expanded;
this._treeGridService.setExpanded(this.item, expanded);
}
}
get expanded() {
return this._expanded;
}
ngOnInit() {
if (!this.item || !this.item.state) {
throw new Error('uxTreeGridRow should be configured with an object emitted by uxTreeGrid.rows.');
}
this.item.state.loading$
.pipe(takeUntil(this._onDestroy))
.subscribe(loading => (this.loading = loading));
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
collapse(event) {
this.expanded = false;
this.expandedChange.emit(false);
if (event) {
event.preventDefault();
}
}
expand(event) {
// take into account whether or not the item can expanded
if (!this.canExpand) {
return;
}
this.expanded = true;
this.expandedChange.emit(true);
if (event) {
event.preventDefault();
}
}
toggle() {
this.expanded ? this.collapse() : this.expand();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridRowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TreeGridRowDirective, isStandalone: false, selector: "[uxTreeGridRow]", inputs: { item: ["uxTreeGridRow", "item"], canExpand: "canExpand", expanded: "expanded" }, outputs: { expandedChange: "expandedChange" }, host: { listeners: { "keydown.ArrowLeft": "collapse($event)", "keydown.ArrowRight": "expand($event)" }, properties: { "class.treegrid-row": "true", "class.treegrid-row-expanded": "this.expanded", "class.treegrid-row-loading": "this.loading" } }, exportAs: ["uxTreeGridRow"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridRowDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTreeGridRow]',
exportAs: 'uxTreeGridRow',
host: {
'[class.treegrid-row]': 'true',
},
standalone: false,
}]
}], propDecorators: { item: [{
type: Input,
args: ['uxTreeGridRow']
}], canExpand: [{
type: Input
}], expanded: [{
type: Input
}, {
type: HostBinding,
args: ['class.treegrid-row-expanded']
}], expandedChange: [{
type: Output
}], loading: [{
type: HostBinding,
args: ['class.treegrid-row-loading']
}], collapse: [{
type: HostListener,
args: ['keydown.ArrowLeft', ['$event']]
}], expand: [{
type: HostListener,
args: ['keydown.ArrowRight', ['$event']]
}] } });
class TreeGridIndentDirective {
constructor() {
this._row = inject(TreeGridRowDirective);
}
/** The amount each level should be indented by */
set uxTreeGridIndent(value) {
this._indent = coerceNumberProperty(value, 25);
}
get uxTreeGridIndent() {
return this._indent;
}
/** The padding value applied to each level */
get indentation() {
return this._row && this._row.item ? 7 + this._row.item.state.level * this._indent : 7;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridIndentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TreeGridIndentDirective, isStandalone: false, selector: "[uxTreeGridIndent]", inputs: { uxTreeGridIndent: "uxTreeGridIndent" }, host: { properties: { "style.padding-left.px": "this.indentation" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridIndentDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTreeGridIndent]',
standalone: false,
}]
}], propDecorators: { uxTreeGridIndent: [{
type: Input
}], indentation: [{
type: HostBinding,
args: ['style.padding-left.px']
}] } });
class TreeGridDirective {
constructor() {
this._changeDetector = inject(ChangeDetectorRef);
this._treeGridService = inject(TreeGridService);
this.rowsChange = new EventEmitter();
this._onDestroy = new Subject();
}
set data(data) {
this._treeGridService.data$.next(data);
}
set loadChildren(loadChildren) {
this._treeGridService.loadChildren = loadChildren;
}
ngOnInit() {
this._treeGridService.rows$.pipe(takeUntil(this._onDestroy)).subscribe(rows => {
this.rowsChange.emit(rows);
this._changeDetector.detectChanges();
});
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: TreeGridDirective, isStandalone: false, selector: "[uxTreeGrid]", inputs: { data: ["uxTreeGrid", "data"], loadChildren: "loadChildren" }, outputs: { rowsChange: "rowsChange" }, host: { classAttribute: "treegrid" }, providers: [TreeGridService], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridDirective, decorators: [{
type: Directive,
args: [{
selector: '[uxTreeGrid]',
providers: [TreeGridService],
host: {
class: 'treegrid',
},
standalone: false,
}]
}], propDecorators: { data: [{
type: Input,
args: ['uxTreeGrid']
}], loadChildren: [{
type: Input
}], rowsChange: [{
type: Output
}] } });
class TreeGridModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TreeGridModule, declarations: [TreeGridDirective, TreeGridRowDirective, TreeGridIndentDirective], exports: [TreeGridDirective, TreeGridRowDirective, TreeGridIndentDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TreeGridModule, decorators: [{
type: NgModule,
args: [{
declarations: [TreeGridDirective, TreeGridRowDirective, TreeGridIndentDirective],
exports: [TreeGridDirective, TreeGridRowDirective, TreeGridIndentDirective],
}]
}] });
class StringFilterPipe {
transform(items, value) {
if (!items) {
return [];
}
return items.filter(it => it.toLowerCase().indexOf(value.toLowerCase()) >= 0);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: StringFilterPipe, isStandalone: false, name: "stringFilter" }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterPipe }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterPipe, decorators: [{
type: Pipe,
args: [{
name: 'stringFilter',
standalone: false,
}]
}, {
type: Injectable
}] });
class StringFilterModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: StringFilterModule, declarations: [StringFilterPipe], exports: [StringFilterPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: StringFilterModule, decorators: [{
type: NgModule,
args: [{
exports: [StringFilterPipe],
declarations: [StringFilterPipe],
}]
}] });
const timelineDefaultOptions = {
timeline: {
backgroundColor: '#f1f2f3',
selectionColor: 'rgba(198, 23, 157, 0.15)',
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange: function onChange() { },
keyboard: {
step: 2_592_000_000, // 30 days
},
handles: {
backgroundColor: '#000',
foregroundColor: '#dcdedf',
focusIndicatorColor: 'rgba(0, 115, 231, 0.5)',
},
range: {
lower: null,
upper: null,
minimum: 0,
maximum: Infinity,
},
state: {
lowerHandleFocus: false,
upperHandleFocus: false,
rangeHandleFocus: false,
},
},
};
class TimelineChartPlugin {
constructor() {
this.id = 'timeline-chart-plugin';
}
/** We only want to register the plugin once per application */
static { this._isRegistered = false; }
/** Register this plugin */
static register() {
/**
* We have to register this plugin globally because
* ng2-charts doesn't support plugins on an invidual
* basis. We must check in all lifecycle hooks that
* it is an timeline chart before performing any actions.
*
* We also need to have it inside the class otherwise it
* will be included in every application by default.
* Having it here allows it to be tree-shaken.
*/
if (!this._isRegistered) {
// if pluginService exists then we are in v2
if (window.Chart?.pluginService) {
window.Chart.pluginService.register(new TimelineChartPlugin());
}
else {
import('chart.js').then(({ Chart }) => {
Chart.register(new TimelineChartPlugin());
});
}
this._isRegistered = true;
}
}
isVersion3() {
return window.Chart?.pluginService ? false : true;
}
/**
* When chart is initialised store the chart instance and context
* for use outside lifecycle hooks.
*
* We should also supply default options for any options that have
* not been specified by the consuming application.
*
* We also need to add some event listeners for events that Chart.js
* does not inform us of.
*/
beforeInit(chart) {
// provide the default options for any missing properties
if (this.getEnabled(chart)) {
// chart.config.options.timeline = { ...timelineDefaultOptions.timeline, ...this.getOptions(chart) };
chart.config.options.timeline = this.getOptionsWithDefaults(this.getOptions(chart));
// get the range
const { lower, upper } = this.getRange(chart);
// ensure we have an initial range set
if (lower === null || upper === null) {
throw new Error('Timeline Chart - Ensure that both an upper and lower range are initially provided.');
}
// setup the function
chart.config.options.timeline.state.onMouseDown = () => this.onMouseDown(chart);
chart.config.options.timeline.state.onMouseUp = () => this.onMouseUp(chart);
// add mouse down and mouseup event listeners
chart.canvas.addEventListener('mousedown', chart.config.options.timeline.state.onMouseDown);
document.addEventListener('mouseup', chart.config.options.timeline.state.onMouseUp);
}
}
/**
* We want to setup some additional functionality
* after the chart has initialized.
*/
afterInit(chart) {
if (this.getEnabled(chart)) {
// add accessibility attributes and elements to the chart
this.setupAccessibility(chart);
// intially call the onChange function
this.triggerOnChange(chart);
}
}
/**
* The timeline chart should have a subtle background
* color behind the main chart area (excluding the axis area).
* Suprisingly Chart.js does not support this out of the box
* so we need to add this functionality but it should be behind
* all chart elements.
*/
beforeDraw(chart) {
if (this.getEnabled(chart)) {
this.drawBackgroundColor(chart);
}
}
/**
* Once the Chart elements have been drawn we want to draw the drag
* handles and the overlay showing the selected region
*/
afterDraw(chart) {
if (this.getEnabled(chart)) {
this.drawSelection(chart);
this.drawHandles(chart);
}
}
/**
* We want to update the cursor whenever the mouse is over
* one of the drag handles. We have do calculate this manually
* as there are no DOM element to add CSS to.
*/
afterEvent(chart, parentEvent) {
const event = this.isVersion3() ? parentEvent.event : parentEvent;
if (parentEvent.replay === true) {
return;
}
// skip this if timeline is not enabled
if (!this.getEnabled(chart)) {
return;
}
switch (event.type) {
case 'mousemove':
this.setCursor(chart, event);
this.setRangeOnDrag(chart, event);
this.handleMouseMove(chart, event);
// store the latest mouse position
this.setState(chart, { mouseX: event.x });
break;
case 'mouseout':
this.resetCursor(chart);
this.hideTooltip(chart);
break;
}
}
/**
* Unbind from the event listeners we manually set up
*/
destroy(chart) {
if (this.getEnabled(chart)) {
document.removeEventListener('mouseup', chart.config.options.timeline.state.onMouseUp, true);
}
}
/** Get the timeline options from the chart instance */
getOptions(chart) {
return chart.config.options.timeline;
}
/** Determine if this chart is using the timeline */
getEnabled(chart) {
return !!this.getOptions(chart);
}
/** Get the timeline range from the chart instance */
getRange(chart) {
return this.getOptions(chart).range;
}
/** Get the chart area but include any padding */
getChartArea(chart) {
const { top, right, bottom, left } = chart.chartArea;
const padding = chart.config.options.layout && chart.config.options.layout.padding
? chart.config.options.layout.padding
: 0;
if (typeof padding === 'number') {
return {
top: top - padding,
right: right - padding,
left: left - padding,
bottom: bottom - padding,
};
}
else if (typeof padding === 'object') {
return {
top: top - padding.top,
right: right - padding.right,
left: left - padding.left,
bottom: bottom - padding.bottom,
};
}
return chart.chartArea;
}
/** Get stored state inside the chart options */
getState(chart) {
return this.getOptions(chart).state;
}
/** Store state inside the chart options */
setState(chart, state) {
// store the latest state
chart.config.options.timeline.state = { ...chart.config.options.timeline.state, ...state };
// trigger a chart re-render
chart.update();
}
/** Call the callback with the latest range */
triggerOnChange(chart) {
// get the current date range
const { lower, upper } = this.getRange(chart);
// get the callback function
const { onChange } = this.getOptions(chart);
// call the callback with the lower and upper values
requestAnimationFrame(() => onChange(lower, upper));
// get the handle elements
const { lowerHandleElement, upperHandleElement } = this.getState(chart);
// update the aria properties
lowerHandleElement.setAttribute('aria-valuemin', new Date(this.getHandleMinimum(chart, TimelineHandle.Lower)).toDateString());
lowerHandleElement.setAttribute('aria-valuenow', lower.toDateString());
lowerHandleElement.setAttribute('aria-valuemax', new Date(this.getHandleMaximum(chart, TimelineHandle.Lower)).toDateString());
upperHandleElement.setAttribute('aria-valuemin', new Date(this.getHandleMinimum(chart, TimelineHandle.Upper)).toDateString());
upperHandleElement.setAttribute('aria-valuenow', upper.toDateString());
upperHandleElement.setAttribute('aria-valuemax', new Date(this.getHandleMaximum(chart, TimelineHandle.Upper)).toDateString());
}
/** To make the chart accessible add some internal elements that can be focused */
setupAccessibility(chart) {
// create the invisible elements
const lowerHandle = document.createElement('div');
const upperHandle = document.createElement('div');
const rangeHandle = document.createElement('div');
// make the items focusable
lowerHandle.setAttribute('tabindex', '0');
upperHandle.setAttribute('tabindex', '0');
rangeHandle.setAttribute('tabindex', '0');
// insert the elements
chart.canvas.appendChild(lowerHandle);
chart.canvas.appendChild(upperHandle);
chart.canvas.appendChild(rangeHandle);
// add the event handlers
lowerHandle.addEventListener('focus', () => this.setState(chart, { lowerHandleFocus: true }));
lowerHandle.addEventListener('blur', () => this.setState(chart, { lowerHandleFocus: false }));
lowerHandle.addEventListener('keydown', (event) => this.onKeydown(chart, event, TimelineHandle.Lower));
upperHandle.addEventListener('focus', () => this.setState(chart, { upperHandleFocus: true }));
upperHandle.addEventListener('blur', () => this.setState(chart, { upperHandleFocus: false }));
upperHandle.addEventListener('keydown', (event) => this.onKeydown(chart, event, TimelineHandle.Upper));
rangeHandle.addEventListener('focus', () => this.setState(chart, { rangeHandleFocus: true }));
rangeHandle.addEventListener('blur', () => this.setState(chart, { rangeHandleFocus: false }));
rangeHandle.addEventListener('keydown', (event) => this.onRangeKeydown(chart, event));
// store the items in the state object
this.setState(chart, {
lowerHandleElement: lowerHandle,
upperHandleElement: upperHandle,
rangeHandleElement: rangeHandle,
});
}
/** Handle keyboard accessibility events */
onKeydown(chart, event, handle) {
// get the current value for the given handle
const value = this.getHandleValue(chart, handle).getTime();
const step = this.getOptions(chart).keyboard.step;
const [minimum, maximum] = this.getChartRange(chart);
switch (event.keyCode) {
case LEFT_ARROW:
this.setHandleValue(chart, handle, new Date(value - step));
event.preventDefault();
break;
case HOME:
this.setHandleValue(chart, handle, new Date(minimum));
event.preventDefault();
break;
case RIGHT_ARROW:
this.setHandleValue(chart, handle, new Date(value + step));
event.preventDefault();
break;
case END:
this.setHandleValue(chart, handle, new Date(maximum));
event.preventDefault();
break;
}
}
/**
* Handle range changes made with the keyboard as these are exempt from
* many of the validation checks that are required when dragging only one
* handle at a time.
*/
onRangeKeydown(chart, event) {
// get the current handle values
let lowerValue = this.getHandleValue(chart, TimelineHandle.Lower).getTime();
let upperValue = this.getHandleValue(chart, TimelineHandle.Upper).getTime();
const step = this.getOptions(chart).keyboard.step;
const difference = upperValue - lowerValue;
// get the chart boundaries
const [minimum, maximum] = this.getChartRange(chart);
switch (event.keyCode) {
case LEFT_ARROW:
lowerValue = Math.max(lowerValue - step, minimum);
upperValue = lowerValue + difference;
event.preventDefault();
break;
case RIGHT_ARROW:
upperValue = Math.min(upperValue + step, maximum);
lowerValue = upperValue - difference;
event.preventDefault();
break;
case HOME:
lowerValue = minimum;
upperValue = lowerValue + difference;
event.preventDefault();
break;
case END:
upperValue = maximum;
lowerValue = upperValue - difference;
event.preventDefault();
break;
}
// store the new values
chart.config.options.timeline.range[TimelineHandle.Lower] = new Date(lowerValue);
chart.config.options.timeline.range[TimelineHandle.Upper] = new Date(upperValue);
// update the chart
chart.update();
// emit the latest range
this.triggerOnChange(chart);
}
/**
* When the mouse is first pressed within a chart we should see if we are
* currently over a drag handle to start the dragging
*/
onMouseDown(chart) {
// ensure we only proceed when we have a chart context
if (!chart.ctx) {
return;
}
// get the position from the chart area
const { top } = this.getChartArea(chart);
// get the properties from the state
const { mouseX } = this.getState(chart);
// check if the event started within a drag handle
const handle = this.isWithinHandle(chart, { x: mouseX, y: top });
// if it did then we are now dragging the handle and should store it
this.setState(chart, { handle: handle !== null ? handle : null });
}
/** When the mouse is released we are no longer dragging */
onMouseUp(chart) {
if (chart.canvas) {
this.setState(chart, { handle: null });
}
}
handleMouseMove(chart, event) {
const mousePosition = this.isWithinHandle(chart, event);
const timelineOptions = (this.isVersion3() ? chart.config.options : chart.options);
// eslint-disable-next-line no-prototype-builtins
const hasTooltipOnRange = timelineOptions.timeline.range.hasOwnProperty('tooltip');
// eslint-disable-next-line no-prototype-builtins
const hasTooltipOnHandles = timelineOptions.timeline.handles.hasOwnProperty('tooltip');
let timelineTooltipText;
let handleTooltipText;
if (hasTooltipOnRange) {
timelineTooltipText = timelineOptions.timeline.range.tooltip.label();
}
if (hasTooltipOnHandles) {
handleTooltipText = timelineOptions.timeline.handles.tooltip.label();
}
if (mousePosition === TimelineHandle.Range && hasTooltipOnRange) {
this.externalTooltipHandler(chart, TimelineHandle.Range, timelineTooltipText);
}
else if (mousePosition === TimelineHandle.Lower && hasTooltipOnHandles) {
this.externalTooltipHandler(chart, TimelineHandle.Lower, handleTooltipText.rangeLower);
}
else if (mousePosition === TimelineHandle.Upper && hasTooltipOnHandles) {
this.externalTooltipHandler(chart, TimelineHandle.Upper, handleTooltipText.rangeUpper);
}
else {
this.hideTooltip(chart);
}
}
hideTooltip(chart) {
const tooltipEl = this.getOrCreateTooltip(chart);
tooltipEl.style.opacity = '0';
}
getOrCreateTooltip(chart) {
let tooltipEl = chart.canvas.parentNode.querySelector('.timeline-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.classList.add('timeline-tooltip');
tooltipEl.classList.add('tooltip');
const caret = document.createElement('div');
caret.classList.add('tooltip-caret');
const span = document.createElement('span');
tooltipEl.appendChild(span);
tooltipEl.appendChild(caret);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
}
externalTooltipHandler(chart, position, tooltipText) {
// Tooltip Element
const tooltipEl = this.getOrCreateTooltip(chart);
const span = tooltipEl.querySelector('span');
span.innerText = tooltipText;
const { x, y } = this.tooltipPositioner(chart, position);
tooltipEl.style.left = x + 'px';
tooltipEl.style.top = y + 'px';
tooltipEl.style.opacity = '1';
}
tooltipPositioner(chart, position) {
const lower = this.getHandleArea(chart, TimelineHandle.Lower).left;
const upper = this.getHandleArea(chart, TimelineHandle.Upper).left;
const tooltipEl = this.getOrCreateTooltip(chart);
const width = tooltipEl.getBoundingClientRect().width;
const caret = tooltipEl.querySelector('.tooltip-caret');
if (position === TimelineHandle.Range) {
caret.style.top = null;
caret.style.right = null;
caret.style.left = '50%';
caret.style.transform = 'rotate(0deg)';
const middle = (lower + upper) / 2;
return {
x: middle + 2,
y: -14,
};
}
else if (position === TimelineHandle.Lower) {
caret.style.top = '40%';
caret.style.right = 'auto';
caret.style.left = '-2px';
caret.style.transform = 'rotate(90deg)';
return {
x: lower + (width / 2 + 20),
y: 10,
};
}
else if (position === TimelineHandle.Upper) {
caret.style.top = '40%';
caret.style.right = '-7px';
caret.style.left = 'auto';
caret.style.transform = 'rotate(-90deg)';
return {
x: upper - (width / 2 + 20),
y: 10,
};
}
}
/** Update the range when dragged */
setRangeOnDrag(chart, event) {
const { handle, mouseX } = this.getState(chart);
// if we are not dragging then do nothing
if (!handle) {
return;
}
// get the chart area
const { left, right } = this.getChartArea(chart);
// get the current range
const { lower, upper } = this.getRange(chart);
// get the difference in x position since the last mouse position
const delta = event.x - mouseX;
// get the width of the chart area
const width = right - left;
// get the time range on the x-axis
const [minimum, maximum] = this.getChartRange(chart);
// determine how much of the time range was spanned in the move
const percentageDiff = (delta / width) * 100;
// calculate the time difference in the movement
const valueDiff = ((maximum - minimum) / 100) * percentageDiff;
if (handle === TimelineHandle.Lower) {
this.setHandleValue(chart, TimelineHandle.Lower, new Date(lower.getTime() + valueDiff));
}
if (handle === TimelineHandle.Upper) {
this.setHandleValue(chart, TimelineHandle.Upper, new Date(upper.getTime() + valueDiff));
}
if (handle === TimelineHandle.Range) {
// get the current range
const range = upper.getTime() - lower.getTime();
// update the values
if (valueDiff < 0) {
this.setHandleValue(chart, TimelineHandle.Upper, new Date(upper.getTime() + valueDiff));
this.setHandleValue(chart, TimelineHandle.Lower, new Date(lower.getTime() + valueDiff));
}
else {
this.setHandleValue(chart, TimelineHandle.Lower, new Date(lower.getTime() + valueDiff));
this.setHandleValue(chart, TimelineHandle.Upper, new Date(upper.getTime() + valueDiff));
}
// calculate the new range
const currentRange = chart.config.options.timeline.range.upper.getTime() -
chart.config.options.timeline.range.lower.getTime();
// ensure the range is still the same
if (currentRange !== range) {
if (valueDiff < 0) {
this.setHandleValue(chart, TimelineHandle.Upper, new Date(chart.config.options.timeline.range.upper.getTime() + (range - currentRange)));
}
else {
this.setHandleValue(chart, TimelineHandle.Lower, new Date(chart.config.options.timeline.range.lower.getTime() + (currentRange - range)));
}
}
}
}
/**
* Draw the background color in the region that sits behind all the chart content
*/
drawBackgroundColor(chart) {
// get the region that the chart is drawn on (excluding axis)
const { top, right, bottom, left } = this.getChartArea(chart);
// fill the background color
chart.ctx.save();
chart.ctx.fillStyle = this.getOptions(chart).backgroundColor;
chart.ctx.fillRect(left, top, right - left, bottom - top);
chart.ctx.restore();
}
/** Draw the overlay that indicates the selected region */
drawSelection(chart) {
// get the region that the chart is drawn on (excluding axis)
const { top, bottom } = this.getChartArea(chart);
// get the fill color
const selectionColor = this.getOptions(chart).selectionColor;
// get the focus indicator color
const { focusIndicatorColor } = this.getOptions(chart).handles;
// get the lower and upper handle render regions
const lower = this.getHandleArea(chart, TimelineHandle.Lower);
const upper = this.getHandleArea(chart, TimelineHandle.Upper);
// draw selection region
chart.ctx.save();
chart.ctx.fillStyle = selectionColor;
chart.ctx.fillRect(lower.left, 0, upper.right - lower.left, bottom - top);
// check if we are focused on the range handle
if (this.isHandleFocused(chart, TimelineHandle.Range)) {
chart.ctx.strokeStyle = focusIndicatorColor;
const handleWidth = 4;
const lineWidth = 2;
chart.ctx.lineWidth = lineWidth;
chart.ctx.strokeRect(lower.left + handleWidth + lineWidth, lineWidth / 2, upper.right - lower.left - (handleWidth + lineWidth) * 2, bottom - top - lineWidth);
}
chart.ctx.restore();
}
/** Darw the drag handles */
drawHandles(chart) {
// get the region that the chart is drawn on (excluding axis)
const { top, bottom } = this.getChartArea(chart);
// get the handle colors
const { backgroundColor, foregroundColor, focusIndicatorColor } = this.getOptions(chart).handles;
// draw each handle
[TimelineHandle.Lower, TimelineHandle.Upper].forEach(handle => {
// get the area of the handle
const area = this.getHandleArea(chart, handle);
const handleWidth = 5;
const chartHeight = bottom - top;
chart.ctx.save();
// if the handle is focused draw an outline
if (this.isHandleFocused(chart, handle)) {
chart.ctx.fillStyle = focusIndicatorColor;
chart.ctx.fillRect(area.left - 2, 0, handleWidth + 4, chartHeight);
}
// draw the handle
chart.ctx.fillStyle = backgroundColor;
chart.ctx.fillRect(area.left, 0, handleWidth, chartHeight);
// draw the 3 drag handles within the drag handle
chart.ctx.fillStyle = foregroundColor;
// calculate size and position
const width = 3;
const height = 3;
const x = area.left + (handleWidth - width) / 2;
const midpoint = area.top + chartHeight / 2;
const topY = midpoint - height * 2.5;
const middleY = midpoint - height / 2;
const bottomY = midpoint + height * 1.5;
chart.ctx.fillRect(x, topY, width, height);
chart.ctx.fillRect(x, middleY, width, height);
chart.ctx.fillRect(x, bottomY, width, height);
chart.ctx.restore();
});
}
/**
* Update the CSS cursor on the canvas element if we are hovering over a drag handle
*/
setCursor(chart, event) {
// get the handle if we are hovering over one
const handle = this.getState(chart).handle || this.isWithinHandle(chart, event);
if (handle === TimelineHandle.Lower || handle === TimelineHandle.Upper) {
chart.canvas.style.cursor = 'ew-resize';
}
else if (handle === TimelineHandle.Range) {
chart.canvas.style.cursor = 'move';
}
else {
this.resetCursor(chart);
}
}
// restore the cursor to the default
resetCursor(chart) {
if (chart.canvas.style.cursor !== '') {
chart.canvas.style.cursor = '';
}
}
isHandleFocused(chart, handle) {
if (handle === TimelineHandle.Lower) {
return this.getState(chart).lowerHandleFocus;
}
if (handle === TimelineHandle.Upper) {
return this.getState(chart).upperHandleFocus;
}
if (handle === TimelineHandle.Range) {
return this.getState(chart).rangeHandleFocus;
}
return false;
}
/** Determine if a position is within one of the drag handles */
isWithinHandle(chart, event) {
// get the lower and upper handle render regions
const lower = this.getHandleArea(chart, TimelineHandle.Lower);
const upper = this.getHandleArea(chart, TimelineHandle.Upper);
// get the position co-ordinates
const { x, y } = event;
if (x >= lower.left && x <= lower.right && y >= lower.top && y <= lower.bottom) {
return TimelineHandle.Lower;
}
if (x >= upper.left && x <= upper.right && y >= upper.top && y <= upper.bottom) {
return TimelineHandle.Upper;
}
if (x > lower.right && x < upper.left && y >= lower.top && y <= lower.bottom) {
return TimelineHandle.Range;
}
return null;
}
/** Get the area a specific handle covers within the chart */
getHandleArea(chart, handle) {
// get the region that the chart is drawn on (excluding axis)
const { left, top, right, bottom } = this.getChartArea(chart);
// perform some calculations on the chart area
const width = right - left;
// get the minimum and maximum ticks on the chart
const [minimum, maximum] = this.getChartRange(chart);
// get the lower and upper range values
const { lower, upper } = this.getOptions(chart).range;
if (handle === TimelineHandle.Lower) {
const percentage = ((lower.getTime() - minimum) / (maximum - minimum)) * 100;
const position = left + (width / 100) * percentage;
return { top, left: position, right: position + 5, bottom };
}
if (handle === TimelineHandle.Upper) {
const percentage = ((upper.getTime() - minimum) / (maximum - minimum)) * 100;
const position = left + (width / 100) * percentage;
return { top, left: position - 5, right: position, bottom };
}
}
/**
* Get the minimum and maximum values on the x-axis
*/
getChartRange(chart) {
let minimum;
let maximum;
if (this.isVersion3()) {
// get the current data
const data = chart.scales;
// get the range on the x-axis
minimum = data.x.min;
maximum = data.x.max;
}
else {
// get the current data
const { data } = chart.getDatasetMeta(0);
// get the range on the x-axis
minimum = data[0]._xScale.min;
maximum = data[0]._xScale.max;
}
return [minimum, maximum];
}
/** Get the value for a given handle */
getHandleValue(chart, handle) {
const { lower, upper } = this.getOptions(chart).range;
return handle === TimelineHandle.Lower ? lower : upper;
}
setHandleValue(chart, handle, value) {
// perform lower handle validation
if (handle === TimelineHandle.Lower) {
value = new Date(Math.min(Math.max(this.getHandleMinimum(chart, handle), value.getTime()), this.getHandleMaximum(chart, handle)));
}
// perform upper handle validation
if (handle === TimelineHandle.Upper) {
value = new Date(Math.max(Math.min(this.getHandleMaximum(chart, handle), value.getTime()), this.getHandleMinimum(chart, handle)));
}
// store the new value
chart.config.options.timeline.range[handle] = value;
// update the chart
chart.update();
// emit the latest range
this.triggerOnChange(chart);
}
getHandleMinimum(chart, handle) {
// get the minimum distance
const minDistance = this.getOptions(chart).range.minimum || 0;
const maxDistance = this.getOptions(chart).range.maximum || Infinity;
// get the chart boundaries
const [minimum] = this.getChartRange(chart);
// get the current date range
const { lower, upper } = this.getRange(chart);
if (handle === TimelineHandle.Lower) {
return Math.max(upper.getTime() - maxDistance, minimum);
}
if (handle === TimelineHandle.Upper) {
return lower.getTime() + minDistance;
}
}
getHandleMaximum(chart, handle) {
// get the minimum distance
const minDistance = this.getOptions(chart).range.minimum || 0;
const maxDistance = this.getOptions(chart).range.maximum || Infinity;
// get the chart boundaries
const [, maximum] = this.getChartRange(chart);
// get the current date range
const { lower, upper } = this.getRange(chart);
if (handle === TimelineHandle.Lower) {
return upper.getTime() - minDistance;
}
if (handle === TimelineHandle.Upper) {
return Math.min(lower.getTime() + maxDistance, maximum);
}
}
getOptionsWithDefaults(options) {
const merge = (target, source) => {
for (const key of Object.keys(source)) {
if (source[key] instanceof Object &&
!(source[key] instanceof Date) &&
typeof source[key] !== 'function') {
Object.assign(source[key], merge(target[key], source[key]));
}
}
return Object.assign(target || {}, source);
};
return merge({ ...timelineDefaultOptions.timeline }, options);
}
}
/**
* Directly exporting a file that is not an Angular component, module, etc..
* can cause build issues. We can use a module that instantiates the plugin
* instead of directly exporting the Chart.js plugin.
*/
class TimelineChartModule {
constructor() {
TimelineChartPlugin.register();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineChartModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: TimelineChartModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineChartModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TimelineChartModule, decorators: [{
type: NgModule,
args: [{}]
}], ctorParameters: () => [] });
var TimelineHandle;
(function (TimelineHandle) {
TimelineHandle["Lower"] = "lower";
TimelineHandle["Upper"] = "upper";
TimelineHandle["Range"] = "range";
})(TimelineHandle || (TimelineHandle = {}));
class CookieAdapter {
getItem(key) {
if (document.cookie) {
// get all the cookies for this site
const cookies = document.cookie.split(';');
// process the cookies into a from we can easily manage
const match = cookies
.map(cookie => ({ key: cookie.split('=')[0].trim(), value: cookie.split('=')[1].trim() }))
.find(cookie => cookie.key === key);
return match ? match.value : null;
}
return null;
}
setItem(key, value) {
document.cookie = `${key}=${value}; path=/`;
}
removeItem(key) {
document.cookie.split(';').forEach(cookie => {
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie;
if (name === key) {
document.cookie = cookie
.trim()
.replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`);
}
});
}
clear() {
// call remove item on each cookie
document.cookie
.split(';')
.map(cookie => cookie.split('=')[0].trim())
.forEach(cookie => this.removeItem(cookie));
}
getSupported() {
// cookies are supported in all browsers
return this;
}
}
class LocalStorageAdapter {
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
localStorage.setItem(key, value);
}
removeItem(key) {
localStorage.removeItem(key);
}
clear() {
localStorage.clear();
}
getSupported() {
// if local storage variable does not exist fall back to cookies
if (!localStorage) {
return new CookieAdapter();
}
// try to make a test save to local storage to see if there are any exceptions
try {
localStorage.setItem('ux-persistent-data-service', 'ux-persistent-data-service');
localStorage.removeItem('ux-persistent-data-service');
return this;
}
catch (err) {
return new CookieAdapter();
}
}
}
class SessionStorageAdapter {
getItem(key) {
return sessionStorage.getItem(key);
}
setItem(key, value) {
sessionStorage.setItem(key, value);
}
removeItem(key) {
sessionStorage.removeItem(key);
}
clear() {
sessionStorage.clear();
}
getSupported() {
// if local storage variable does not exist fall back to cookies
if (!sessionStorage) {
return new CookieAdapter();
}
// try to make a test save to local storage to see if there are any exceptions
try {
sessionStorage.setItem('ux-persistent-data-service', 'ux-persistent-data-service');
sessionStorage.removeItem('ux-persistent-data-service');
return this;
}
catch (err) {
return new CookieAdapter();
}
}
}
class PersistentDataService {
/**
* Save the item in some form of persistent storage
*/
setItem(key, value, type = PersistentDataStorageType.LocalStorage) {
this.getAdapter(type).setItem(key, value);
}
/**
* Get a stored value from persistent storage
*/
getItem(key, type = PersistentDataStorageType.LocalStorage) {
return this.getAdapter(type).getItem(key);
}
/**
* Remove a stored value from persistent storage
*/
removeItem(key, type = PersistentDataStorageType.LocalStorage) {
this.getAdapter(type).removeItem(key);
}
/**
* Remove a stored value from persistent storage
*/
clear(type = PersistentDataStorageType.LocalStorage) {
this.getAdapter(type).clear();
}
/**
* Return the appropriate adapter based on the type requested
*/
getAdapter(type) {
switch (type) {
case PersistentDataStorageType.Cookie:
return new CookieAdapter();
case PersistentDataStorageType.LocalStorage:
// eslint-disable-next-line no-case-declarations
const localStorageAdapter = new LocalStorageAdapter();
return localStorageAdapter.getSupported();
case PersistentDataStorageType.SessionStorage:
// eslint-disable-next-line no-case-declarations
const sessionStorageAdapter = new SessionStorageAdapter();
return sessionStorageAdapter.getSupported();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataService, decorators: [{
type: Injectable
}] });
var PersistentDataStorageType;
(function (PersistentDataStorageType) {
PersistentDataStorageType[PersistentDataStorageType["LocalStorage"] = 0] = "LocalStorage";
PersistentDataStorageType[PersistentDataStorageType["Cookie"] = 1] = "Cookie";
PersistentDataStorageType[PersistentDataStorageType["SessionStorage"] = 2] = "SessionStorage";
})(PersistentDataStorageType || (PersistentDataStorageType = {}));
class PersistentDataModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataModule, providers: [PersistentDataService] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: PersistentDataModule, decorators: [{
type: NgModule,
args: [{
providers: [PersistentDataService],
}]
}] });
class StorageAdapter {
}
/**
* Export Common Functionality
*/
/**
* Generated bundle index. Do not edit.
*/
export { ACCESSIBILITY_OPTIONS_TOKEN, AccessibilityModule, AccessibilityOptionsService, AccordionComponent, AccordionModule, AccordionPanelComponent, AccordionPanelHeadingDirective, AccordionService, ActionDirection, AlertComponent, AlertIconDirective, AlertModule, AudioService, AudioServiceModule, AutoGrowDirective, AutoGrowModule, BadgeDirective, BadgeModule, BaseSearchComponent, BreadcrumbsComponent, BreadcrumbsModule, CHECKBOX_VALUE_ACCESSOR, COLOR_SET_TOKEN, CONDUITS, CardTabComponent, CardTabContentDirective, CardTabsModule, CardTabsService, CardTabsetComponent, CheckboxComponent, CheckboxModule, ClickOutsideDirective, ClickOutsideModule, Color, ColorContrastDirective, ColorPickerColor, ColorPickerComponent, ColorPickerModule, ColorService, ColorServiceModule, ColumnPickerComponent, ColumnSortingComponent, ColumnSortingDirective, ColumnSortingModule, ColumnSortingState, Conduit, ConduitComponent, ConduitModule, ConduitSubject, ConduitZone, ConduitZoneComponent, ContrastService, CookieAdapter, DashboardComponent, DashboardDragHandleDirective, DashboardGrabHandleDirective, DashboardModule, DashboardService, DashboardWidgetComponent, DateFormatterPipe, DateFormatterPipeModule, DatePickerHeaderEvent, DatePickerMode, DateRangeOptions, DateRangePicker, DateRangePickerComponent, DateRangePickerDirective, DateRangePickerModule, DateRangeService, DateTimePickerComponent, DateTimePickerConfig, DateTimePickerModule, DateTimePickerService, DefaultFocusIndicatorDirective, DragDirective, DragModule, DragService, DropDirective, DurationPipe, DurationPipeModule, EboxComponent, EboxContentDirective, EboxHeaderDirective, EboxModule, Facet, FacetCheckListComponent, FacetCheckListItemComponent, FacetClearButtonDirective, FacetContainerComponent, FacetDeselect, FacetDeselectAll, FacetHeaderComponent, FacetSelect, FacetService, FacetTypeaheadHighlight, FacetTypeaheadListComponent, FacetTypeaheadListItemComponent, FacetsModule, FileSizePipe, FileSizePipeModule, FilterAddEvent, FilterContainerComponent, FilterDropdownComponent, FilterDynamicComponent, FilterModule, FilterRemoveAllEvent, FilterRemoveEvent, FilterService, FilterTypeaheadHighlight, FixedHeaderTableDirective, FixedHeaderTableModule, FlippableCardBackDirective, FlippableCardComponent, FlippableCardFrontDirective, FlippableCardModule, FloatLabelDirective, FloatLabelModule, FloatingActionButtonComponent, FloatingActionButtonsComponent, FloatingActionButtonsModule, FocusIfDirective, FocusIfModule, FocusIndicator, FocusIndicatorDirective, FocusIndicatorOptionsDirective, FocusIndicatorOrigin, FocusIndicatorOriginDirective, FocusIndicatorOriginService, FocusIndicatorService, FocusWithinDirective, FocusableItemToken, FrameExtractionService, HelpCenterItemDirective, HelpCenterModule, HelpCenterService, HierarchyBarCollapsedComponent, HierarchyBarComponent, HierarchyBarModule, HierarchyBarNodeIconDirective, HierarchyBarStandardComponent, HoverActionContainerDirective, HoverActionDirective, HoverActionModule, ICON_OPTIONS_TOKEN, IconComponent, IconModule, IconService, IconType, InfiniteScrollDirective, InfiniteScrollLoadButtonDirective, InfiniteScrollLoadErrorEvent, InfiniteScrollLoadedEvent, InfiniteScrollLoadingDirective, InfiniteScrollLoadingEvent, InfiniteScrollModule, InputDropdownComponent, InputDropdownModule, ItemDisplayPanelComponent, ItemDisplayPanelContentDirective, ItemDisplayPanelFooterDirective, ItemDisplayPanelModule, LayoutSwitcherDirective, LayoutSwitcherItemDirective, LayoutSwitcherModule, LocalFocusIndicatorOptions, LocalStorageAdapter, ManagedFocusContainerDirective, ManagedFocusContainerService, MarqueeWizardComponent, MarqueeWizardModule, MarqueeWizardStepComponent, MarqueeWizardStepIconDirective, MediaPlayerBaseExtensionDirective, MediaPlayerComponent, MediaPlayerControlsExtensionComponent, MediaPlayerCustomControlDirective, MediaPlayerModule, MediaPlayerTimelineExtensionComponent, MenuComponent, MenuDividerComponent, MenuInitialFocusDirective, MenuItemComponent, MenuItemCustomControlDirective, MenuItemType, MenuModule, MenuNavigationDirective, MenuNavigationItemDirective, MenuNavigationModule, MenuNavigationToggleDirective, MenuTabbableItemDirective, MenuTriggerDirective, ModeDirection, NAVIGATION_MODULE_OPTIONS, NUMBER_PICKER_VALUE_ACCESSOR, NavigationComponent, NavigationItemComponent, NavigationLinkDirective, NavigationModule, NavigationService, NestedDonutChartComponent, NestedDonutChartModule, NotificationListComponent, NotificationModule, NotificationService, NumberPickerComponent, NumberPickerModule, ObserversModule, OrganizationChartAxis, OrganizationChartComponent, OrganizationChartModule, OverflowDirective, OverlayPlacementService, PAGINATION_CONTROL_VALUE_ACCESSOR, PageHeaderComponent, PageHeaderCustomMenuDirective, PageHeaderIconMenuComponent, PageHeaderModule, PageHeaderNavigationComponent, PaginationComponent, PaginationModule, PartitionMapComponent, PartitionMapModule, PartitionMapSegmentEventsDirective, PersistentDataModule, PersistentDataService, PersistentDataStorageType, PopoverComponent, PopoverDirective, PopoverModule, ProgressBarComponent, ProgressBarModule, RADIOBUTTON_VALUE_ACCESSOR, RADIO_GROUP_CONTROL_VALUE_ACCESSOR, RadioButtonComponent, RadioButtonGroupDirective, RadioButtonModule, ReorderableDirective, ReorderableHandleDirective, ReorderableModelDirective, ReorderableModule, ResizableExpandingTableDirective, ResizableTableCellComponent, ResizableTableColumnComponent, ResizableTableDirective, ResizeDirective, ResizeModule, ResizeService, Rounding, SELECT_VALUE_ACCESSOR, SPIN_BUTTON_VALUE_ACCESSOR, SankeyChart, SankeyChartComponent, SankeyChartModule, SankeyNodeDirective, ScrollIntoViewDirective, ScrollIntoViewIfDirective, ScrollIntoViewService, ScrollModule, SearchBuilderComponent, SearchBuilderFocusService, SearchBuilderGroupComponent, SearchBuilderGroupService, SearchBuilderModule, SearchBuilderOutletDirective, SearchBuilderService, SearchDateComponent, SearchDateRangeComponent, SearchSelectComponent, SearchTextComponent, SelectComponent, SelectListComponent, SelectListItemComponent, SelectListModule, SelectModule, SelectionDirective, SelectionItemDirective, SelectionModule, SelectionService, SelectionStrategy, SessionStorageAdapter, SidePanelCloseDirective, SidePanelComponent, SidePanelModule, SliderCalloutTrigger, SliderComponent, SliderModule, SliderSize, SliderSnap, SliderStyle, SliderThumb, SliderThumbEvent, SliderTickType, SliderType, SparkComponent, SparkModule, SpinButtonComponent, SpinButtonModule, SplitterAccessibilityDirective, StepChangingEvent, StepDirection, StorageAdapter, StringFilterModule, StringFilterPipe, TIME_PICKER_VALUE_ACCESSOR, TOOLBAR_SEARCH_VALUE_ACCESSOR, TabComponent, TabHeadingDirective, TabbableListDirective, TabbableListItemDirective, TabbableListService, TableModule, TabsetComponent, TabsetModule, TabsetService, TagInputComponent, TagInputEvent, TagInputModule, ThemeColor, TimePickerComponent, TimePickerModule, TimelineChartModule, TimelineChartPlugin, TimelineComponent, TimelineEventComponent, TimelineHandle, TimelineModule, ToggleSwitchComponent, ToggleSwitchModule, ToolbarSearchButtonDirective, ToolbarSearchComponent, ToolbarSearchFieldDirective, ToolbarSearchModule, TooltipComponent, TooltipDirective, TooltipModule, TooltipService, TreeGridDirective, TreeGridIndentDirective, TreeGridModule, TreeGridRowDirective, TreeGridState, TypeaheadComponent, TypeaheadKeyService, TypeaheadModule, TypeaheadOptionEvent, VirtualForContainerComponent, VirtualForDirective, VirtualForService, VirtualScrollCellDirective, VirtualScrollComponent, VirtualScrollLoadButtonDirective, VirtualScrollLoadingDirective, VirtualScrollModule, WizardComponent, WizardModule, WizardService, WizardStepComponent, colorSets, compareDays, dateComparator, dateRange, defaultConduitProps, defaultOptions, differenceBetweenDates, getIconType, getStartOfDay, gridify, isColumnPickerGroupItem, isDateAfter, isDateBefore, isKeyboardTrigger, isMouseTrigger, meridians, months, monthsShort, range, tick, timezoneComparator, timezones, uxIconset, weekdays, weekdaysShort };
//# sourceMappingURL=ux-aspects-ux-aspects.mjs.map