@ipi-soft/ng-components
Version:
Custom Angular Components
338 lines (334 loc) • 20.3 kB
JavaScript
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