UNPKG

@ipi-soft/ng-components

Version:

Custom Angular Components

338 lines (334 loc) 20.3 kB
import * as i0 from '@angular/core'; import { EventEmitter, Component, ViewChild, Input, Output, HostListener } from '@angular/core'; import { NgClass } from '@angular/common'; import { fromEvent, tap, debounceTime } from 'rxjs'; import { TooltipPosition, IpiTooltipDirective } from '@ipi-soft/ng-components/tooltip'; var MouseDirection; (function (MouseDirection) { MouseDirection[MouseDirection["Up"] = 0] = "Up"; MouseDirection[MouseDirection["Down"] = 1] = "Down"; })(MouseDirection || (MouseDirection = {})); class IpiListboxComponent { constructor(elementRef, changeDetectorRef) { this.elementRef = elementRef; this.changeDetectorRef = changeDetectorRef; this.listbox = null; this.selectChange = new EventEmitter(); this.tooltipPosition = TooltipPosition; this.controlInvalid = false; this.control = null; this.controlSubscription = null; this.documentKeyupValue = ''; this.documentKeyupValueResetTime = 1000; this.isMetaHold = false; this.isShiftHold = false; this.lastSelectedItemIndex = 0; this.lastSelectedItemIndexOnShiftHold = 0; this.changeDetectorRef.detach(); } ngOnInit() { this.control = this.getControl(); this.selectedItems = new Array(this.options.data.length).fill(null); this.changeDetectorRef.detectChanges(); } ngAfterViewInit() { this.documentKeyDownSubscription = fromEvent(this.elementRef.nativeElement, 'keydown') .pipe(tap(event => { this.keydown(event); }), debounceTime(this.documentKeyupValueResetTime)) .subscribe(() => { this.documentKeyupValue = ''; }); } ngOnDestroy() { this.documentKeyDownSubscription.unsubscribe(); if (this.controlSubscription) { this.controlSubscription?.unsubscribe(); } } ngOnChanges(changes) { if (changes['options'] && changes['options'].currentValue['data'] && this.selectedItems) { this.selectedItems.length = 0; this.changeDetectorRef.detectChanges(); } } mousedown(targetItem) { const items = this.listbox.nativeElement.children[0].children; if (this.isShiftHold) { const itemValue = parseInt(targetItem.getAttribute('value')); const max = Math.max(itemValue, this.lastSelectedItemIndexOnShiftHold); const min = Math.min(itemValue, this.lastSelectedItemIndexOnShiftHold); const itemsToAdd = []; const itemsToRemove = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (i >= min && i <= max) { item.classList.add('selected'); itemsToAdd.push(parseInt(item.getAttribute('value'))); } else { item.classList.remove('selected'); itemsToRemove.push(parseInt(item.getAttribute('value'))); } } this.removeSelectedItems(itemsToRemove); this.addSelectedItems(itemsToAdd); return; } if (this.isMetaHold) { const isSelected = targetItem.classList.contains('selected'); if (isSelected) { targetItem.classList.remove('selected'); this.removeSelectedItems(parseInt(targetItem.getAttribute('value'))); } else { targetItem.classList.add('selected'); this.addSelectedItems(parseInt(targetItem.getAttribute('value'))); } return; } const mouseCustomData = { hoverItemIndex: 0, targetItemIndex: 0, mouseDirection: MouseDirection.Up }; const itemsToRemove = []; for (let i = 0; i < items.length; i++) { const item = items[i]; item.classList.remove('selected'); itemsToRemove.push(parseInt(item.getAttribute('value'))); if (targetItem === item) { mouseCustomData.hoverItemIndex = i; mouseCustomData.targetItemIndex = i; } item.onmouseover = () => { this.mouseover(i, items, mouseCustomData); }; } targetItem.classList.add('selected'); this.removeSelectedItems(itemsToRemove); this.addSelectedItems(parseInt(targetItem.getAttribute('value'))); } mouseup() { const items = this.listbox.nativeElement.children[0].children; for (let i = 0; i < items.length; i++) { items[i].onmouseover = null; } } keyup(event) { if (event.code.toLowerCase().includes('shift')) { this.isShiftHold = false; return; } if (event.code.toLowerCase().includes('meta') || event.code.toLowerCase().includes('control')) { this.isMetaHold = false; } } mouseover(i, items, mouseCustomData) { if (i > mouseCustomData.targetItemIndex) { mouseCustomData.mouseDirection = MouseDirection.Down; } if (i < mouseCustomData.targetItemIndex) { mouseCustomData.mouseDirection = MouseDirection.Up; } if (mouseCustomData.mouseDirection === MouseDirection.Down) { if (i >= mouseCustomData.hoverItemIndex) { this.lastSelectedItemIndex = parseInt(items[i].getAttribute('value')); items[i].classList.add('selected'); this.addSelectedItems(parseInt(items[i].getAttribute('value'))); } else { items[i + 1].classList.remove('selected'); this.removeSelectedItems(parseInt(items[i + 1].getAttribute('value'))); } } else { if (i <= mouseCustomData.hoverItemIndex) { this.lastSelectedItemIndex = parseInt(items[i].getAttribute('value')); items[i].classList.add('selected'); this.addSelectedItems(parseInt(items[i].getAttribute('value'))); } else { items[i - 1].classList.remove('selected'); this.removeSelectedItems(parseInt(items[i - 1].getAttribute('value'))); } } mouseCustomData.hoverItemIndex = i; } keydown(event) { if ((document.activeElement !== this.listbox?.nativeElement) && (document.activeElement !== this.listbox?.nativeElement.childNodes[0])) { return; } if (event.code.toLowerCase().includes('shift')) { this.isShiftHold = true; this.lastSelectedItemIndexOnShiftHold = this.lastSelectedItemIndex; } if (event.code.toLowerCase().includes('meta') || event.code.toLowerCase().includes('control')) { this.isMetaHold = true; } if (event.code.toLowerCase().includes('arrowdown')) { event.preventDefault(); this.documentArrowsKeyup(true); return; } if (event.code.toLowerCase().includes('arrowup')) { event.preventDefault(); this.documentArrowsKeyup(false); return; } this.documentKeyup(event); } documentArrowsKeyup(isDownArrow) { const dataLength = this.options.data.length; let newIndex = isDownArrow ? this.lastSelectedItemIndex + 1 : this.lastSelectedItemIndex - 1; const items = this.listbox.nativeElement.children[0].children; if (this.isShiftHold) { if (newIndex < 0 || newIndex === dataLength) { return; } const isSelected = items[newIndex].classList.contains('selected'); if (isSelected) { const indexForDeselect = isDownArrow ? newIndex - 1 : newIndex + 1; items[indexForDeselect].classList.remove('selected'); this.removeSelectedItems(parseInt(items[indexForDeselect].getAttribute('value'))); } else { items[newIndex].classList.add('selected'); this.addSelectedItems(parseInt(items[newIndex].getAttribute('value'))); } this.lastSelectedItemIndex = newIndex; items[newIndex].scrollIntoView({ block: 'nearest' }); return; } const itemsToRemove = []; for (let i = 0; i < items.length; i++) { items[i].classList.remove('selected'); itemsToRemove.push(parseInt(items[i].getAttribute('value'))); } this.removeSelectedItems(itemsToRemove); newIndex = newIndex < 0 ? 0 : newIndex; newIndex = newIndex >= dataLength ? dataLength - 1 : newIndex; items[newIndex].classList.add('selected'); items[newIndex].scrollIntoView({ block: 'nearest' }); this.addSelectedItems(parseInt(items[newIndex].getAttribute('value'))); } documentKeyup(event) { if (this.isMetaHold && event.key.toLowerCase() === 'a') { event.preventDefault(); const items = this.listbox.nativeElement.children[0].children; const itemsToAdd = []; for (let i = 0; i < items.length; i++) { 2; items[i].classList.add('selected'); itemsToAdd.push(parseInt(items[i].getAttribute('value'))); } this.addSelectedItems(itemsToAdd); return; } this.documentKeyupValue = this.documentKeyupValue + event.key.toLowerCase(); let newIndex = null; for (let i = 0; i < this.options.data.length; i++) { const subLabel = this.options.data[i].label.substring(0, this.documentKeyupValue.length).toLowerCase(); if (subLabel === this.documentKeyupValue) { newIndex = i; break; } } if (newIndex !== null) { const items = this.listbox.nativeElement.children[0].children; const itemsToRemove = []; for (let i = 0; i < items.length; i++) { items[i].classList.remove('selected'); itemsToRemove.push(parseInt(items[i].getAttribute('value'))); } this.removeSelectedItems(itemsToRemove); items[newIndex].classList.add('selected'); items[newIndex].scrollIntoView({ block: 'center' }); this.addSelectedItems(parseInt(items[newIndex].getAttribute('value'))); } } addSelectedItems(index) { const isIndexNumber = typeof index === 'number'; this.lastSelectedItemIndex = isIndexNumber ? index : index.slice(-1)[0]; if (isIndexNumber) { this.selectedItems[index] = this.options.data[index]; } else { for (let i of index) { this.selectedItems[i] = this.options.data[i]; } } if (this.control) { this.control.setValue(this.selectedItems.filter(value => value !== null)); } this.selectChange.emit(this.selectedItems.filter(Boolean)); this.changeDetectorRef.detectChanges(); } removeSelectedItems(index) { const isIndexNumber = typeof index === 'number'; if (isIndexNumber) { this.selectedItems[index] = null; } else { for (let i of index) { this.selectedItems[i] = null; } } if (this.control) { this.control?.setValue(this.selectedItems.filter(value => value !== null)); } this.selectChange.emit(this.selectedItems.filter(Boolean)); this.changeDetectorRef.detectChanges(); } getControl() { if (this.options.formGroup && this.options.formControlName) { this.controlSubscription = this.options.formGroup.controls[this.options.formControlName].valueChanges.subscribe(() => { if (!this.control) { return; } this.checkIfControlInvalid(this.control); this.getControlError(this.control); }); return this.options.formGroup.get(this.options.formControlName); } return null; } checkIfControlInvalid(control) { this.controlInvalid = control.invalid; } getControlError(control) { this.controlError = ''; const options = this.options; if (options.errors && this.controlInvalid) { for (const error in options.errors) { if (control.hasError(error)) { this.controlError = options.errors[error]; return; } } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: IpiListboxComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.4", type: IpiListboxComponent, isStandalone: true, selector: "ipi-listbox", inputs: { options: "options" }, outputs: { selectChange: "selectChange" }, host: { listeners: { "document:mouseup": "mouseup($event)", "keyup": "keyup($event)" } }, viewQueries: [{ propertyName: "listbox", first: true, predicate: ["listbox"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (options) {\n <div class=\"listbox-header\">\n @if (options.label) {\n <label>{{ options.label }}</label>\n }\n\n @if (options.tooltip) {\n <svg class=\"tooltip-icon\" [ipiTooltip]=\"options.tooltip\" [tooltipPosition]=\"tooltipPosition.Above\" width=\"16\" height=\"16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <path d=\"M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.5 7.5H8V11h.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M8.25 5.25a.25.25 0 1 1-.5 0 .25.25 0 0 1 .5 0z\" fill=\"#fff\"/>\n </g>\n </svg>\n }\n </div>\n\n <div #listbox tabindex=\"0\" class=\"container\" [ngClass]=\"{ 'error': controlInvalid }\">\n <div class=\"container-inner\">\n @for (data of options.data; track data.value) {\n <div #item [attr.value]=\"$index\" class=\"item\" (mousedown)=\"mousedown(item)\">\n {{ data.label }}\n </div>\n }\n </div>\n </div>\n\n <div class=\"footer\" [ngClass]=\"{ 'error': controlInvalid }\">\n {{ controlError }}\n </div>\n}\n", styles: [":host{width:fit-content}.container-inner:focus{outline:none}:host(.no-margin) .container{margin-top:0}.listbox-header{display:flex;align-items:center;justify-content:flex-start}label{font-size:14px;font-weight:600;color:var(--ipi-listbox-label-color, default);padding:2px}.tooltip-icon path{fill:var(--ipi-listbox-tooltip-icon-fill, transparent);stroke:var(--ipi-listbox-tooltip-icon-stroke, #C6C6C6)}.container{min-width:264px;outline:none;box-sizing:border-box;color:var(--ipi-listbox-item-color, #00000099);background-color:var(--ipi-listbox-background-color, #FFFFFF);border:1px solid var(--ipi-listbox-border-color, #F2F2F2);border-radius:8px;padding:2px}.container:hover,.container:focus{border-color:var(--ipi-listbox-border-hover-color, #4B5368)}.container.error{border-color:var(--ipi-listbox-border-invalid-color, #F96138)}.container-inner{height:224px;overflow-y:scroll;padding:12px}.item{-webkit-user-select:none;user-select:none;font-size:14px;padding:1px 4px;position:relative;overflow:hidden;cursor:pointer;z-index:1}.item.selected{color:var(--ipi-listbox-item-selected-color, #F96138)}.item:after{width:5px;height:5px;position:absolute;top:50%;left:50%;content:\"\";background:#80808031;border-radius:50%;transform:translate(-50%,-50%) scale(0);transition:transform .2s ease-out}.item.selected:after{transform:translate(-50%,-50%) scale(50)}.footer{height:14px;font-size:12px;margin-top:2px}.footer.error{color:var(--ipi-input-error-color, #F96138)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IpiTooltipDirective, selector: "[ipiTooltip]", inputs: ["ipiTooltip", "tooltipPosition"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: IpiListboxComponent, decorators: [{ type: Component, args: [{ selector: 'ipi-listbox', imports: [ NgClass, IpiTooltipDirective, ], template: "@if (options) {\n <div class=\"listbox-header\">\n @if (options.label) {\n <label>{{ options.label }}</label>\n }\n\n @if (options.tooltip) {\n <svg class=\"tooltip-icon\" [ipiTooltip]=\"options.tooltip\" [tooltipPosition]=\"tooltipPosition.Above\" width=\"16\" height=\"16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <path d=\"M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.5 7.5H8V11h.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M8.25 5.25a.25.25 0 1 1-.5 0 .25.25 0 0 1 .5 0z\" fill=\"#fff\"/>\n </g>\n </svg>\n }\n </div>\n\n <div #listbox tabindex=\"0\" class=\"container\" [ngClass]=\"{ 'error': controlInvalid }\">\n <div class=\"container-inner\">\n @for (data of options.data; track data.value) {\n <div #item [attr.value]=\"$index\" class=\"item\" (mousedown)=\"mousedown(item)\">\n {{ data.label }}\n </div>\n }\n </div>\n </div>\n\n <div class=\"footer\" [ngClass]=\"{ 'error': controlInvalid }\">\n {{ controlError }}\n </div>\n}\n", styles: [":host{width:fit-content}.container-inner:focus{outline:none}:host(.no-margin) .container{margin-top:0}.listbox-header{display:flex;align-items:center;justify-content:flex-start}label{font-size:14px;font-weight:600;color:var(--ipi-listbox-label-color, default);padding:2px}.tooltip-icon path{fill:var(--ipi-listbox-tooltip-icon-fill, transparent);stroke:var(--ipi-listbox-tooltip-icon-stroke, #C6C6C6)}.container{min-width:264px;outline:none;box-sizing:border-box;color:var(--ipi-listbox-item-color, #00000099);background-color:var(--ipi-listbox-background-color, #FFFFFF);border:1px solid var(--ipi-listbox-border-color, #F2F2F2);border-radius:8px;padding:2px}.container:hover,.container:focus{border-color:var(--ipi-listbox-border-hover-color, #4B5368)}.container.error{border-color:var(--ipi-listbox-border-invalid-color, #F96138)}.container-inner{height:224px;overflow-y:scroll;padding:12px}.item{-webkit-user-select:none;user-select:none;font-size:14px;padding:1px 4px;position:relative;overflow:hidden;cursor:pointer;z-index:1}.item.selected{color:var(--ipi-listbox-item-selected-color, #F96138)}.item:after{width:5px;height:5px;position:absolute;top:50%;left:50%;content:\"\";background:#80808031;border-radius:50%;transform:translate(-50%,-50%) scale(0);transition:transform .2s ease-out}.item.selected:after{transform:translate(-50%,-50%) scale(50)}.footer{height:14px;font-size:12px;margin-top:2px}.footer.error{color:var(--ipi-input-error-color, #F96138)}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { listbox: [{ type: ViewChild, args: ['listbox'] }], options: [{ type: Input }], selectChange: [{ type: Output }], mouseup: [{ type: HostListener, args: ['document:mouseup', ['$event']] }], keyup: [{ type: HostListener, args: ['keyup', ['$event']] }] } }); /** * Generated bundle index. Do not edit. */ export { IpiListboxComponent }; //# sourceMappingURL=ipi-soft-ng-components-listbox.mjs.map