UNPKG

novo-elements

Version:

427 lines (422 loc) 25.9 kB
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { hasModifierKey } from '@angular/cdk/keycodes'; import * as i0 from '@angular/core'; import { Directive, EventEmitter, HostListener, Input, ViewChild, ContentChildren, ContentChild, Output, Component, NgModule } from '@angular/core'; import { Subscription, Subject, merge, of } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { NovoButtonElement } from 'novo-elements/elements/button'; import * as i1 from 'novo-elements/elements/common'; import { mixinOverlay, mixinTabIndex, mixinDisabled, _countGroupLabelsBeforeOption, _getOptionScrollPosition, NovoOverlayTemplateComponent, NovoOptgroup, NovoOption, NovoOverlayModule, NovoOptionModule } from 'novo-elements/elements/common'; import { notify, BooleanInput } from 'novo-elements/utils'; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; class NovoDropDownTrigger { constructor(element) { this.element = element; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropDownTrigger, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NovoDropDownTrigger, isStandalone: false, selector: "[dropdownTrigger]", host: { classAttribute: "novo-dropdown-trigger" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropDownTrigger, decorators: [{ type: Directive, args: [{ selector: '[dropdownTrigger]', host: { class: 'novo-dropdown-trigger', }, standalone: false, }] }], ctorParameters: () => [{ type: i0.ElementRef }] }); // Create Base Class from Mixins // Boilerplate for applying mixins class NovoDropdownBase { constructor() { } } const NovoDropdownMixins = mixinOverlay(mixinTabIndex(mixinDisabled(NovoDropdownBase), 1)); class NovoDropdownElement extends NovoDropdownMixins { /** Whether the user should be allowed to select multiple options. */ get multiple() { return this._multiple; } set multiple(value) { this._multiple = coerceBooleanProperty(value); } /** Whether the dropdown should scroll to the active item whenever it is opened. */ get scrollToActiveItemOnOpen() { return this._scrollToActiveItemOnOpen; } set scrollToActiveItemOnOpen(value) { this._scrollToActiveItemOnOpen = coerceBooleanProperty(value); } get button() { return this._trigger || this._button; } constructor(element, ref) { super(); this.element = element; this.ref = ref; this.parentScrollAction = 'close'; this.side = 'default'; this.scrollStrategy = 'reposition'; /** * Keep dropdown open after an item is selected */ this.keepOpen = false; this.width = -1; // Defaults to dynamic width (no hardcoded width value and no host width lookup) this.appendToBody = false; // Deprecated this.toggled = new EventEmitter(); this._selectedOptionChanges = Subscription.EMPTY; /** The Subject to complete all subscriptions when destroyed. */ this._onDestroy = new Subject(); this._multiple = false; this._scrollToActiveItemOnOpen = false; this.clickHandler = this.togglePanel.bind(this); this.closeHandler = this.closePanel.bind(this); } ngOnInit() { if (this.appendToBody) { notify('\'appendToBody\' has been deprecated. Please remove this attribute.'); } } ngAfterContentInit() { // Add a click handler to the button to toggle the menu this.button.element.nativeElement.addEventListener('click', this.clickHandler); this.button.element.nativeElement.tabIndex = -1; this.options.changes.pipe(takeUntil(this._onDestroy)).subscribe(() => { this._initKeyManager(); this._watchSelectionEvents(); }); this._initKeyManager(); this._watchSelectionEvents(); this.focus(); } ngAfterViewInit() { this._watchPanelEvents(); } ngOnDestroy() { this._onDestroy.next(); this._onDestroy.complete(); // Remove listener if (this.button) { this.button.element.nativeElement.removeEventListener('click', this.clickHandler); } } focus(options) { if (!this.disabled) { this.element.nativeElement.focus(options); } } openPanel() { super.openPanel(); if (this.scrollToActiveItemOnOpen) { this._scrollOptionIntoView(this.findFirstSelectedOptionIndex(this.options) || 0); } } findFirstSelectedOptionIndex(options) { return options.toArray().findIndex((option) => { return option.selected === true; }); } set items(items) { } /** Handles all keydown events on the dropdown. */ _handleKeydown(event) { if (!this.disabled) { this.panelOpen ? this._handleOpenKeydown(event) : this._handleClosedKeydown(event); } } /** Handles keyboard events while the dropdown is closed. */ _handleClosedKeydown(event) { const key = event.key; const isArrowKey = key === "ArrowDown" /* Key.ArrowDown */ || key === "ArrowUp" /* Key.ArrowUp */ || key === "ArrowLeft" /* Key.ArrowLeft */ || key === "ArrowRight" /* Key.ArrowRight */; const isOpenKey = key === "Enter" /* Key.Enter */ || key === " " /* Key.Space */; const manager = this._keyManager; // Open the select on ALT + arrow key to match the native <select> if ((!manager.isTyping() && isOpenKey && !hasModifierKey(event)) || ((this.multiple || event.altKey) && isArrowKey)) { event.preventDefault(); // prevents the page from scrolling down when pressing space this.openPanel(); } } /** Handles keyboard events when the dropdown is open. */ _handleOpenKeydown(event) { const manager = this._keyManager; const key = event.key; const isArrowKey = key === "ArrowDown" /* Key.ArrowDown */ || key === "ArrowUp" /* Key.ArrowUp */; const isTyping = manager.isTyping(); const isInputField = event.target; if (isArrowKey && event.altKey) { // Close the dropdown on ALT + arrow key to match the native <select> event.preventDefault(); this.closePanel(); // Don't do anything in this case if the user is typing, // because the typing sequence can include the space key. } else if (!isTyping && (key === "Enter" /* Key.Enter */ || key === " " /* Key.Space */) && manager.activeItem && !hasModifierKey(event)) { event.preventDefault(); this._multiple ? manager.activeItem._selectViaInteraction() : manager.activeItem._clickViaInteraction(); } else if (!isTyping && this._multiple && ['a', 'A'].includes(key) && event.ctrlKey) { event.preventDefault(); const hasDeselectedOptions = this.options.some((opt) => !opt.disabled && !opt.selected); this.options.forEach((option) => { if (!option.disabled) { hasDeselectedOptions ? option.select() : option.deselect(); } }); } else if ("Escape" /* Key.Escape */ === key) { this.closePanel(); } else { const previouslyFocusedIndex = manager.activeItemIndex; manager.onKeydown(event); if (this._multiple && isArrowKey && event.shiftKey && manager.activeItem && manager.activeItemIndex !== previouslyFocusedIndex) { manager.activeItem._selectViaInteraction(); } } } _watchPanelEvents() { const panelStateChanges = merge(this.overlay.opening, this.overlay.closing); panelStateChanges.pipe(takeUntil(this._onDestroy)).subscribe((event) => this.toggled.emit(event)); } _watchSelectionEvents() { const selectionEvents = this.options ? merge(...this.options.map((option) => option.onSelectionChange)) : of(); this._selectedOptionChanges.unsubscribe(); this._selectedOptionChanges = selectionEvents.pipe(takeUntil(this._onDestroy)).subscribe((event) => { if (event.isUserInput && !this.multiple) { this._clearPreviousSelectedOption(this._keyManager.activeItem); event.source.select(); if (!this.keepOpen && this.panelOpen) { this.closePanel(); this.focus(); } } else { event.source.select(); } }); } /** * Clear any previous selected option and emit a selection change event for this option */ _clearPreviousSelectedOption(skip) { this.options.forEach((option) => { if (option !== skip && option.selected) { option.deselect(); } }); } /** Sets up a key manager to listen to keyboard events on the overlay panel. */ _initKeyManager() { this._keyManager = new ActiveDescendantKeyManager(this.options).withTypeAhead(250).withHomeAndEnd(); this._keyManager.tabOut.pipe(takeUntil(this._onDestroy)).subscribe(() => { if (this.panelOpen) { // Restore focus to the trigger before closing. Ensures that the focus // position won't be lost if the user got focus into the overlay. this.focus(); this.closePanel(); } }); this._keyManager.change.pipe(takeUntil(this._onDestroy)).subscribe(() => { if (this.panelOpen && this.overlay) { this._scrollOptionIntoView(this._keyManager.activeItemIndex || 0); } }); } /** Scrolls the active option into view. */ _scrollOptionIntoView(index) { const labelCount = _countGroupLabelsBeforeOption(index, this.options, this.optionGroups); const itemHeight = this._getItemHeight(); this.panel.nativeElement.scrollTop = _getOptionScrollPosition((index + labelCount) * itemHeight, itemHeight, this.panel.nativeElement.scrollTop, this.panel.nativeElement.offsetHeight); } /** Calculates the height of the select's options. */ _getItemHeight() { const [first] = this.options; if (first) { return first._getHostElement().offsetHeight; } return 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownElement, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NovoDropdownElement, isStandalone: false, selector: "novo-dropdown", inputs: { parentScrollSelector: "parentScrollSelector", parentScrollAction: "parentScrollAction", containerClass: "containerClass", side: "side", scrollStrategy: "scrollStrategy", keepOpen: "keepOpen", height: "height", width: "width", appendToBody: "appendToBody", multiple: "multiple", scrollToActiveItemOnOpen: "scrollToActiveItemOnOpen" }, outputs: { toggled: "toggled" }, host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "attr.tabIndex": "disabled ? -1 : 0" } }, queries: [{ propertyName: "_button", first: true, predicate: NovoButtonElement, descendants: true }, { propertyName: "_trigger", first: true, predicate: NovoDropDownTrigger, descendants: true }, { propertyName: "optionGroups", predicate: NovoOptgroup, descendants: true }, { propertyName: "options", predicate: NovoOption, descendants: true }], viewQueries: [{ propertyName: "overlay", first: true, predicate: NovoOverlayTemplateComponent, descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }], usesInheritance: true, ngImport: i0, template: ` <ng-content select="button,novo-button,[dropdownTrigger]" #trigger></ng-content> <novo-overlay-template [parent]="element" [width]="width" [position]="side" [scrollStrategy]="scrollStrategy"> <div #panel class="dropdown-container {{ containerClass }}" [style.max-height.px]="height" [class.has-height]="!!height"> <ng-content></ng-content> </div> </novo-overlay-template> `, isInline: true, styles: [":host{display:inline-block;position:relative;outline:none}:host ::ng-deep .novo-dropdown-trigger{cursor:pointer;-webkit-appearance:none}:host ::ng-deep button,:host ::ng-deep novo-button{position:relative;z-index:0}:host ::ng-deep button .novo-button-icon,:host ::ng-deep novo-button .novo-button-icon{font-size:.8em!important;width:1em!important;height:1em!important;margin:0 .5em}.dropdown-container{background-color:var(--background-bright, #f7f7f7);list-style:none;margin:0;padding:0;min-width:180px;margin-top:.5rem;margin-bottom:.5rem;box-shadow:0 4px 10px #00000040}.dropdown-container.has-height{overflow:auto}.dropdown-container ::ng-deep list dropdown-item-header{color:#9e9e9e;font-size:.8em;flex:1;font-weight:500;text-transform:uppercase;padding:.5rem 1rem;display:block}.dropdown-container ::ng-deep list hr{border:none;height:1px;background:#dbdbdb}.dropdown-container.novo-table-dropdown-cell ::ng-deep list{max-height:400px;display:block;overflow:auto;padding:5px 0}.dropdown-container.novo-table-dropdown-cell ::ng-deep item{height:30px!important;padding:0 16px!important}.dropdown-container.novo-table-dropdown-cell ::ng-deep item span{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;display:inline-block;max-width:80%}.dropdown-container.novo-table-dropdown-cell ::ng-deep item.active{font-weight:500}.dropdown-container.novo-table-dropdown-cell ::ng-deep dropdown-item-header{padding:0 10px!important}\n"], dependencies: [{ kind: "component", type: i1.NovoOverlayTemplateComponent, selector: "novo-overlay-template", inputs: ["position", "scrollStrategy", "width", "minWidth", "height", "closeOnSelect", "hasBackdrop", "parent"], outputs: ["select", "opening", "closing", "backDropClicked"] }] }); } } __decorate([ BooleanInput(), __metadata("design:type", Boolean) ], NovoDropdownElement.prototype, "keepOpen", void 0); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownElement, decorators: [{ type: Component, args: [{ selector: 'novo-dropdown', template: ` <ng-content select="button,novo-button,[dropdownTrigger]" #trigger></ng-content> <novo-overlay-template [parent]="element" [width]="width" [position]="side" [scrollStrategy]="scrollStrategy"> <div #panel class="dropdown-container {{ containerClass }}" [style.max-height.px]="height" [class.has-height]="!!height"> <ng-content></ng-content> </div> </novo-overlay-template> `, host: { '[attr.tabIndex]': 'disabled ? -1 : 0', }, standalone: false, styles: [":host{display:inline-block;position:relative;outline:none}:host ::ng-deep .novo-dropdown-trigger{cursor:pointer;-webkit-appearance:none}:host ::ng-deep button,:host ::ng-deep novo-button{position:relative;z-index:0}:host ::ng-deep button .novo-button-icon,:host ::ng-deep novo-button .novo-button-icon{font-size:.8em!important;width:1em!important;height:1em!important;margin:0 .5em}.dropdown-container{background-color:var(--background-bright, #f7f7f7);list-style:none;margin:0;padding:0;min-width:180px;margin-top:.5rem;margin-bottom:.5rem;box-shadow:0 4px 10px #00000040}.dropdown-container.has-height{overflow:auto}.dropdown-container ::ng-deep list dropdown-item-header{color:#9e9e9e;font-size:.8em;flex:1;font-weight:500;text-transform:uppercase;padding:.5rem 1rem;display:block}.dropdown-container ::ng-deep list hr{border:none;height:1px;background:#dbdbdb}.dropdown-container.novo-table-dropdown-cell ::ng-deep list{max-height:400px;display:block;overflow:auto;padding:5px 0}.dropdown-container.novo-table-dropdown-cell ::ng-deep item{height:30px!important;padding:0 16px!important}.dropdown-container.novo-table-dropdown-cell ::ng-deep item span{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;display:inline-block;max-width:80%}.dropdown-container.novo-table-dropdown-cell ::ng-deep item.active{font-weight:500}.dropdown-container.novo-table-dropdown-cell ::ng-deep dropdown-item-header{padding:0 10px!important}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { parentScrollSelector: [{ type: Input }], parentScrollAction: [{ type: Input }], containerClass: [{ type: Input }], side: [{ type: Input }], scrollStrategy: [{ type: Input }], keepOpen: [{ type: Input }], height: [{ type: Input }], width: [{ type: Input }], appendToBody: [{ type: Input }], toggled: [{ type: Output }], overlay: [{ type: ViewChild, args: [NovoOverlayTemplateComponent] }], _button: [{ type: ContentChild, args: [NovoButtonElement] }], _trigger: [{ type: ContentChild, args: [NovoDropDownTrigger] }], optionGroups: [{ type: ContentChildren, args: [NovoOptgroup, { descendants: true }] }], options: [{ type: ContentChildren, args: [NovoOption, { descendants: true }] }], panel: [{ type: ViewChild, args: ['panel'] }], multiple: [{ type: Input }], scrollToActiveItemOnOpen: [{ type: Input }], _handleKeydown: [{ type: HostListener, args: ['keydown', ['$event']] }] } }); // Deprecated below here --------------------------- class NovoItemElement { constructor(dropdown, element) { this.dropdown = dropdown; this.element = element; this.keepOpen = false; this.action = new EventEmitter(); this.active = false; notify('\'item\' element has been deprecated. Please use \'novo-option\' and \'novo-optgroup\'.'); } onClick(event) { // Poor man's disable if (!this.disabled) { // Close if keepOpen is false if (!this.keepOpen) { this.dropdown.closePanel(); } // Emit the action this.action.emit({ originalEvent: event }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoItemElement, deps: [{ token: NovoDropdownElement }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NovoItemElement, isStandalone: false, selector: "item", inputs: { disabled: "disabled", keepOpen: "keepOpen" }, outputs: { action: "action" }, host: { listeners: { "click": "onClick($event)" }, properties: { "class.disabled": "disabled", "class.active": "active" } }, ngImport: i0, template: '<novo-option><ng-content></ng-content></novo-option>', isInline: true, dependencies: [{ kind: "component", type: i1.NovoOption, selector: "novo-option", inputs: ["selected", "keepOpen", "novoInert", "value", "disabled"], exportAs: ["novoOption"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoItemElement, decorators: [{ type: Component, args: [{ selector: 'item', template: '<novo-option><ng-content></ng-content></novo-option>', host: { '[class.disabled]': 'disabled', '[class.active]': 'active', }, standalone: false, }] }], ctorParameters: () => [{ type: NovoDropdownElement }, { type: i0.ElementRef }], propDecorators: { disabled: [{ type: Input }], keepOpen: [{ type: Input }], action: [{ type: Output }], onClick: [{ type: HostListener, args: ['click', ['$event']] }] } }); class NovoDropdownListElement { constructor(dropdown) { this.dropdown = dropdown; notify('\'list\' element has been deprecated. Please use novo-option and novo-optgroup.'); } ngAfterContentInit() { this.dropdown.items = this.items; this.items.changes.subscribe(() => { this.dropdown.items = this.items; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownListElement, deps: [{ token: NovoDropdownElement }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NovoDropdownListElement, isStandalone: false, selector: "list", queries: [{ propertyName: "items", predicate: NovoItemElement }], ngImport: i0, template: '<ng-content></ng-content>', isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownListElement, decorators: [{ type: Component, args: [{ selector: 'list', template: '<ng-content></ng-content>', standalone: false, }] }], ctorParameters: () => [{ type: NovoDropdownElement }], propDecorators: { items: [{ type: ContentChildren, args: [NovoItemElement] }] } }); class NovoDropDownItemHeaderElement { constructor() { notify('\'dropdown-item-header\' element has been deprecated. Please use novo-option and novo-optgroup.'); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropDownItemHeaderElement, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NovoDropDownItemHeaderElement, isStandalone: false, selector: "dropdown-item-header", ngImport: i0, template: '<ng-content></ng-content>', isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropDownItemHeaderElement, decorators: [{ type: Component, args: [{ selector: 'dropdown-item-header', template: '<ng-content></ng-content>', standalone: false, }] }], ctorParameters: () => [] }); // NG2 class NovoDropdownModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownModule, declarations: [NovoDropdownElement, NovoItemElement, NovoDropdownListElement, NovoDropDownItemHeaderElement, NovoDropDownTrigger], imports: [NovoOverlayModule, NovoOptionModule], exports: [NovoDropdownElement, NovoItemElement, NovoDropdownListElement, NovoDropDownItemHeaderElement, NovoDropDownTrigger] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownModule, imports: [NovoOverlayModule, NovoOptionModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoDropdownModule, decorators: [{ type: NgModule, args: [{ imports: [NovoOverlayModule, NovoOptionModule], declarations: [NovoDropdownElement, NovoItemElement, NovoDropdownListElement, NovoDropDownItemHeaderElement, NovoDropDownTrigger], exports: [NovoDropdownElement, NovoItemElement, NovoDropdownListElement, NovoDropDownItemHeaderElement, NovoDropDownTrigger], }] }] }); /** * Generated bundle index. Do not edit. */ export { NovoDropDownItemHeaderElement, NovoDropDownTrigger, NovoDropdownElement, NovoDropdownListElement, NovoDropdownModule, NovoItemElement }; //# sourceMappingURL=novo-elements-elements-dropdown.mjs.map