mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'develop' into feature/responsive-customer-orders
This commit is contained in:
6
apps/shared/components/filter/ng-package.json
Normal file
6
apps/shared/components/filter/ng-package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div>
|
||||
<div class="inputs overflow-y-auto">
|
||||
<button
|
||||
class="ui-input"
|
||||
type="button"
|
||||
*ngFor="let input of uiInputGroup?.input"
|
||||
[class.active]="activeInput === input"
|
||||
[class.has-options]="input?.hasSelectedOptions() || input?.hasUnselectedOptions() || input?.selected"
|
||||
(click)="setActiveInput(input)"
|
||||
>
|
||||
<div class="grow">
|
||||
{{ input?.label }}
|
||||
</div>
|
||||
<ui-icon icon="arrow_head" size="1rem"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<shared-filter-input-options
|
||||
*ngIf="activeInput?.options?.values?.length"
|
||||
[class.remove-rounded-top-left]="isFirstInputSelected"
|
||||
class="options"
|
||||
[inputOptions]="activeInput?.options"
|
||||
>
|
||||
</shared-filter-input-options>
|
||||
|
||||
<div *ngIf="!activeInput?.options?.values?.length" class="bg-white p-4 rounded" [ngSwitch]="activeInput?.type">
|
||||
<p *ngIf="activeInput?.description" class="font-bold">{{ activeInput?.description }}</p>
|
||||
|
||||
<ng-container *ngIf="inputTemplate">
|
||||
<ng-container *ngTemplateOutlet="inputTemplate; context: { $implicit: activeInput }"> </ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!inputTemplate">
|
||||
<shared-filter-input-text *ngSwitchCase="1" [input]="activeInput"></shared-filter-input-text>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
:host {
|
||||
@apply grid grid-flow-col gap-2;
|
||||
grid-template-columns: 240px 1fr;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply transition transform text-cool-grey;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
@apply grid grid-flow-row gap-2;
|
||||
max-height: calc(100vh - 377px);
|
||||
|
||||
.ui-input {
|
||||
@apply flex flex-row items-center border-none outline-none p-4 font-bold text-base bg-white text-black text-left rounded-card transition transform;
|
||||
}
|
||||
|
||||
.has-options {
|
||||
@apply text-white bg-active-customer;
|
||||
|
||||
ui-icon {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-input.active {
|
||||
@apply text-black bg-white pr-6 -mr-2 rounded-r-none;
|
||||
|
||||
ui-icon {
|
||||
@apply rotate-180 text-cool-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.space-right {
|
||||
@apply mr-4;
|
||||
}
|
||||
|
||||
.grow {
|
||||
@apply flex-grow;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-filter-filter-group-filter .input-options-wrapper {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
::ng-deep ui-filter-filter-group-filter .remove-rounded-top-left .input-options-wrapper {
|
||||
@apply rounded-tl-none;
|
||||
}
|
||||
|
||||
::ng-deep .branch ui-filter-filter-group-filter {
|
||||
.inputs {
|
||||
.ui-input.has-options {
|
||||
@apply text-white bg-active-branch;
|
||||
|
||||
ui-icon {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-input.active {
|
||||
@apply text-black bg-white pr-6 -mr-2 rounded-r-none;
|
||||
|
||||
ui-icon {
|
||||
@apply rotate-180 text-active-branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .branch ui-filter-input-options {
|
||||
.has-options {
|
||||
@apply bg-cool-grey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, TemplateRef } from '@angular/core';
|
||||
import { IInputGroup, FilterInput, InputGroup } from '../../tree';
|
||||
import { FilterComponent } from '../../filter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-filter-group-filter',
|
||||
templateUrl: 'filter-filter-group-filter.component.html',
|
||||
styleUrls: ['filter-filter-group-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterFilterGroupFilterComponent {
|
||||
private _inputGroup: InputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IInputGroup) {
|
||||
if (value instanceof InputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = InputGroup.create(value);
|
||||
}
|
||||
}
|
||||
|
||||
get uiInputGroup() {
|
||||
return this._inputGroup;
|
||||
}
|
||||
|
||||
private _activeInput: FilterInput;
|
||||
|
||||
get activeInput() {
|
||||
return this.uiInputGroup?.input?.find((f) => f?.key === this._activeInput?.key) || this.uiInputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
get isFirstInputSelected() {
|
||||
return this.activeInput === this.uiInputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
get inputTemplate(): TemplateRef<any> {
|
||||
return this._hostComponent.customInputs?.find((f) => f.key === this.activeInput?.key)?.templateRef;
|
||||
}
|
||||
|
||||
constructor(private _hostComponent: FilterComponent) {}
|
||||
|
||||
setActiveInput(input: FilterInput) {
|
||||
this._activeInput = input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterFilterGroupFilterComponent } from './filter-filter-group-filter.component';
|
||||
import { FilterInputOptionsModule } from '../../shared/filter-input-options';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { FilterInputModule } from '../../shared/filter-input';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule, FilterInputOptionsModule, FilterInputModule],
|
||||
exports: [FilterFilterGroupFilterComponent],
|
||||
declarations: [FilterFilterGroupFilterComponent],
|
||||
})
|
||||
export class FilterFilterGroupFilterModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-filter-group-filter.component';
|
||||
export * from './filter-filter-group-filter.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1 @@
|
||||
<shared-filter-input-chip *ngFor="let input of uiInputGroup?.input" [input]="input"></shared-filter-input-chip>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-flow-col items-center justify-center gap-4;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { FilterInputChipModule } from '../../shared/filter-input-chip';
|
||||
import { InputGroup } from '../../tree';
|
||||
import { FilterFilterGroupMainComponent } from './filter-filter-group-main.component';
|
||||
|
||||
describe('UiFilterFilterGroupMainComponent', () => {
|
||||
let spectator: Spectator<FilterFilterGroupMainComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: FilterFilterGroupMainComponent,
|
||||
imports: [FilterInputChipModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('inputGroup', () => {
|
||||
it('should create an instance of UiInputGroup when value is a plain object', () => {
|
||||
spectator.setInput({ inputGroup: {} });
|
||||
expect(spectator.component.uiInputGroup instanceof InputGroup).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the UiInputGroup if it is already an instance of UiInputGroup', () => {
|
||||
const instance = InputGroup.create({});
|
||||
spectator.setInput({ inputGroup: instance });
|
||||
expect(spectator.component.uiInputGroup).toBe(instance);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui-filter-input-chip Element', () => {
|
||||
it('should render an Element for each uiInputGroup.input', () => {
|
||||
const instance = InputGroup.create({
|
||||
input: [
|
||||
{ label: 'Label 1', type: 0 },
|
||||
{ label: 'label 2', type: 0 },
|
||||
{ label: 'label 3', type: 0 },
|
||||
],
|
||||
});
|
||||
spectator.setInput({ inputGroup: instance });
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
expect(spectator.queryAll('shared-filter-input-chip').length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { IInputGroup, InputGroup } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-filter-group-main',
|
||||
templateUrl: 'filter-filter-group-main.component.html',
|
||||
styleUrls: ['filter-filter-group-main.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterFilterGroupMainComponent {
|
||||
private _inputGroup: InputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IInputGroup) {
|
||||
if (value instanceof InputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = InputGroup.create(value);
|
||||
}
|
||||
}
|
||||
|
||||
get uiInputGroup() {
|
||||
return this._inputGroup;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterFilterGroupMainComponent } from './filter-filter-group-main.component';
|
||||
import { FilterInputChipModule } from '../../shared/filter-input-chip';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FilterInputChipModule],
|
||||
exports: [FilterFilterGroupMainComponent],
|
||||
declarations: [FilterFilterGroupMainComponent],
|
||||
})
|
||||
export class FilterFilterGroupMainModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-filter-group-main.component';
|
||||
export * from './filter-filter-group-main.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,25 @@
|
||||
<ui-searchbox
|
||||
[placeholder]="uiInput?.placeholder"
|
||||
[query]="uiInput?.value"
|
||||
(queryChange)="onQueryChange($event)"
|
||||
(complete)="complete.next($event)"
|
||||
(search)="emitSearch($event)"
|
||||
(scan)="emitSearch($event)"
|
||||
[loading]="loading"
|
||||
[hint]="hint"
|
||||
[scanner]="scanner"
|
||||
>
|
||||
<ui-autocomplete *ngIf="autocompleteProvider">
|
||||
<button *ngFor="let item of autocompleteResults$ | async" [uiAutocompleteItem]="item.query">
|
||||
{{ item.display }}
|
||||
</button>
|
||||
</ui-autocomplete>
|
||||
</ui-searchbox>
|
||||
<ng-container *ngIf="showDescription && uiInput?.description">
|
||||
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
|
||||
i
|
||||
</button>
|
||||
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
|
||||
{{ uiInput.description }}
|
||||
</ui-tooltip>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.info-tooltip-button {
|
||||
@apply border-font-customer border-2 border-solid bg-white rounded-md text-base font-bold absolute;
|
||||
border-style: outset;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
top: 14px;
|
||||
right: -45px;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Optional,
|
||||
ViewChild,
|
||||
AfterViewInit,
|
||||
OnInit,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { Observable, Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, filter, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { FilterAutocomplete, FilterAutocompleteProvider } from '../../providers';
|
||||
import { IInputGroup, FilterInput, InputGroup } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-input-group-main',
|
||||
templateUrl: 'filter-input-group-main.component.html',
|
||||
styleUrls: ['filter-input-group-main.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'sharedFilterInputGroupMain',
|
||||
})
|
||||
export class FilterInputGroupMainComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChild(UiAutocompleteComponent, { read: UiAutocompleteComponent, static: false })
|
||||
autocompleteComponent: UiAutocompleteComponent;
|
||||
|
||||
@Output()
|
||||
queryChange = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
search = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
loading: boolean;
|
||||
|
||||
@Input()
|
||||
hint: string;
|
||||
|
||||
@Input()
|
||||
showDescription: boolean = true;
|
||||
|
||||
@Input()
|
||||
scanner = false;
|
||||
|
||||
private _inputGroup: InputGroup;
|
||||
|
||||
@Input()
|
||||
set inputGroup(value: IInputGroup) {
|
||||
if (value instanceof InputGroup) {
|
||||
this._inputGroup = value;
|
||||
} else {
|
||||
this._inputGroup = InputGroup.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
this.initAutocomplete();
|
||||
}
|
||||
|
||||
get uiInput(): FilterInput {
|
||||
return this._inputGroup?.input?.find((f) => f);
|
||||
}
|
||||
|
||||
private _autocompleteProvider: FilterAutocompleteProvider;
|
||||
get autocompleteProvider() {
|
||||
return this._autocompleteProvider;
|
||||
}
|
||||
|
||||
autocompleteResults$: Observable<FilterAutocomplete[]>;
|
||||
|
||||
complete = new Subject<string>();
|
||||
|
||||
private _cancelComplete = new Subject<void>();
|
||||
|
||||
private changeSubscriptions: Subscription;
|
||||
|
||||
constructor(
|
||||
@Inject(FilterAutocompleteProvider) @Optional() private autocompleteProviders: FilterAutocompleteProvider[],
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
// cancle autocomplete
|
||||
cancelAutocomplete() {
|
||||
this._cancelComplete.next();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._autocompleteProvider = this.autocompleteProviders?.find((provider) => !!provider);
|
||||
}
|
||||
|
||||
onQueryChange(query: string) {
|
||||
this.uiInput?.setValue(query);
|
||||
this.queryChange.emit(query);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.initAutocomplete();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
const sub = this.uiInput?.changes?.pipe(filter((changes) => changes?.keys.includes('value'))).subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
if (sub) {
|
||||
this.changeSubscriptions.add(sub);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.changeSubscriptions?.unsubscribe();
|
||||
this.changeSubscriptions = new Subscription();
|
||||
}
|
||||
|
||||
initAutocomplete() {
|
||||
this.autocompleteResults$ = this.complete.asObservable().pipe(
|
||||
debounceTime(this._debounceTimeAutocompleteMilliseconds()),
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => this.autocompleteProvider.complete(this.uiInput).pipe(takeUntil(this._cancelComplete))),
|
||||
tap((complete) => {
|
||||
if (complete?.length > 0) {
|
||||
this.autocompleteComponent.open();
|
||||
} else {
|
||||
this.autocompleteComponent.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setAutocompleteProvider(provider: FilterAutocompleteProvider) {
|
||||
this._autocompleteProvider = provider;
|
||||
}
|
||||
|
||||
private _debounceTimeAutocompleteMilliseconds(): number {
|
||||
if (!this.autocompleteProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
let debounceTimeMilliseconds: number;
|
||||
switch (this.autocompleteProvider.for) {
|
||||
case 'catalog':
|
||||
debounceTimeMilliseconds = 250;
|
||||
break;
|
||||
case 'goods-in':
|
||||
debounceTimeMilliseconds = 300;
|
||||
break;
|
||||
case 'goods-out':
|
||||
debounceTimeMilliseconds = 300;
|
||||
break;
|
||||
default:
|
||||
debounceTimeMilliseconds = 300;
|
||||
break;
|
||||
}
|
||||
return debounceTimeMilliseconds;
|
||||
}
|
||||
|
||||
emitSearch(query: string) {
|
||||
setTimeout(() => {
|
||||
this.search.emit(query);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputGroupMainComponent } from './filter-input-group-main.component';
|
||||
import { UiAutocompleteModule } from '@ui/autocomplete';
|
||||
import { UiSearchboxNextModule } from '@ui/searchbox';
|
||||
import { UiTooltipModule } from 'apps/ui/tooltip/src/public-api';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiCommonModule, UiSearchboxNextModule, UiAutocompleteModule, UiTooltipModule],
|
||||
exports: [FilterInputGroupMainComponent],
|
||||
declarations: [FilterInputGroupMainComponent],
|
||||
})
|
||||
export class FilterInputGroupMainModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-group-main.component';
|
||||
export * from './filter-input-group-main.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './filter-filter-group-filter';
|
||||
export * from './filter-filter-group-main';
|
||||
export * from './filter-input-group-main';
|
||||
10
apps/shared/components/filter/src/lib/filter.component.html
Normal file
10
apps/shared/components/filter/src/lib/filter.component.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<ui-filter-filter-group-main [inputGroup]="uiFilter?.filter | group: 'main'"></ui-filter-filter-group-main>
|
||||
<ui-filter-input-group-main
|
||||
*ngIf="uiFilter?.input | group: 'main'; let inputGroupMain"
|
||||
[inputGroup]="inputGroupMain"
|
||||
(search)="emitSearch($event)"
|
||||
[loading]="loading"
|
||||
[hint]="hint"
|
||||
[scanner]="scanner"
|
||||
></ui-filter-input-group-main>
|
||||
<ui-filter-filter-group-filter [inputGroup]="uiFilter?.filter | group: 'filter'"></ui-filter-filter-group-filter>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-8;
|
||||
}
|
||||
|
||||
ui-filter-input-group-main {
|
||||
@apply mx-auto w-full;
|
||||
max-width: 600px;
|
||||
}
|
||||
107
apps/shared/components/filter/src/lib/filter.component.ts
Normal file
107
apps/shared/components/filter/src/lib/filter.component.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ElementRef,
|
||||
Renderer2,
|
||||
Inject,
|
||||
AfterViewInit,
|
||||
HostListener,
|
||||
ViewChild,
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
} from '@angular/core';
|
||||
import { FilterInputGroupMainComponent } from './filter-group/filter-input-group-main';
|
||||
import { FilterCustomInputDirective } from './shared/filter-input';
|
||||
import { IFilter, Filter } from './tree/filter';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter',
|
||||
templateUrl: 'filter.component.html',
|
||||
styleUrls: ['filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterComponent implements OnChanges, AfterViewInit {
|
||||
@Input()
|
||||
loading: boolean;
|
||||
|
||||
@Input()
|
||||
hint: string;
|
||||
|
||||
@Output()
|
||||
search = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
filter: IFilter;
|
||||
|
||||
@Input()
|
||||
resizeInputOptionsToElement: string | HTMLElement;
|
||||
|
||||
@Input()
|
||||
scanner = false;
|
||||
|
||||
@ViewChild(FilterInputGroupMainComponent)
|
||||
filterInputGroupMainComponent: FilterInputGroupMainComponent;
|
||||
|
||||
@ContentChildren(FilterCustomInputDirective)
|
||||
customInputs: QueryList<FilterCustomInputDirective>;
|
||||
|
||||
get uiFilter() {
|
||||
return this.filter instanceof Filter && this.filter;
|
||||
}
|
||||
|
||||
constructor(private elementRef: ElementRef, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
ngOnChanges({ filter }: SimpleChanges): void {
|
||||
if (filter) {
|
||||
if (!(this.filter instanceof Filter)) {
|
||||
this.filter = Filter.create(this.filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.resizeInputOptions(), 500);
|
||||
}
|
||||
|
||||
getHtmlElement(element: string | HTMLElement): HTMLElement {
|
||||
return typeof element === 'string' ? this.document.querySelector(element) : element;
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:click', ['$event'])
|
||||
resizeInputOptions() {
|
||||
if (this.resizeInputOptionsToElement) {
|
||||
const inputOptions = this.elementRef.nativeElement.querySelector('.input-options');
|
||||
const targetElement = this.getHtmlElement(this.resizeInputOptionsToElement);
|
||||
|
||||
// set the max-height of the input-options to the height of the actions, depending on the distance between the two
|
||||
if (inputOptions && targetElement) {
|
||||
const distanceBetweenElements = this.getDistanceBetweenElements(inputOptions, targetElement) - 15;
|
||||
this.renderer.setStyle(inputOptions, 'max-height', `${distanceBetweenElements}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDistanceBetweenElements(element1: HTMLElement, element2: HTMLElement) {
|
||||
const rect1 = element1.getBoundingClientRect();
|
||||
const rect2 = element2.getBoundingClientRect();
|
||||
|
||||
return Math.abs(rect1.top - rect2.top);
|
||||
}
|
||||
|
||||
emitSearch(query: string) {
|
||||
setTimeout(() => {
|
||||
this.search.emit(query);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
cancelAutocomplete() {
|
||||
this.filterInputGroupMainComponent?.cancelAutocomplete();
|
||||
}
|
||||
}
|
||||
23
apps/shared/components/filter/src/lib/filter.module.ts
Normal file
23
apps/shared/components/filter/src/lib/filter.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterComponent } from './filter.component';
|
||||
import { InputGroupSelectorPipe } from './pipe';
|
||||
import { FilterInputGroupMainModule } from './filter-group/filter-input-group-main';
|
||||
import { FilterFilterGroupMainModule } from './filter-group/filter-filter-group-main';
|
||||
import { FilterFilterGroupFilterModule } from './filter-group/filter-filter-group-filter';
|
||||
import { FilterInputModule } from './shared/filter-input';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FilterInputGroupMainModule, FilterFilterGroupMainModule, FilterFilterGroupFilterModule, FilterInputModule],
|
||||
exports: [
|
||||
FilterComponent,
|
||||
InputGroupSelectorPipe,
|
||||
FilterInputGroupMainModule,
|
||||
FilterFilterGroupMainModule,
|
||||
FilterFilterGroupFilterModule,
|
||||
FilterInputModule,
|
||||
],
|
||||
declarations: [FilterComponent, InputGroupSelectorPipe],
|
||||
})
|
||||
export class FilterNextModule {}
|
||||
9
apps/shared/components/filter/src/lib/index.ts
Normal file
9
apps/shared/components/filter/src/lib/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './filter-group';
|
||||
export * from './order-by-filter';
|
||||
export * from './pipe';
|
||||
export * from './providers';
|
||||
export * from './shared';
|
||||
export * from './testing';
|
||||
export * from './tree';
|
||||
export * from './ui-filter.component';
|
||||
export * from './ui-filter.module';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './order-by-filter.component';
|
||||
export * from './order-by-filter.module';
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="order-by-filter-button-wrapper">
|
||||
<button
|
||||
[attr.data-label]="label"
|
||||
class="order-by-filter-button"
|
||||
type="button"
|
||||
*ngFor="let label of orderByKeys"
|
||||
(click)="setActive(label)"
|
||||
>
|
||||
<span>
|
||||
{{ label }}
|
||||
</span>
|
||||
<ui-icon
|
||||
[class.asc]="label === activeOrderBy?.label && !activeOrderBy.desc"
|
||||
[class.desc]="label === activeOrderBy?.label && activeOrderBy.desc"
|
||||
icon="arrow"
|
||||
size="14px"
|
||||
></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
:host {
|
||||
@apply box-border flex justify-center items-center;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.order-by-filter-button-wrapper {
|
||||
@apply flex flex-row items-center justify-center;
|
||||
}
|
||||
|
||||
.order-by-filter-button {
|
||||
@apply bg-transparent outline-none border-none text-regular font-bold flex flex-row justify-center items-center m-0 py-2 px-2;
|
||||
}
|
||||
|
||||
::ng-deep .tablet ui-order-by-filter .order-by-filter-button {
|
||||
@apply mx-0;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply hidden transform ml-2 rounded-full p-1;
|
||||
transition: 250ms all ease-in-out;
|
||||
}
|
||||
|
||||
ui-icon.asc,
|
||||
ui-icon.desc {
|
||||
@apply flex rounded-full visible text-white;
|
||||
}
|
||||
|
||||
ui-icon.asc {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
|
||||
ui-icon.desc {
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
::ng-deep .customer ui-order-by-filter {
|
||||
ui-icon {
|
||||
@apply bg-active-customer;
|
||||
}
|
||||
|
||||
.order-by-filter-button {
|
||||
@apply text-active-customer;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .branch ui-order-by-filter {
|
||||
ui-icon {
|
||||
@apply bg-active-branch;
|
||||
}
|
||||
|
||||
.order-by-filter-button {
|
||||
@apply text-active-branch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { UiOrderBy } from '@ui/filter';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-order-by-filter',
|
||||
templateUrl: 'order-by-filter.component.html',
|
||||
styleUrls: ['order-by-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OrderByFilterComponent implements OnInit {
|
||||
@Input()
|
||||
orderBy: UiOrderBy[];
|
||||
|
||||
@Output()
|
||||
selectedOrderByChange = new EventEmitter<UiOrderBy>();
|
||||
|
||||
get orderByKeys() {
|
||||
return this.orderBy?.map((ob) => ob.label).filter((key, idx, self) => self.indexOf(key) === idx);
|
||||
}
|
||||
|
||||
get activeOrderBy() {
|
||||
return this.orderBy?.find((f) => f.selected);
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
setTimeout(() => this.cdr.markForCheck(), 0);
|
||||
}
|
||||
|
||||
setActive(orderBy: string) {
|
||||
const active = this.activeOrderBy;
|
||||
const orderBys = this.orderBy?.filter((f) => f.label === orderBy);
|
||||
let next: UiOrderBy;
|
||||
|
||||
if (orderBys?.length) {
|
||||
if (active?.label !== orderBy) {
|
||||
next = orderBys?.find((f) => !f.desc);
|
||||
} else if (!active.desc) {
|
||||
next = orderBys?.find((f) => f.desc);
|
||||
}
|
||||
}
|
||||
|
||||
this.orderBy?.filter((f) => f.selected)?.forEach((f) => f.setSelected(false));
|
||||
|
||||
next?.setSelected(true);
|
||||
|
||||
this.selectedOrderByChange.next(next);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
import { OrderByFilterComponent } from './order-by-filter.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule],
|
||||
exports: [OrderByFilterComponent],
|
||||
declarations: [OrderByFilterComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class OrderByFilterModule {}
|
||||
1
apps/shared/components/filter/src/lib/pipe/index.ts
Normal file
1
apps/shared/components/filter/src/lib/pipe/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './input-group-selector.pipe';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator';
|
||||
import { IInputGroup } from '../tree';
|
||||
import { InputGroupSelectorPipe } from './input-group-selector.pipe';
|
||||
|
||||
describe('InputGroupSelectorPipe', () => {
|
||||
let spectator: SpectatorPipe<InputGroupSelectorPipe>;
|
||||
const createPipe = createPipeFactory(InputGroupSelectorPipe);
|
||||
const inputGroups: IInputGroup[] = [{ group: 'group1' }, { group: 'group2' }, { group: 'group3' }];
|
||||
|
||||
it('should return the IUiInputGroup with group to be unittest', () => {
|
||||
spectator = createPipe(`{{ (value | group:group)?.group }}`, {
|
||||
hostProps: { value: inputGroups, group: 'group2' },
|
||||
});
|
||||
|
||||
expect(spectator.element).toHaveText('group2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { IInputGroup } from '../tree';
|
||||
|
||||
@Pipe({
|
||||
name: 'group',
|
||||
})
|
||||
export class InputGroupSelectorPipe implements PipeTransform {
|
||||
transform(value: IInputGroup[], group: string): any {
|
||||
return value?.find((f) => f?.group === group);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FilterInput } from '../tree';
|
||||
|
||||
export interface FilterAutocomplete {
|
||||
/**
|
||||
* Anzeige / Bezeichner
|
||||
*/
|
||||
display?: string;
|
||||
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Abfragewert
|
||||
*/
|
||||
query?: string;
|
||||
|
||||
/**
|
||||
* Art (z.B. Titel, Autor, Verlag, ...)
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export abstract class FilterAutocompleteProvider {
|
||||
abstract readonly for: string;
|
||||
|
||||
abstract complete(input: FilterInput): Observable<FilterAutocomplete[]>;
|
||||
}
|
||||
1
apps/shared/components/filter/src/lib/providers/index.ts
Normal file
1
apps/shared/components/filter/src/lib/providers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './filter-autocomplete.provider';
|
||||
@@ -0,0 +1,30 @@
|
||||
<button
|
||||
*ngIf="!uiInput?.hasOptions()"
|
||||
type="button"
|
||||
class="ui-filter-chip"
|
||||
[class.selected]="uiInput?.selected"
|
||||
(click)="uiInput?.setSelected(!uiInput.selected)"
|
||||
[attr.data-label]="uiInput?.label"
|
||||
[attr.data-value]="uiInput?.value"
|
||||
[attr.data-key]="uiInput?.key"
|
||||
[attr.data-type]="uiInput?.type"
|
||||
[attr.data-selected]="uiInput?.selected"
|
||||
>
|
||||
{{ uiInput?.label }}
|
||||
</button>
|
||||
<ng-container *ngIf="uiInput?.hasOptions()">
|
||||
<button
|
||||
*ngFor="let option of uiInput?.options?.values"
|
||||
type="button"
|
||||
class="ui-filter-chip"
|
||||
[class.selected]="option?.selected"
|
||||
(click)="option?.setSelected(!option.selected)"
|
||||
[attr.data-label]="uiInput?.label"
|
||||
[attr.data-value]="uiInput?.value"
|
||||
[attr.data-key]="uiInput?.key"
|
||||
[attr.data-type]="uiInput?.type"
|
||||
[attr.data-selected]="uiInput?.selected"
|
||||
>
|
||||
{{ option?.label }}
|
||||
</button>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,21 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
button.ui-filter-chip {
|
||||
@apply grid grid-flow-col gap-2 items-center rounded-full text-base px-4 py-3 bg-white text-inactive-customer border-none font-bold;
|
||||
}
|
||||
|
||||
/** styling branch bereich **/
|
||||
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip {
|
||||
@apply text-inactive-branch;
|
||||
}
|
||||
|
||||
button.ui-filter-chip.selected {
|
||||
@apply bg-active-customer text-white;
|
||||
}
|
||||
|
||||
/** styling branch bereich **/
|
||||
::ng-deep .branch ui-filter-input-chip button.ui-filter-chip.selected {
|
||||
@apply bg-active-branch;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
// import { UiIconModule } from '@ui/icon';
|
||||
// import { UiInput, UiInputGroup, UiInputType } from '../../tree';
|
||||
// import { UiFilterInputChipComponent } from './filter-input-chip.component';
|
||||
|
||||
// describe('UiFilterInputChipComponent', () => {
|
||||
// let spectator: Spectator<UiFilterInputChipComponent>;
|
||||
// const createComponent = createComponentFactory({
|
||||
// component: UiFilterInputChipComponent,
|
||||
// imports: [UiIconModule],
|
||||
// });
|
||||
|
||||
// beforeEach(() => {
|
||||
// spectator = createComponent();
|
||||
// });
|
||||
|
||||
// it('should create', () => {
|
||||
// expect(spectator.component).toBeTruthy();
|
||||
// });
|
||||
|
||||
// describe('input', () => {
|
||||
// it('should create an instance of UiInput if value is a plain object', () => {
|
||||
// spectator.setInput({
|
||||
// input: { type: UiInputType.NotSet },
|
||||
// });
|
||||
|
||||
// expect(spectator.component.uiInput instanceof UiInput).toBe(true);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('button.ui-filter-chip Element', () => {
|
||||
// it('should have the text content of uiInput.label', () => {
|
||||
// spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ label: 'My Label', type: UiInputType.NotSet }));
|
||||
// spectator.detectComponentChanges();
|
||||
// expect(spectator.query('button.ui-filter-chip')).toContainText('My Label');
|
||||
// });
|
||||
|
||||
// it('should have the class selected if uiInput.selected is true', () => {
|
||||
// spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ selected: true, type: UiInputType.NotSet }));
|
||||
// spectator.detectComponentChanges();
|
||||
// expect(spectator.query('button.ui-filter-chip')).toHaveClass('selected');
|
||||
// });
|
||||
|
||||
// it('should not have the class selected if uiInput.selected is true', () => {
|
||||
// spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ selected: false, type: UiInputType.NotSet }));
|
||||
// spectator.detectComponentChanges();
|
||||
// expect(spectator.query('button.ui-filter-chip')).not.toHaveClass('selected');
|
||||
// });
|
||||
|
||||
// it('should call uiInput.setSelected(!uiInput.selected) on click', () => {
|
||||
// spyOnProperty(spectator.component, 'uiInput', 'get').and.returnValue(UiInput.create({ type: UiInputType.NotSet }));
|
||||
// spyOn(spectator.component.uiInput, 'setSelected');
|
||||
// spectator.detectComponentChanges();
|
||||
// spectator.click('button.ui-filter-chip');
|
||||
// expect(spectator.component.uiInput.setSelected).toHaveBeenCalledWith(!spectator.component.uiInput.selected);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('ui-icon Element', () => {
|
||||
// it('should not be visible if selected is false', () => {
|
||||
// spectator.setInput({ input: { type: 0, selected: false } });
|
||||
// spectator.detectComponentChanges();
|
||||
// expect(spectator.query('ui-icon')).not.toBeVisible();
|
||||
// });
|
||||
|
||||
// it('should be visible if selected is true', () => {
|
||||
// spectator.setInput({ input: { type: 0, selected: true } });
|
||||
// spectator.detectComponentChanges();
|
||||
// expect(spectator.query('ui-icon')).toBeVisible();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import { IInput, FilterInput } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-input-chip',
|
||||
templateUrl: 'filter-input-chip.component.html',
|
||||
styleUrls: ['filter-input-chip.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputChipComponent implements OnDestroy {
|
||||
private _input: FilterInput;
|
||||
|
||||
@Input()
|
||||
set input(value: IInput) {
|
||||
if (value instanceof FilterInput) {
|
||||
this._input = value;
|
||||
} else {
|
||||
this._input = FilterInput.create(value);
|
||||
}
|
||||
this.registerChanges();
|
||||
}
|
||||
|
||||
get uiInput() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private changeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unregisterChanges();
|
||||
}
|
||||
|
||||
registerChanges() {
|
||||
this.unregisterChanges();
|
||||
|
||||
if (this.uiInput) {
|
||||
merge(this.uiInput.changes, ...(this.uiInput?.options?.values?.map((o) => o.changes) || [])).subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unregisterChanges() {
|
||||
this.changeSubscription?.unsubscribe();
|
||||
this.changeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputChipComponent } from './filter-input-chip.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [FilterInputChipComponent],
|
||||
declarations: [FilterInputChipComponent],
|
||||
})
|
||||
export class FilterInputChipModule {}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './filter-input-chip.component';
|
||||
export * from './filter-input-chip.module';
|
||||
@@ -0,0 +1,24 @@
|
||||
<div
|
||||
class="ui-option"
|
||||
[attr.data-label]="uiOption?.label"
|
||||
[attr.data-value]="uiOption?.value"
|
||||
[attr.data-key]="uiOption?.key"
|
||||
[attr.data-selected]="uiOption?.selected"
|
||||
[attr.data-target]="uiOption?.expanded"
|
||||
>
|
||||
<ui-checkbox [ngModel]="uiOption?.selected" (ngModelChange)="uiOption?.setSelected($event)" [indeterminate]="hasPartiallyCheckedChildren">
|
||||
{{ uiOption?.label }}
|
||||
</ui-checkbox>
|
||||
<button
|
||||
class="btn-expand"
|
||||
(click)="uiOption.setExpanded(!uiOption?.expanded)"
|
||||
[class.expanded]="uiOption?.expanded"
|
||||
type="button"
|
||||
*ngIf="uiOption?.values?.length"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="uiOption?.expanded">
|
||||
<ui-input-option-bool class="ml-10" *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-bool>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row;
|
||||
}
|
||||
|
||||
.ui-option {
|
||||
@apply px-4 pt-2 pb-4 flex flex-row justify-between items-center;
|
||||
|
||||
.btn-expand {
|
||||
@apply border-none outline-none bg-transparent text-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-all transform rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-expand.expanded ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-bool ui-checkbox ui-icon {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { FilterInputOptionBoolComponent } from './filter-input-option-bool.component';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { UiCheckboxComponent } from '@ui/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
describe('UiFilterInputOptionBoolComponent', () => {
|
||||
let spectator: Spectator<FilterInputOptionBoolComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
imports: [FormsModule],
|
||||
component: FilterInputOptionBoolComponent,
|
||||
declarations: [MockComponent(UiCheckboxComponent)],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IOption, Option } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-option-bool',
|
||||
templateUrl: 'filter-input-option-bool.component.html',
|
||||
styleUrls: ['filter-input-option-bool.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputOptionBoolComponent implements OnDestroy {
|
||||
private _option: Option;
|
||||
|
||||
@Input()
|
||||
set option(value: IOption) {
|
||||
if (value instanceof Option) {
|
||||
this._option = value;
|
||||
} else {
|
||||
this._option = Option.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
@Output() optionChange = new EventEmitter<Option>();
|
||||
|
||||
get uiOption() {
|
||||
return this._option;
|
||||
}
|
||||
|
||||
optionChangeSubscription = new Subscription();
|
||||
|
||||
get hasPartiallyCheckedChildren() {
|
||||
const options = this.uiOption?.values ?? [];
|
||||
return !options.every((option) => option.selected) && options.some((option) => option.selected);
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiOption.changes.subscribe((change) => {
|
||||
this.cdr.markForCheck();
|
||||
this.optionChange.next(change.target);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputOptionBoolComponent } from './filter-input-option-bool.component';
|
||||
import { UiCheckboxModule } from '@ui/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, UiCheckboxModule, UiIconModule],
|
||||
exports: [FilterInputOptionBoolComponent],
|
||||
declarations: [FilterInputOptionBoolComponent],
|
||||
})
|
||||
export class FilterInputOptionBoolModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-bool.component';
|
||||
export * from './filter-input-option-bool.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,70 @@
|
||||
<div class="options-wrapper">
|
||||
<div
|
||||
*ngIf="uiStartOption"
|
||||
class="option"
|
||||
[attr.data-label]="uiStartOption?.label"
|
||||
[attr.data-value]="uiStartOption?.value"
|
||||
[attr.data-key]="uiStartOption?.key"
|
||||
[attr.data-selected]="uiStartOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStartOption?.label }}: </span>
|
||||
<button
|
||||
class="cta-picker"
|
||||
[class.open]="dpStartTrigger?.opened"
|
||||
[uiOverlayTrigger]="dpStart"
|
||||
#dpStartTrigger="uiOverlayTrigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{{ uiStartOption?.value | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker
|
||||
class="dp-left"
|
||||
#dpStart
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
[ngModel]="uiStartOption?.value"
|
||||
saveLabel="Übernehmen"
|
||||
(save)="uiStartOption?.setValue($event)"
|
||||
>
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="uiStopOption"
|
||||
class="option"
|
||||
[attr.data-label]="uiStopOption?.label"
|
||||
[attr.data-value]="uiStopOption?.value"
|
||||
[attr.data-key]="uiStopOption?.key"
|
||||
[attr.data-selected]="uiStopOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStopOption?.label }}: </span>
|
||||
<button
|
||||
class="cta-picker"
|
||||
[class.open]="dpStopTrigger?.opened"
|
||||
[uiOverlayTrigger]="dpStop"
|
||||
#dpStopTrigger="uiOverlayTrigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{{ uiStopOptionValue | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker
|
||||
class="dp-right"
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
#dpStop
|
||||
[ngModel]="uiStopOptionValue"
|
||||
(save)="setStopValue($event)"
|
||||
saveLabel="Übernehmen"
|
||||
>
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
:host {
|
||||
@apply block p-4;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
@apply grid grid-flow-col justify-start gap-4;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply font-bold;
|
||||
|
||||
button.cta-picker {
|
||||
@apply bg-transparent text-base outline-none border-none font-bold inline-flex flex-row items-center justify-between;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply ml-2 transition transform rotate-90 text-cool-grey;
|
||||
}
|
||||
|
||||
button.cta-picker.open ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
@apply grid grid-flow-col gap-2 items-center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-date-range ui-datepicker.dp-left {
|
||||
.dp {
|
||||
left: 102px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-date-range ui-datepicker.dp-right {
|
||||
.dp {
|
||||
right: -6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
// import { UiInput, UiInputType, UiOption } from '../../../tree';
|
||||
// import { UiFilterInputOptionDateRangeComponent } from './filter-input-option-date-range.component';
|
||||
|
||||
// describe('UiFilterInputOptionDateRangeComponent', () => {
|
||||
// let spectator: Spectator<UiFilterInputOptionDateRangeComponent>;
|
||||
// const createComponent = createComponentFactory({
|
||||
// component: UiFilterInputOptionDateRangeComponent,
|
||||
// });
|
||||
|
||||
// beforeEach(() => {
|
||||
// spectator = createComponent();
|
||||
// });
|
||||
|
||||
// it('should create', () => {
|
||||
// expect(spectator.component).toBeTruthy();
|
||||
// });
|
||||
|
||||
// describe('set options(value: IUiOption[])', () => {
|
||||
// it('should create an instance of UiOption when values are not an instance of UiOption', () => {
|
||||
// spectator.setInput({
|
||||
// options: [{ key: 'start' }, { key: 'stop' }],
|
||||
// });
|
||||
|
||||
// spectator.component['_options'].forEach((uiOption) => {
|
||||
// expect(uiOption instanceof UiOption).toBe(true);
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should set the values when vales are an isntance of UiOption', () => {
|
||||
// const option1 = UiOption.create({ key: 'start' });
|
||||
// const option2 = UiOption.create({ key: 'stop' });
|
||||
// spectator.setInput({
|
||||
// options: [option1, option2],
|
||||
// });
|
||||
|
||||
// expect(spectator.component['_options'][0]).toBe(option1);
|
||||
// expect(spectator.component['_options'][1]).toBe(option2);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('get uiOptions()', () => {
|
||||
// it('should return the value of _options', () => {
|
||||
// const option1 = UiOption.create({ key: 'start' });
|
||||
// const option2 = UiOption.create({ key: 'stop' });
|
||||
// const options = [option1, option2];
|
||||
// spectator.component['_options'] = options;
|
||||
|
||||
// expect(spectator.component.uiOptions).toBe(options);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('get uiStartOption()', () => {
|
||||
// it('should retun the option with the key start', () => {
|
||||
// const option1 = UiOption.create({ key: 'start' });
|
||||
// const option2 = UiOption.create({ key: 'stop' });
|
||||
// const options = [option1, option2];
|
||||
// spectator.component['_options'] = options;
|
||||
|
||||
// expect(spectator.component.uiStartOption).toBe(option1);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('get uiStopOption()', () => {
|
||||
// it('should return the option with the key stop', () => {
|
||||
// const option1 = UiOption.create({ key: 'start' });
|
||||
// const option2 = UiOption.create({ key: 'stop' });
|
||||
// const options = [option1, option2];
|
||||
// spectator.component['_options'] = options;
|
||||
|
||||
// expect(spectator.component.uiStopOption).toBe(option2);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IOption, Option } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-option-date-range',
|
||||
templateUrl: 'filter-input-option-date-range.component.html',
|
||||
styleUrls: ['filter-input-option-date-range.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputOptionDateRangeComponent {
|
||||
private _options: Option[];
|
||||
|
||||
@Input()
|
||||
set options(value: IOption[]) {
|
||||
this._options = value?.map((option) => (option instanceof Option ? option : Option.create(option)));
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOptions() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get uiStartOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'start');
|
||||
}
|
||||
|
||||
get uiStopOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'stop');
|
||||
}
|
||||
|
||||
get uiStopOptionValue() {
|
||||
const stopDate = new Date(this.uiStopOption?.value);
|
||||
stopDate?.setDate(stopDate?.getDate() - 1); // to update the view correctly after setStopValue() gets called !
|
||||
return stopDate?.toJSON();
|
||||
}
|
||||
|
||||
optionChangeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiStartOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStartOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.uiStopOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStopOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription?.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
|
||||
setStopValue(date: Date) {
|
||||
const stopDate = date;
|
||||
stopDate?.setDate(stopDate?.getDate() + 1); // to include the selected stop date !
|
||||
this.uiStopOption?.setValue(stopDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputOptionDateRangeComponent } from './filter-input-option-date-range.component';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiCommonModule, UiDatepickerModule, FormsModule, UiIconModule],
|
||||
exports: [FilterInputOptionDateRangeComponent],
|
||||
declarations: [FilterInputOptionDateRangeComponent],
|
||||
})
|
||||
export class FilterInputOptionDateRangeModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-date-range.component';
|
||||
export * from './filter-input-option-date-range.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="options-wrapper">
|
||||
<div
|
||||
class="option"
|
||||
*ngIf="uiStartOption"
|
||||
[attr.data-label]="uiStartOption?.label"
|
||||
[attr.data-value]="uiStartOption?.value"
|
||||
[attr.data-key]="uiStartOption?.key"
|
||||
[attr.data-selected]="uiStartOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<ui-form-control [label]="uiStartOption?.label">
|
||||
<input type="text" [ngModel]="uiStartOption?.value" (ngModelChange)="uiStartOption?.setValue($event)" />
|
||||
</ui-form-control>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="option"
|
||||
*ngIf="uiStopOption"
|
||||
[attr.data-label]="uiStopOption?.label"
|
||||
[attr.data-value]="uiStopOption?.value"
|
||||
[attr.data-key]="uiStopOption?.key"
|
||||
[attr.data-selected]="uiStopOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<ui-form-control [label]="uiStopOption?.label">
|
||||
<input type="text" [ngModel]="uiStopOption?.value" (ngModelChange)="uiStopOption?.setValue($event)" />
|
||||
</ui-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="ui-filter-date-range-validation">
|
||||
{{ uiStartOption?.validate() || uiStopOption?.validate() }}
|
||||
</p>
|
||||
@@ -0,0 +1,21 @@
|
||||
:host {
|
||||
@apply block p-4;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
@apply grid grid-flow-col justify-start gap-4;
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
@apply grid grid-flow-col gap-2 items-center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.ui-filter-date-range-validation {
|
||||
@apply text-brand text-base font-bold;
|
||||
}
|
||||
|
||||
ui-form-control input {
|
||||
max-width: 200px;
|
||||
@apply px-0 rounded-none;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IOption, Option } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-option-number-range',
|
||||
templateUrl: 'filter-input-option-number-range.component.html',
|
||||
styleUrls: ['filter-input-option-number-range.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InputOptionNumberRangeComponent {
|
||||
private _options: Option[];
|
||||
|
||||
@Input()
|
||||
set options(value: IOption[]) {
|
||||
this._options = value?.map((option) => (option instanceof Option ? option : Option.create(option)));
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOptions() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get uiStartOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'start');
|
||||
}
|
||||
|
||||
get uiStopOption() {
|
||||
return this.uiOptions?.find((o) => o.key === 'stop');
|
||||
}
|
||||
|
||||
optionChangeSubscription: Subscription;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiStartOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStartOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.uiStopOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStopOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription?.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { InputOptionNumberRangeComponent } from './filter-input-option-number-range.component';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiFormControlModule, FormsModule],
|
||||
exports: [InputOptionNumberRangeComponent],
|
||||
declarations: [InputOptionNumberRangeComponent],
|
||||
})
|
||||
export class InputOptionNumberRangeModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-number-range.component';
|
||||
export * from './filter-input-option-number-range.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,24 @@
|
||||
<div
|
||||
class="ui-option"
|
||||
[attr.data-label]="uiOption?.label"
|
||||
[attr.data-value]="uiOption?.value"
|
||||
[attr.data-key]="uiOption?.key"
|
||||
[attr.data-selected]="uiOption?.selected"
|
||||
>
|
||||
<div>
|
||||
<ui-switch [ngModel]="uiOption?.selected" (ngModelChange)="uiOption?.setSelected($event)" labelOn="mit" labelOff="ohne"> </ui-switch>
|
||||
{{ uiOption?.label }}
|
||||
</div>
|
||||
<button
|
||||
class="btn-expand"
|
||||
(click)="uiOption.setExpanded(!uiOption?.expanded)"
|
||||
[class.expanded]="uiOption?.expanded"
|
||||
type="button"
|
||||
*ngIf="uiOption?.values?.length"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="uiOption?.expanded">
|
||||
<ui-input-option-tri-state *ngFor="let subOption of uiOption?.values" [option]="subOption"></ui-input-option-tri-state>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,27 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row;
|
||||
}
|
||||
|
||||
ui-switch {
|
||||
@apply mr-2;
|
||||
}
|
||||
|
||||
.ui-option {
|
||||
@apply px-4 py-2 flex flex-row justify-between items-center;
|
||||
|
||||
.btn-expand {
|
||||
@apply border-none outline-none bg-transparent text-cool-grey;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-all transform rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-expand.expanded ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep ui-input-option-bool ui-checkbox ui-icon {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, ChangeDetectionStrategy, OnDestroy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IOption, Option } from '../../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-option-tri-state',
|
||||
templateUrl: 'filter-input-option-tri-state.component.html',
|
||||
styleUrls: ['filter-input-option-tri-state.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InputOptionTriStateComponent implements OnDestroy {
|
||||
private _option: Option;
|
||||
|
||||
@Input()
|
||||
set option(value: IOption) {
|
||||
if (value instanceof Option) {
|
||||
this._option = value;
|
||||
} else {
|
||||
this._option = Option.create(value);
|
||||
}
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
get uiOption() {
|
||||
return this._option;
|
||||
}
|
||||
|
||||
optionChangeSubscription = new Subscription();
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeChanges();
|
||||
}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiOption) {
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiOption.changes.subscribe(() => {
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeChanges() {
|
||||
this.optionChangeSubscription.unsubscribe();
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { InputOptionTriStateComponent } from './filter-input-option-tri-state.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSwitchModule } from '@ui/switch';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiSwitchModule, FormsModule, UiIconModule],
|
||||
exports: [InputOptionTriStateComponent],
|
||||
declarations: [InputOptionTriStateComponent],
|
||||
})
|
||||
export class InputOptionTriStateModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './filter-input-option-tri-state.component';
|
||||
export * from './filter-input-option-tri-state.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="input-options-wrapper">
|
||||
<div class="hidden-overflow">
|
||||
<div class="input-options-header" [class.header-shadow]="scrollPersantage > 0">
|
||||
<button type="button" (click)="setSelected(undefined)">
|
||||
Alle entfernen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="setSelected(true)"
|
||||
*ngIf="!uiInputOptions?.max && (uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4)"
|
||||
>
|
||||
Alle auswählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-options-wrapper">
|
||||
<p class="input-desription">
|
||||
{{ uiInputOptions?.parent?.description }}
|
||||
</p>
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 2 || uiInputOptions?.parent?.type === 4">
|
||||
<div class="input-options" #inputOptionsConainter (scroll)="markForCheck()">
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 2">
|
||||
<ui-input-option-bool
|
||||
*ngFor="let option of uiInputOptions?.values"
|
||||
[option]="option"
|
||||
(optionChange)="optionChange($event)"
|
||||
></ui-input-option-bool>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="uiInputOptions?.parent?.type === 4">
|
||||
<ui-input-option-tri-state *ngFor="let option of uiInputOptions?.values" [option]="option"> </ui-input-option-tri-state>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button class="cta-scroll" [class.up]="scrollPersantage > 20" *ngIf="scrollable" (click)="scroll(20)">
|
||||
<ui-icon icon="arrow" size="20px"></ui-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ui-input-option-date-range *ngIf="uiInputOptions?.parent?.type === 128" [options]="uiInputOptions?.values">
|
||||
</ui-input-option-date-range>
|
||||
<ui-input-option-number-range *ngIf="uiInputOptions?.parent?.type === 4096" [options]="uiInputOptions?.values">
|
||||
</ui-input-option-number-range>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.input-desription {
|
||||
@apply my-0 px-4 font-bold text-base;
|
||||
}
|
||||
|
||||
.input-options-wrapper {
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply grid bg-white rounded-card;
|
||||
}
|
||||
|
||||
.hidden-overflow {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.input-options-header {
|
||||
@apply grid grid-flow-col justify-end items-center mb-2;
|
||||
|
||||
button {
|
||||
@apply bg-transparent p-4 text-base outline-none border-none font-semibold text-inactive-customer;
|
||||
}
|
||||
|
||||
&.header-shadow {
|
||||
@apply shadow-card;
|
||||
}
|
||||
}
|
||||
|
||||
.input-options-wrapper {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.input-options {
|
||||
@apply overflow-scroll;
|
||||
}
|
||||
|
||||
.cta-scroll {
|
||||
@apply absolute bottom-4 right-4 shadow-cta border-none outline-none bg-white w-10 h-10 rounded-full flex flex-row flex-nowrap items-center justify-center text-active-customer;
|
||||
|
||||
ui-icon {
|
||||
@apply transition-transform transform rotate-90;
|
||||
}
|
||||
|
||||
&.up ui-icon {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .branch ui-filter-input-options {
|
||||
.cta-scroll {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
|
||||
.input-options-header {
|
||||
button {
|
||||
@apply text-cool-grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
// import { UiFilterInputOptionsComponent } from './filter-input-options.component';
|
||||
|
||||
// describe('UiFilterInputOptionsComponent', () => {
|
||||
// let spectator: Spectator<UiFilterInputOptionsComponent>;
|
||||
// const createComponent = createComponentFactory({
|
||||
// component: UiFilterInputOptionsComponent,
|
||||
// });
|
||||
|
||||
// beforeEach(() => {
|
||||
// spectator = createComponent();
|
||||
// });
|
||||
|
||||
// it('should create', () => {
|
||||
// expect(spectator.component).toBeTruthy();
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IInputOptions, InputOptions, InputType, Option } from '../../tree';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-input-options',
|
||||
templateUrl: 'filter-input-options.component.html',
|
||||
styleUrls: ['filter-input-options.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputOptionsComponent {
|
||||
@ViewChild('inputOptionsConainter', { read: ElementRef, static: false })
|
||||
inputOptionsConainter: ElementRef;
|
||||
|
||||
get inputOptionsConainterNative(): HTMLElement {
|
||||
return this.inputOptionsConainter?.nativeElement;
|
||||
}
|
||||
|
||||
private _inputOptions: InputOptions;
|
||||
|
||||
@Input()
|
||||
set inputOptions(value: IInputOptions) {
|
||||
if (value instanceof InputOptions) {
|
||||
this._inputOptions = value;
|
||||
} else {
|
||||
this._inputOptions = InputOptions.create(value);
|
||||
}
|
||||
|
||||
this.markForCheck();
|
||||
}
|
||||
|
||||
get uiInputOptions() {
|
||||
return this._inputOptions;
|
||||
}
|
||||
|
||||
get scrollable() {
|
||||
return this.inputOptionsConainterNative?.scrollHeight > this.inputOptionsConainterNative?.clientHeight;
|
||||
}
|
||||
|
||||
get scrollPersantage() {
|
||||
const scrollHeight = this.inputOptionsConainterNative?.scrollHeight - this.inputOptionsConainterNative?.clientHeight;
|
||||
if (scrollHeight === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const scrollTop = this.inputOptionsConainterNative?.scrollTop;
|
||||
|
||||
return (100 / scrollHeight) * scrollTop;
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
optionChange(option: Option) {
|
||||
if (!this.uiInputOptions?.max || !option.selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const max = this.uiInputOptions.max;
|
||||
const selectedOptions = this.uiInputOptions.values.filter((o) => o.selected);
|
||||
|
||||
if (selectedOptions.length > max) {
|
||||
const optionsToUnselect = selectedOptions.filter((o) => o.label !== option.label);
|
||||
optionsToUnselect.forEach((option) => option.setSelected(false));
|
||||
}
|
||||
}
|
||||
|
||||
markForCheck() {
|
||||
setTimeout(() => this.cdr.markForCheck(), 0);
|
||||
}
|
||||
|
||||
setSelected(value?: boolean) {
|
||||
this.uiInputOptions.values?.forEach((option) => option.setSelected(value));
|
||||
}
|
||||
|
||||
scroll(percent: number) {
|
||||
if (this.scrollPersantage > percent) {
|
||||
this.scrollToTop();
|
||||
} else {
|
||||
this.scrollBottom();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
this.inputOptionsConainterNative?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
scrollBottom() {
|
||||
this.inputOptionsConainterNative?.scrollTo({
|
||||
top: this.inputOptionsConainterNative?.scrollHeight - this.inputOptionsConainterNative?.clientHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputOptionsComponent } from './filter-input-options.component';
|
||||
import { FilterInputOptionBoolModule } from './filter-input-option-bool';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { FilterInputOptionDateRangeModule } from './filter-input-option-date-range';
|
||||
import { InputOptionTriStateModule } from './filter-input-option-tri-state';
|
||||
import { InputOptionNumberRangeModule } from './filter-input-option-number-range';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiIconModule,
|
||||
FilterInputOptionBoolModule,
|
||||
FilterInputOptionDateRangeModule,
|
||||
InputOptionTriStateModule,
|
||||
InputOptionNumberRangeModule,
|
||||
],
|
||||
exports: [FilterInputOptionsComponent],
|
||||
declarations: [FilterInputOptionsComponent],
|
||||
})
|
||||
export class FilterInputOptionsModule {}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './filter-input-option-bool';
|
||||
export * from './filter-input-option-date-range';
|
||||
export * from './filter-input-option-number-range';
|
||||
export * from './filter-input-option-tri-state';
|
||||
export * from './filter-input-options.component';
|
||||
export * from './filter-input-options.module';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Directive, Input } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { IInput, FilterInput } from '../../tree';
|
||||
|
||||
@Directive({})
|
||||
export abstract class AbstractUiFilterInputDirective {
|
||||
private _input: FilterInput;
|
||||
|
||||
@Input()
|
||||
set input(value: IInput) {
|
||||
if (value instanceof FilterInput) {
|
||||
this._input = value;
|
||||
} else {
|
||||
this._input = FilterInput.create(value);
|
||||
}
|
||||
|
||||
this._onUiInputChange.next(this._input);
|
||||
}
|
||||
|
||||
get input() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
get uiInput() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.uiInput?.value;
|
||||
}
|
||||
|
||||
protected _onUiInputChange = new Subject<FilterInput>();
|
||||
|
||||
onUiInputChange$ = this._onUiInputChange.asObservable();
|
||||
|
||||
constructor() {}
|
||||
|
||||
setValue(value: string) {
|
||||
this.uiInput.setValue(value, { emitEvent: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Directive, Input, TemplateRef } from '@angular/core';
|
||||
import { FilterInput } from '../../tree';
|
||||
|
||||
export interface UiInputContext {
|
||||
$implicit: Input;
|
||||
}
|
||||
|
||||
@Directive({ selector: '[sharedFilterCustomInput]' })
|
||||
export class FilterCustomInputDirective {
|
||||
@Input('uiFilterCustomInput')
|
||||
key: string;
|
||||
|
||||
constructor(public templateRef: TemplateRef<UiInputContext>) {}
|
||||
|
||||
static ngTemplateContextGuard<T>(dir: FilterCustomInputDirective, ctx: any): ctx is UiInputContext {
|
||||
return ctx.$implicit instanceof Input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FilterCustomInputDirective } from './custom-input.directive';
|
||||
import { FilterInputTextModule } from './input-text';
|
||||
|
||||
@NgModule({
|
||||
imports: [FilterInputTextModule],
|
||||
declarations: [FilterCustomInputDirective],
|
||||
exports: [FilterInputTextModule, FilterCustomInputDirective],
|
||||
})
|
||||
export class FilterInputModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './input-text';
|
||||
export * from './abstract-filter-input.directive';
|
||||
export * from './custom-input.directive';
|
||||
export * from './filter-input.module';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './input-text.component';
|
||||
export * from './input-text.module';
|
||||
@@ -0,0 +1,4 @@
|
||||
<ui-form-control [label]="input?.label" class="w-[200px]">
|
||||
<input type="text" [formControl]="control" />
|
||||
</ui-form-control>
|
||||
<p class="text-brand text-base font-bold" *ngIf="control.errors?.pattern">Ungültige {{ input?.label }}</p>
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AbstractUiFilterInputDirective } from '../abstract-filter-input.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-filter-input-text',
|
||||
templateUrl: 'input-text.component.html',
|
||||
styleUrls: ['input-text.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputTextComponent extends AbstractUiFilterInputDirective implements OnInit, OnDestroy {
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
control = new FormControl<string>('');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.control.setValue(this.value);
|
||||
this.updateValidator();
|
||||
|
||||
const onInputChangeSub = this.onUiInputChange$.subscribe((input) => {
|
||||
if (this.control.value !== input.value) this.control.setValue(input.value);
|
||||
|
||||
this.updateValidator();
|
||||
});
|
||||
|
||||
const onControlValueChangeSub = this.control.valueChanges.subscribe((value) => {
|
||||
if (this.value !== value) this.setValue(value);
|
||||
});
|
||||
|
||||
this._subscriptions.add(onInputChangeSub);
|
||||
this._subscriptions.add(onControlValueChangeSub);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
updateValidator() {
|
||||
if (this.input.constraint) {
|
||||
this.control.setValidators(Validators.pattern(this.input.constraint));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputTextComponent } from './input-text.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ReactiveFormsModule, UiFormControlModule],
|
||||
exports: [FilterInputTextComponent],
|
||||
declarations: [FilterInputTextComponent],
|
||||
})
|
||||
export class FilterInputTextModule {}
|
||||
3
apps/shared/components/filter/src/lib/shared/index.ts
Normal file
3
apps/shared/components/filter/src/lib/shared/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './filter-input';
|
||||
export * from './filter-input-chip';
|
||||
export * from './filter-input-options';
|
||||
3
apps/shared/components/filter/src/lib/testing/index.ts
Normal file
3
apps/shared/components/filter/src/lib/testing/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './query-settings.data';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,111 @@
|
||||
import { IFilter } from '../tree';
|
||||
|
||||
export const querySettingsData: IFilter = {
|
||||
input: [{ group: 'main', input: [{ key: 'qs', placeholder: 'ISBN/EAN, Titel, Kundenname', type: 1, constraint: '^.{2,100}$' }] }],
|
||||
filter: [
|
||||
{
|
||||
group: 'main',
|
||||
input: [
|
||||
{ key: 'all_branches', label: 'Alle Filialen', type: 2, target: 'filter' },
|
||||
{ key: 'customer_name', label: 'Kundenname', type: 2, target: 'input' },
|
||||
{ key: 'product_contributor', label: 'Autor', type: 2, target: 'input' },
|
||||
{ key: 'product_name', label: 'Titel', type: 2, target: 'input' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'filter',
|
||||
input: [
|
||||
{
|
||||
key: 'orderitemprocessingstatus',
|
||||
label: 'Status',
|
||||
type: 4,
|
||||
target: 'filter',
|
||||
options: {
|
||||
values: [
|
||||
{ label: 'abgeholt', value: '256' },
|
||||
{ label: 'angefragt', value: '524288' },
|
||||
{ label: 'ans Lager (nicht abgeholt)', value: '262144' },
|
||||
{ label: 'bestellt', value: '16', selected: true },
|
||||
{ label: 'derzeit nicht lieferbar', value: '16777216' },
|
||||
{ label: 'eingetroffen', value: '128' },
|
||||
{ label: 'Lieferant wird ermittelt', value: '8388608' },
|
||||
{ label: 'nachbestellt', value: '8192', selected: true },
|
||||
{ label: 'neu', value: '1' },
|
||||
{ label: 'nicht lieferbar', value: '4096' },
|
||||
{ label: 'storniert', value: '1024', selected: true },
|
||||
{ label: 'storniert (Kunde)', value: '512', selected: true },
|
||||
{ label: 'storniert (Lieferant)', value: '2048', selected: true },
|
||||
{ label: 'versendet', value: '64' },
|
||||
{ label: 'weitergeleitet intern', value: '1048576' },
|
||||
{ label: 'zugestellt', value: '4194304' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'estimatedshippingdate',
|
||||
label: 'vsl. Lieferdatum',
|
||||
description: 'Geben Sie das vsl. Lieferdatum ein, um die Suche zu verfeinern.',
|
||||
type: 128,
|
||||
target: 'filter',
|
||||
options: {
|
||||
values: [
|
||||
{ key: 'start', label: 'von' },
|
||||
{ key: 'stop', label: 'bis' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'supplier',
|
||||
label: 'Lieferant',
|
||||
type: 2,
|
||||
target: 'filter',
|
||||
options: {
|
||||
values: [
|
||||
{ label: 'Dummy', value: 'D' },
|
||||
{ label: 'Filiale', value: 'F' },
|
||||
{ label: 'H', value: 'H' },
|
||||
{ label: 'Hugendubel Digital', value: 'DIG' },
|
||||
{ label: 'KNV', value: 'K' },
|
||||
{ label: 'KNV-Ausland', value: 'KA' },
|
||||
{ label: 'KNV-Import', value: 'G' },
|
||||
{ label: 'KNV-Import (Baker & Taylor)', value: 'T' },
|
||||
{ label: 'Libri', value: 'L' },
|
||||
{ label: 'Libri Verlag', value: 'LV1' },
|
||||
{ label: 'Libri-Import', value: 'I' },
|
||||
{ label: 'Libri-Import (Bertrams)', value: 'B' },
|
||||
{ label: 'Petersen', value: 'P' },
|
||||
{ label: 'Verlag', value: 'LV3' },
|
||||
{ label: 'Weltbild', value: 'W' },
|
||||
{ label: 'ZL', value: 'Z' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'channel',
|
||||
label: 'Bestellkanal',
|
||||
type: 2,
|
||||
target: 'filter',
|
||||
options: {
|
||||
values: [
|
||||
{ label: 'hugendubel.de', value: '8;16' },
|
||||
{ label: 'Filiale', value: '2' },
|
||||
{ label: 'HSC', value: '4' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
orderBy: [
|
||||
{
|
||||
by: 'by1',
|
||||
desc: true,
|
||||
label: 'label1',
|
||||
},
|
||||
{
|
||||
by: 'by2',
|
||||
desc: false,
|
||||
label: 'label2',
|
||||
},
|
||||
],
|
||||
};
|
||||
25
apps/shared/components/filter/src/lib/tree/filter.spec.ts
Normal file
25
apps/shared/components/filter/src/lib/tree/filter.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { querySettingsData } from '../testing';
|
||||
import { Filter } from './filter';
|
||||
import { InputGroup } from './input-group';
|
||||
import { OrderBy } from './order-by';
|
||||
|
||||
describe('Filter', () => {
|
||||
const testData = querySettingsData;
|
||||
|
||||
let uiFilter: Filter;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(InputGroup, 'create');
|
||||
spyOn(OrderBy, 'create');
|
||||
uiFilter = Filter.create(testData);
|
||||
});
|
||||
|
||||
describe('static create(inputOptions: IUiQuerySettingsDTO)', () => {
|
||||
it('should return an instance of UiInputDTO', () => {
|
||||
expect(uiFilter instanceof Filter).toBe(true);
|
||||
testData.filter.forEach((filter) => expect(InputGroup.create).toHaveBeenCalled());
|
||||
testData.input.forEach((input) => expect(InputGroup.create).toHaveBeenCalled());
|
||||
testData.orderBy.forEach((orderBy) => expect(OrderBy.create).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
});
|
||||
219
apps/shared/components/filter/src/lib/tree/filter.ts
Normal file
219
apps/shared/components/filter/src/lib/tree/filter.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { QueryTokenDTO } from '@swagger/oms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { IInputGroup, InputGroup } from './input-group';
|
||||
import { InputType } from './input-type.enum';
|
||||
import { IOrderBy, OrderBy } from './order-by';
|
||||
|
||||
const ALLOWED_SUB_OPTIONS = [InputType.Bool, InputType.TriState];
|
||||
|
||||
export interface IFilter {
|
||||
filter?: Array<IInputGroup>;
|
||||
input?: Array<IInputGroup>;
|
||||
orderBy?: Array<IOrderBy>;
|
||||
}
|
||||
|
||||
export class Filter implements IFilter {
|
||||
//#region implements IUiFilterQuerySettingsDTO
|
||||
private _filter?: Array<InputGroup>;
|
||||
get filter() {
|
||||
return this._filter;
|
||||
}
|
||||
|
||||
private _input?: Array<InputGroup>;
|
||||
get input() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private _orderBy?: Array<OrderBy>;
|
||||
get orderBy() {
|
||||
return this._orderBy;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IFilter)[]; orderBy: Filter }>();
|
||||
|
||||
getQueryToken(): QueryTokenDTO {
|
||||
const token: QueryTokenDTO = {
|
||||
input: this.getInputAsStringDictionary(),
|
||||
filter: this.getFilterAsStringDictionary(),
|
||||
orderBy: this.orderBy?.filter((ob) => ob.selected).map((ob) => ob.toObject()),
|
||||
};
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
getQueryParams(): Record<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
this.input?.forEach((inputGroup) => {
|
||||
const group = inputGroup.group;
|
||||
inputGroup.input.forEach((input) => {
|
||||
const key = input.key;
|
||||
params[`${group}_${key}`] = input.toStringValue();
|
||||
});
|
||||
});
|
||||
|
||||
this.filter?.forEach((inputGroup) => {
|
||||
const group = inputGroup.group;
|
||||
inputGroup.input.forEach((input) => {
|
||||
const key = input.key;
|
||||
|
||||
const values = inputGroup.input
|
||||
?.filter((i) => i.key === key)
|
||||
?.map((i) => i.toStringValue())
|
||||
?.filter((i) => !!i)
|
||||
?.join(';');
|
||||
|
||||
params[`${group}_${key}`] = values;
|
||||
});
|
||||
});
|
||||
|
||||
this.orderBy
|
||||
?.filter((ob) => ob.selected)
|
||||
.forEach((ob) => {
|
||||
params[`order_by_${ob.by}`] = ob?.desc ? 'desc' : 'asc';
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
fromQueryParams(params: Record<string, string>) {
|
||||
const groupKeys = Object.keys(params);
|
||||
|
||||
groupKeys.forEach((gk) => {
|
||||
const group = gk.split('_')[0];
|
||||
const key = gk.substr(group.length + 1);
|
||||
|
||||
this.input
|
||||
?.filter((ig) => ig.group === group)
|
||||
.forEach((inputGroup) => {
|
||||
inputGroup.input
|
||||
.filter((i) => i.key === key)
|
||||
.forEach((input) => {
|
||||
input.fromStringValue(key, params[gk]);
|
||||
});
|
||||
});
|
||||
|
||||
this.filter
|
||||
?.filter((ig) => ig.group === group)
|
||||
.forEach((inputGroup) => {
|
||||
inputGroup.input
|
||||
.filter((i) => i.key === key)
|
||||
.forEach((input, index) => {
|
||||
let value = params[gk];
|
||||
|
||||
if (input.type === InputType.Text && value?.includes(';')) {
|
||||
const splitted = value?.split(';');
|
||||
if (splitted?.length >= index) {
|
||||
value = splitted[index];
|
||||
}
|
||||
}
|
||||
input.fromStringValue(key, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.orderBy?.forEach((ob) => {
|
||||
const order = params[`order_by_${ob.by}`];
|
||||
const selected = (order && ob.desc && order === 'desc') || (!ob.desc && order === 'asc');
|
||||
ob.setSelected(selected);
|
||||
});
|
||||
}
|
||||
|
||||
getInputAsStringDictionary(): Record<string, string> {
|
||||
const input: Record<string, string> = {};
|
||||
|
||||
this.input?.forEach((ig) => {
|
||||
const group = ig.group;
|
||||
const inputWithValue = ig.input.find((i) => i.toStringValue());
|
||||
if (!inputWithValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputsWithTheSameGroup = this.filter?.find((f) => f.group === group);
|
||||
const inputsWithTargetInput = inputsWithTheSameGroup?.input?.filter((ig) => ig.selected && ig.target === 'input');
|
||||
|
||||
if (inputsWithTargetInput?.length) {
|
||||
inputsWithTargetInput.forEach((ifk) => {
|
||||
input[ifk.key] = inputWithValue.toStringValue();
|
||||
});
|
||||
} else {
|
||||
input[inputWithValue.key] = inputWithValue.value;
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
getFilterAsStringDictionary(): Record<string, string> {
|
||||
const filter: Record<string, string> = {};
|
||||
for (const inputGroup of this.filter) {
|
||||
for (const input of inputGroup.input.filter((i) => i.target === 'filter')) {
|
||||
const subKeys = input.subKeys;
|
||||
let value = undefined;
|
||||
let key = undefined;
|
||||
|
||||
if (subKeys.length === 0 || !ALLOWED_SUB_OPTIONS.includes(input.type)) {
|
||||
key = input.key;
|
||||
value = inputGroup.input
|
||||
?.filter((i) => i.key === key)
|
||||
?.map((i) => i.toStringValue())
|
||||
?.filter((i) => !!i)
|
||||
?.join(';');
|
||||
if (!!value) {
|
||||
filter[key] = value;
|
||||
}
|
||||
} else {
|
||||
for (let subKey of subKeys) {
|
||||
key = `${input.key}.${subKey}`;
|
||||
const options = input.options.values.filter((value) => value.key === subKey);
|
||||
value = options
|
||||
?.map((i) => i.toStringValue())
|
||||
?.filter((i) => !!i)
|
||||
?.join(';');
|
||||
if (!!value) {
|
||||
filter[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// getKeyValue():
|
||||
|
||||
toObject(): IFilter {
|
||||
return {
|
||||
filter: this.filter?.map((filter) => filter.toObject()),
|
||||
input: this.input?.map((input) => input.toObject()),
|
||||
orderBy: this.orderBy?.map((orderBy) => orderBy.toObject()),
|
||||
};
|
||||
}
|
||||
|
||||
static create(settings: IFilter) {
|
||||
const target = new Filter();
|
||||
|
||||
target._filter = settings?.filter?.map((filter) => InputGroup.create(filter, target)) || [];
|
||||
target._input = settings?.input?.map((input) => InputGroup.create(input, target)) || [];
|
||||
target._orderBy = settings?.orderBy?.map((orderBy) => OrderBy.create(orderBy, target)) || [];
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
static getQueryParamsFromQueryTokenDTO(queryToken: QueryTokenDTO): Record<string, string> {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (queryToken.input?.qs) {
|
||||
queryParams['main_qs'] = queryToken.input.qs;
|
||||
}
|
||||
|
||||
if (!!queryToken.filter) {
|
||||
for (const key in queryToken.filter) {
|
||||
queryParams[`filter_${key}`] = queryToken.filter[key];
|
||||
}
|
||||
}
|
||||
|
||||
return queryParams;
|
||||
}
|
||||
}
|
||||
7
apps/shared/components/filter/src/lib/tree/index.ts
Normal file
7
apps/shared/components/filter/src/lib/tree/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './filter';
|
||||
export * from './input-group';
|
||||
export * from './input-options';
|
||||
export * from './input-type.enum';
|
||||
export * from './input';
|
||||
export * from './option';
|
||||
export * from './order-by';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FilterInput } from './input';
|
||||
import { IInputGroup, InputGroup } from './input-group';
|
||||
|
||||
describe('InputGroup', () => {
|
||||
const testData: IInputGroup = {
|
||||
description: 'test description',
|
||||
group: 'main',
|
||||
focused: true,
|
||||
input: [
|
||||
{ key: 'all_branches', label: 'Alle Filialen', type: 2, target: 'filter' },
|
||||
{ key: 'customer_name', label: 'Kundenname', type: 2, target: 'input' },
|
||||
{ key: 'product_contributor', label: 'Autor', type: 2, target: 'input' },
|
||||
{ key: 'product_name', label: 'Titel', type: 2, target: 'input' },
|
||||
],
|
||||
};
|
||||
|
||||
let inputGroup: InputGroup;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(FilterInput, 'create').and.callThrough();
|
||||
inputGroup = InputGroup.create(testData);
|
||||
});
|
||||
|
||||
describe('static create(inputGroup: IUiInputGroup)', () => {
|
||||
it('should create and return an instance of UiInputGroup', () => {
|
||||
expect(inputGroup instanceof InputGroup).toBe(true);
|
||||
expect(inputGroup.description).toEqual(testData.description);
|
||||
expect(inputGroup.group).toEqual(testData.group);
|
||||
|
||||
testData.input.forEach((input) => expect(FilterInput.create).toHaveBeenCalledWith(input, inputGroup));
|
||||
|
||||
expect(inputGroup.label).toEqual(testData.label);
|
||||
});
|
||||
});
|
||||
});
|
||||
60
apps/shared/components/filter/src/lib/tree/input-group.ts
Normal file
60
apps/shared/components/filter/src/lib/tree/input-group.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { Filter } from './filter';
|
||||
import { IInput, FilterInput } from './input';
|
||||
|
||||
export interface IInputGroup {
|
||||
readonly description?: string;
|
||||
readonly group?: string;
|
||||
readonly input?: Array<IInput>;
|
||||
readonly label?: string;
|
||||
readonly focused?: boolean;
|
||||
}
|
||||
|
||||
export class InputGroup implements IInputGroup {
|
||||
//#region implements IUiFilterInputGroup
|
||||
private _description?: string;
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
private _group?: string;
|
||||
get group() {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
private _input?: Array<FilterInput>;
|
||||
get input() {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private _label?: string;
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IInputGroup)[]; target: InputGroup }>();
|
||||
|
||||
constructor(public readonly parent?: Filter) {}
|
||||
|
||||
toObject(): IInputGroup {
|
||||
return {
|
||||
description: this.description,
|
||||
group: this.group,
|
||||
input: this.input?.map((input) => input.toObject()),
|
||||
label: this.label,
|
||||
};
|
||||
}
|
||||
|
||||
static create(inputGroup: IInputGroup, parent?: Filter) {
|
||||
const target = new InputGroup(parent);
|
||||
|
||||
target._description = inputGroup?.description;
|
||||
target._group = inputGroup?.group;
|
||||
target._input = inputGroup?.input?.map((input) => FilterInput.create(input, target));
|
||||
target._label = inputGroup?.label;
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IInputOptions, InputOptions } from './input-options';
|
||||
import { Option } from './option';
|
||||
|
||||
describe('InputOptions', () => {
|
||||
const testData: IInputOptions = {
|
||||
max: 2,
|
||||
values: [
|
||||
{ label: 'abgeholt', value: '256' },
|
||||
{ label: 'angefragt', value: '524288' },
|
||||
{ label: 'ans Lager (nicht abgeholt)', value: '262144' },
|
||||
{ label: 'bestellt', value: '16', selected: true },
|
||||
],
|
||||
};
|
||||
|
||||
let inputOptions: InputOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Option, 'create');
|
||||
inputOptions = InputOptions.create(testData);
|
||||
});
|
||||
|
||||
describe('static create(inputOptions: IUiInputOptions)', () => {
|
||||
it('should return an instance of IUiInputOptions', () => {
|
||||
expect(inputOptions instanceof InputOptions).toBe(true);
|
||||
expect(inputOptions.max).toBe(testData.max);
|
||||
testData.values.forEach((value) => expect(Option.create).toHaveBeenCalledWith(value, inputOptions));
|
||||
});
|
||||
});
|
||||
});
|
||||
50
apps/shared/components/filter/src/lib/tree/input-options.ts
Normal file
50
apps/shared/components/filter/src/lib/tree/input-options.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { FilterInput } from './input';
|
||||
import { IOption, Option } from './option';
|
||||
|
||||
export interface IInputOptions {
|
||||
readonly max?: number;
|
||||
readonly values?: Array<IOption>;
|
||||
}
|
||||
|
||||
export class InputOptions implements IInputOptions {
|
||||
//#region implements IUiFilterInputOptions
|
||||
private _max?: number;
|
||||
get max() {
|
||||
return this._max;
|
||||
}
|
||||
|
||||
private _values?: Array<Option>;
|
||||
get values() {
|
||||
return this._values;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IInputOptions)[]; inputOptions: InputOptions }>();
|
||||
|
||||
constructor(public readonly parent?: FilterInput) {}
|
||||
|
||||
getSelectedOptions(): Option[] {
|
||||
return this.values?.map((o) => o.getSelectedOptions()).reduce((agg, options) => [...agg, ...options], []) || [];
|
||||
}
|
||||
|
||||
getUnselectedOptions(): Option[] {
|
||||
return this.values?.map((o) => o.getUnselectedOptions()).reduce((agg, options) => [...agg, ...options], []) || [];
|
||||
}
|
||||
|
||||
toObject(): IInputOptions {
|
||||
return {
|
||||
max: this.max,
|
||||
values: this.values?.map((value) => value.toObject()),
|
||||
};
|
||||
}
|
||||
|
||||
static create(inputOptions: IInputOptions, parent?: FilterInput) {
|
||||
const target = new InputOptions(parent);
|
||||
|
||||
target._max = inputOptions?.max;
|
||||
target._values = inputOptions?.values?.map((value) => Option.create(value, target));
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export enum InputType {
|
||||
NotSet = 0,
|
||||
Text = 1,
|
||||
Bool = 2,
|
||||
TriState = 4,
|
||||
InputSelector = 8,
|
||||
Date = 16,
|
||||
DateTime = 32,
|
||||
Time = 64,
|
||||
DateRange = 128,
|
||||
DateTimeRange = 256,
|
||||
TimeRange = 512,
|
||||
Integer = 1024,
|
||||
Decimal = 2048,
|
||||
Number = 3072,
|
||||
IntegerRange = 4096,
|
||||
DecimalRange = 8192,
|
||||
NumberRange = 12288,
|
||||
}
|
||||
287
apps/shared/components/filter/src/lib/tree/input.ts
Normal file
287
apps/shared/components/filter/src/lib/tree/input.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { InputGroup } from './input-group';
|
||||
import { IInputOptions, InputOptions } from './input-options';
|
||||
import { InputType } from './input-type.enum';
|
||||
|
||||
export interface IInput {
|
||||
readonly constraint?: string;
|
||||
readonly description?: string;
|
||||
readonly key?: string;
|
||||
readonly label?: string;
|
||||
readonly maxValue?: string;
|
||||
readonly minValue?: string;
|
||||
readonly options?: IInputOptions;
|
||||
readonly placeholder?: string;
|
||||
readonly target?: string;
|
||||
readonly type: InputType;
|
||||
readonly value?: string;
|
||||
readonly selected?: boolean;
|
||||
}
|
||||
|
||||
export class FilterInput implements IInput {
|
||||
//#region implements IUiFilterInputDTO
|
||||
private _constraint?: string;
|
||||
get constraint() {
|
||||
return this._constraint;
|
||||
}
|
||||
|
||||
private _description?: string;
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
private _key?: string;
|
||||
get key() {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
get subKeys() {
|
||||
return this._options?.values?.filter((value) => !!value.key).map((value) => value.key) ?? [];
|
||||
}
|
||||
|
||||
private _label?: string;
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
private _maxValue?: string;
|
||||
get maxValue() {
|
||||
return this._maxValue;
|
||||
}
|
||||
|
||||
private _minValue?: string;
|
||||
get minValue() {
|
||||
return this._minValue;
|
||||
}
|
||||
|
||||
private _options?: InputOptions;
|
||||
get options() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
private _placeholder?: string;
|
||||
get placeholder() {
|
||||
return this._placeholder;
|
||||
}
|
||||
|
||||
private _target?: string;
|
||||
get target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
private _type = InputType.NotSet;
|
||||
get type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
private _value?: string;
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
private _selected?: boolean;
|
||||
get selected() {
|
||||
if (this.hasOptions()) {
|
||||
return this.options?.values.filter((s) => s.selected)?.length === this.options?.values?.length;
|
||||
}
|
||||
|
||||
return this._selected;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IInput)[]; target: FilterInput }>();
|
||||
|
||||
private _errors: Record<string, string> | null;
|
||||
get errors() {
|
||||
return this._errors;
|
||||
}
|
||||
|
||||
constructor(public readonly parent?: InputGroup) {}
|
||||
|
||||
toStringValue() {
|
||||
switch (this.type) {
|
||||
case InputType.Bool:
|
||||
case InputType.InputSelector:
|
||||
return this.boolToStringValue();
|
||||
case InputType.Text:
|
||||
case InputType.Integer:
|
||||
return this.textToStringValue();
|
||||
case InputType.DateRange:
|
||||
return this.dateRangeToStringValue();
|
||||
case InputType.IntegerRange:
|
||||
case InputType.NumberRange:
|
||||
return this.rangeToStringValue();
|
||||
case InputType.TriState:
|
||||
return this.triStateToString();
|
||||
}
|
||||
}
|
||||
|
||||
fromStringValue(key: string, value: string) {
|
||||
if (this.key === key) {
|
||||
if (this.type === InputType.Text || this.type === InputType.Integer) {
|
||||
this.setValue(value);
|
||||
} else {
|
||||
if (this.options) {
|
||||
this.options?.values?.forEach((option) => {
|
||||
option.trySetFromValue(value);
|
||||
});
|
||||
} else {
|
||||
this.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolToStringValue() {
|
||||
if (this.hasOptions()) {
|
||||
return this.getSelectedOptions()
|
||||
.map((options) => options.value ?? options.key)
|
||||
.join(';');
|
||||
}
|
||||
return this.selected ? String(this.selected) : undefined;
|
||||
}
|
||||
|
||||
private dateRangeToStringValue() {
|
||||
if (this.hasOptions()) {
|
||||
const selected = this.getSelectedOptions();
|
||||
|
||||
if (selected?.length) {
|
||||
const range = selected.reduce((res, option) => {
|
||||
const cp: Record<string, string> = { ...res };
|
||||
cp[option.key] = option.value;
|
||||
return cp;
|
||||
}, {});
|
||||
|
||||
if (range['start'] && range['stop']) {
|
||||
return `"${range['start']}"-"${range['stop']}"`;
|
||||
} else if (range['start']) {
|
||||
return `"${range['start']}"-`;
|
||||
} else if (range['stop']) {
|
||||
return `-"${range['stop']}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.selected ? this.value : undefined;
|
||||
}
|
||||
|
||||
private rangeToStringValue() {
|
||||
if (this.hasOptions()) {
|
||||
const selected = this.getSelectedOptions();
|
||||
|
||||
if (selected?.length) {
|
||||
const range = selected.reduce((res, option) => {
|
||||
const cp: Record<string, string> = { ...res };
|
||||
cp[option.key] = option.value;
|
||||
return cp;
|
||||
}, {});
|
||||
|
||||
if (range['start'] && range['stop']) {
|
||||
return `${range['start']}-${range['stop']}`;
|
||||
} else if (range['start']) {
|
||||
return `${range['start']}-`;
|
||||
} else if (range['stop']) {
|
||||
return `-${range['stop']}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.selected ? this.value : undefined;
|
||||
}
|
||||
|
||||
private textToStringValue() {
|
||||
if (this.hasOptions()) {
|
||||
return this.getSelectedOptions()
|
||||
.map((v) => v.value)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
return this.selected ? String(this.value) : undefined;
|
||||
}
|
||||
|
||||
private triStateToString() {
|
||||
if (this.hasOptions()) {
|
||||
const selected = this.getSelectedOptions()?.map((options) => options.value ?? options.key);
|
||||
const unselected = this.getUnselectedOptions()?.map((options) => `!${options.value ?? options.key}`);
|
||||
return [...selected, ...unselected].join(';');
|
||||
}
|
||||
}
|
||||
|
||||
hasOptions() {
|
||||
return !!this.options?.values?.length || false;
|
||||
}
|
||||
|
||||
getSelectedOptions() {
|
||||
return this.options?.getSelectedOptions() || [];
|
||||
}
|
||||
|
||||
getUnselectedOptions() {
|
||||
return this.options?.getUnselectedOptions() || [];
|
||||
}
|
||||
|
||||
hasSelectedOptions() {
|
||||
return this.getSelectedOptions().length > 0;
|
||||
}
|
||||
|
||||
hasUnselectedOptions() {
|
||||
return this.getUnselectedOptions().length > 0;
|
||||
}
|
||||
|
||||
setValue(value: string, { emitEvent } = { emitEvent: true }) {
|
||||
if (this.value !== value) {
|
||||
this._value = value;
|
||||
|
||||
this.setSelected(!!value, { emitEvent: false });
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['value', 'selected'], target: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(value: boolean, { emitEvent } = { emitEvent: true }) {
|
||||
if (this.selected !== value) {
|
||||
this._selected = value;
|
||||
|
||||
this.options?.values?.forEach((f) => f.setSelected(value, { emitEvent }));
|
||||
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['selected'], target: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toObject(): IInput {
|
||||
return {
|
||||
type: this.type,
|
||||
constraint: this.constraint,
|
||||
description: this.description,
|
||||
key: this.key,
|
||||
label: this.label,
|
||||
maxValue: this.maxValue,
|
||||
minValue: this.minValue,
|
||||
options: this.options?.toObject(),
|
||||
placeholder: this.placeholder,
|
||||
selected: this.selected,
|
||||
target: this.target,
|
||||
value: this.value,
|
||||
};
|
||||
}
|
||||
|
||||
static create(input: IInput, parent?: InputGroup) {
|
||||
const target = new FilterInput(parent);
|
||||
|
||||
target._constraint = input?.constraint;
|
||||
target._description = input?.description;
|
||||
target._key = input?.key;
|
||||
target._label = input?.label;
|
||||
target._maxValue = input?.maxValue;
|
||||
target._minValue = input?.minValue;
|
||||
target._options = input?.options ? InputOptions.create(input.options, target) : undefined;
|
||||
target._placeholder = input?.placeholder;
|
||||
target._target = input?.target;
|
||||
target._type = input?.type;
|
||||
target._value = input?.value;
|
||||
target._selected = input?.selected;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
51
apps/shared/components/filter/src/lib/tree/option.spec.ts
Normal file
51
apps/shared/components/filter/src/lib/tree/option.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { IOption, Option } from './option';
|
||||
|
||||
describe('Option', () => {
|
||||
describe('static create(inputOptions: IUiOptionDTO)', () => {
|
||||
const testData: IOption = {
|
||||
description: 'description',
|
||||
enabled: false,
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
maxValue: '2',
|
||||
minValue: '1',
|
||||
placeholder: 'placeholder',
|
||||
selected: true,
|
||||
value: 'value',
|
||||
values: [
|
||||
{
|
||||
description: 'sub description',
|
||||
enabled: true,
|
||||
key: 'sub key',
|
||||
label: 'sub label',
|
||||
maxValue: 'sub 2',
|
||||
minValue: 'sub 1',
|
||||
placeholder: 'sub placeholder',
|
||||
selected: false,
|
||||
value: 'sub value',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let uiOption: Option;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Option, 'create').and.callThrough();
|
||||
uiOption = Option.create(testData);
|
||||
});
|
||||
|
||||
it('should return an instance of UiInputDTO', () => {
|
||||
expect(uiOption instanceof Option).toBe(true);
|
||||
expect(uiOption.description).toBe(testData.description);
|
||||
expect(uiOption.enabled).toBe(testData.enabled);
|
||||
expect(uiOption.key).toBe(testData.key);
|
||||
expect(uiOption.label).toBe(testData.label);
|
||||
expect(uiOption.maxValue).toBe(testData.maxValue);
|
||||
expect(uiOption.minValue).toBe(testData.minValue);
|
||||
expect(uiOption.placeholder).toBe(testData.placeholder);
|
||||
expect(uiOption.value).toBe(testData.value);
|
||||
|
||||
testData.values.forEach((value) => expect(Option.create).toHaveBeenCalledWith(value, uiOption));
|
||||
});
|
||||
});
|
||||
});
|
||||
344
apps/shared/components/filter/src/lib/tree/option.ts
Normal file
344
apps/shared/components/filter/src/lib/tree/option.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { InputOptions } from './input-options';
|
||||
import { InputType } from './input-type.enum';
|
||||
|
||||
export interface IOption {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
key?: string;
|
||||
label?: string;
|
||||
maxValue?: string;
|
||||
minValue?: string;
|
||||
placeholder?: string;
|
||||
selected?: boolean;
|
||||
value?: string;
|
||||
values?: Array<IOption>;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export class Option implements IOption {
|
||||
//#region implements IUiFilterOptionDTO
|
||||
private _description?: string;
|
||||
get description() {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
private _enabled?: boolean;
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
private _key?: string;
|
||||
get key() {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
private _label?: string;
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
private _maxValue?: string;
|
||||
get maxValue() {
|
||||
return this._maxValue;
|
||||
}
|
||||
|
||||
private _minValue?: string;
|
||||
get minValue() {
|
||||
return this._minValue;
|
||||
}
|
||||
|
||||
private _placeholder?: string;
|
||||
get placeholder() {
|
||||
return this._placeholder;
|
||||
}
|
||||
|
||||
private _selected?: boolean;
|
||||
get selected() {
|
||||
if (this.values?.length) {
|
||||
return this.values.filter((f) => f.selected).length === this.values.length;
|
||||
}
|
||||
|
||||
if (this.getParentInput()?.type === InputType.DateRange) {
|
||||
return !!this.value;
|
||||
}
|
||||
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
private _value?: string;
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
private _values?: Array<Option>;
|
||||
get values() {
|
||||
return this._values;
|
||||
}
|
||||
|
||||
private _expanded?: boolean;
|
||||
get expanded() {
|
||||
return this._expanded;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IOption)[]; target: Option }>();
|
||||
|
||||
constructor(public readonly parent?: Option | InputOptions) {}
|
||||
|
||||
getParentInput() {
|
||||
return this.getParentInputOptions()?.parent;
|
||||
}
|
||||
|
||||
getParentInputOptions() {
|
||||
let parent = this.parent;
|
||||
while (!!parent) {
|
||||
if (parent instanceof InputOptions) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return !!this.values?.length;
|
||||
}
|
||||
|
||||
getSelectedOptions(includeSelf: boolean = true): Option[] {
|
||||
const selected: Option[] = [];
|
||||
|
||||
if (this.selected && includeSelf) {
|
||||
selected.push(this);
|
||||
}
|
||||
|
||||
const selectedChildren = this.values?.map((f) => f.getSelectedOptions()).reduce((agg, options) => [...agg, ...options], []);
|
||||
|
||||
if (selectedChildren?.length) {
|
||||
selected.push(...selectedChildren);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
getUnselectedOptions(): Option[] {
|
||||
if (this?.getParentInput()?.type === InputType.TriState && this.selected === false) {
|
||||
return [this];
|
||||
} else if (this.hasChildren()) {
|
||||
return this.values.map((f) => f.getUnselectedOptions()).reduce((agg, options) => [...agg, ...options], []);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
setSelected(value: boolean, { emitEvent } = { emitEvent: true }) {
|
||||
this._selected = value;
|
||||
|
||||
const type = this.getParentInput()?.type;
|
||||
if (!value && (type === InputType.DateRange || type === InputType.NumberRange || type === InputType.IntegerRange)) {
|
||||
this.setValue(undefined, { emitEvent: false });
|
||||
}
|
||||
|
||||
this.values?.forEach((option) => option.setSelected(value));
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['selected', 'value'], target: this });
|
||||
}
|
||||
}
|
||||
|
||||
setExpanded(value: boolean, { emitEvent } = { emitEvent: true }) {
|
||||
if (this.expanded !== value) {
|
||||
this._expanded = value;
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['expanded'], target: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: string | Date, { emitEvent } = { emitEvent: true }) {
|
||||
if (value instanceof Date) {
|
||||
value = value?.toJSON();
|
||||
}
|
||||
|
||||
if (this.value !== value) {
|
||||
this._value = value;
|
||||
|
||||
this.setSelected(!!this.value, { emitEvent: false });
|
||||
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['value', 'selected'], target: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toStringValue() {
|
||||
switch (this.getParentInput()?.type) {
|
||||
case InputType.Bool:
|
||||
case InputType.InputSelector:
|
||||
return this.boolToStringValue();
|
||||
}
|
||||
}
|
||||
|
||||
hasOptions() {
|
||||
return this._values?.length > 0 ?? false;
|
||||
}
|
||||
|
||||
private boolToStringValue() {
|
||||
if (this.hasOptions()) {
|
||||
return this.getSelectedOptions(false)
|
||||
.map((options) => options.value ?? options.key)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
return this.selected ? String(this.value ?? this.key) : undefined;
|
||||
}
|
||||
|
||||
trySetFromValue(value: string) {
|
||||
const type = this.getParentInput()?.type;
|
||||
|
||||
switch (type) {
|
||||
case InputType.Bool:
|
||||
case InputType.InputSelector:
|
||||
this.trySetBoolFromValue(value);
|
||||
break;
|
||||
case InputType.TriState:
|
||||
this.trySetTriStateFromValue(value);
|
||||
break;
|
||||
case InputType.NumberRange:
|
||||
case InputType.IntegerRange:
|
||||
this.trySetFromNumberRange(value);
|
||||
break;
|
||||
case InputType.DateRange:
|
||||
this.trySetFromDateRange(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
trySetFromNumberRange(value: string) {
|
||||
const split = value?.split('-');
|
||||
if (this.key === 'start') {
|
||||
this.setValue(split[0]);
|
||||
} else if (this.key === 'stop') {
|
||||
this.setValue(split[1]);
|
||||
}
|
||||
}
|
||||
|
||||
trySetFromDateRange(value: string) {
|
||||
let split: string[] = [];
|
||||
|
||||
if (value) {
|
||||
if (value?.includes('"-"')) {
|
||||
split = value.split('"-"');
|
||||
} else if (value?.includes('-"')) {
|
||||
split = value.split('-"');
|
||||
} else if (value?.includes('"-')) {
|
||||
split = value.split('"-');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.key === 'start') {
|
||||
if (split[0]) {
|
||||
split[0] = split[0].replace(new RegExp('"', 'g'), '');
|
||||
}
|
||||
this.setValue(split[0]);
|
||||
} else if (this.key === 'stop') {
|
||||
if (split[1]) {
|
||||
split[1] = split[1].replace(new RegExp('"', 'g'), '');
|
||||
}
|
||||
this.setValue(split[1]);
|
||||
}
|
||||
}
|
||||
|
||||
trySetBoolFromValue(value: string) {
|
||||
const valueSplits = value?.split(';');
|
||||
if (valueSplits.includes(this.value)) {
|
||||
this.setSelected(true);
|
||||
} else {
|
||||
this.setSelected(undefined);
|
||||
this.values?.forEach((option) => option?.trySetBoolFromValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
trySetTriStateFromValue(value: string) {
|
||||
const valueSplits = value?.split(';');
|
||||
if (valueSplits.includes(this.value)) {
|
||||
this.setSelected(true);
|
||||
} else if (valueSplits.includes(`!${this.value}`)) {
|
||||
this.setSelected(false);
|
||||
} else {
|
||||
this.setSelected(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
validate(): string | null {
|
||||
const input = this.getParentInput();
|
||||
|
||||
switch (input?.type) {
|
||||
// NumberRange
|
||||
case InputType.NumberRange:
|
||||
case InputType.IntegerRange:
|
||||
return this.validateNumber() || this.validateNumberRange();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
validateNumber(): string | null {
|
||||
if (!Number.isNaN(+this.value)) {
|
||||
if (!Number.isNaN(+this.minValue) && +this.value < +this.minValue) {
|
||||
return `Der Wert darf nicht kleiner als ${this.minValue} sein.`;
|
||||
} else if (!Number.isNaN(+this.maxValue) && +this.value > +this.maxValue) {
|
||||
return `Der Wert darf nicht größer als ${this.maxValue} sein.`;
|
||||
} else if (!!this.value && !/^[0-9]+$/.exec(this.value)) {
|
||||
return `Es werden nur ganzzahlige Werte akzeptiert.`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
validateNumberRange(): string | null {
|
||||
if (!Number.isNaN(+this.value)) {
|
||||
const start = this.parent.values.find((v) => v.key === 'start');
|
||||
const stop = this.parent.values.find((v) => v.key === 'stop');
|
||||
|
||||
if (!Number.isNaN(+start.value) && !Number.isNaN(+stop.value) && +start.value > +stop.value) {
|
||||
return `Der Wert "${start.value}" darf nicht gößer sein als "${stop.value}"`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
toObject(): IOption {
|
||||
return {
|
||||
description: this.description,
|
||||
enabled: this.enabled,
|
||||
expanded: this.expanded,
|
||||
key: this.key,
|
||||
label: this.label,
|
||||
maxValue: this.maxValue,
|
||||
minValue: this.minValue,
|
||||
placeholder: this.placeholder,
|
||||
selected: this.selected,
|
||||
value: this.value,
|
||||
values: this.values?.map((value) => value.toObject()),
|
||||
};
|
||||
}
|
||||
|
||||
static create(option: IOption, parent?: Option | InputOptions) {
|
||||
const target = new Option(parent);
|
||||
|
||||
target._description = option?.description;
|
||||
target._enabled = option?.enabled;
|
||||
target._key = option?.key;
|
||||
target._label = option?.label;
|
||||
target._maxValue = option?.maxValue;
|
||||
target._minValue = option?.minValue;
|
||||
target._placeholder = option?.placeholder;
|
||||
target._selected = option?.selected;
|
||||
target._value = option?.value;
|
||||
target._values = option?.values?.map((value) => Option.create(value, target));
|
||||
target._expanded = option?.expanded;
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
24
apps/shared/components/filter/src/lib/tree/order-by.spec.ts
Normal file
24
apps/shared/components/filter/src/lib/tree/order-by.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IOrderBy, OrderBy } from './order-by';
|
||||
|
||||
describe('OrderBy', () => {
|
||||
const testData: IOrderBy = {
|
||||
by: 'by',
|
||||
desc: true,
|
||||
label: 'label',
|
||||
};
|
||||
|
||||
let orderBy: OrderBy;
|
||||
|
||||
beforeEach(() => {
|
||||
orderBy = OrderBy.create(testData);
|
||||
});
|
||||
|
||||
describe('static create(inputOptions: IUiOrderByDTO)', () => {
|
||||
it('should return an instance of UiInputDTO', () => {
|
||||
expect(orderBy instanceof OrderBy).toBe(true);
|
||||
expect(orderBy.by).toEqual(testData.by);
|
||||
expect(orderBy.desc).toEqual(testData.desc);
|
||||
expect(orderBy.label).toEqual(testData.label);
|
||||
});
|
||||
});
|
||||
});
|
||||
68
apps/shared/components/filter/src/lib/tree/order-by.ts
Normal file
68
apps/shared/components/filter/src/lib/tree/order-by.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Subject } from 'rxjs';
|
||||
import { Filter } from './filter';
|
||||
|
||||
export interface IOrderBy {
|
||||
by?: string;
|
||||
desc?: boolean;
|
||||
label?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export class OrderBy implements IOrderBy {
|
||||
//#region implements IUiFilterOrderByDTO
|
||||
private _by?: string;
|
||||
get by() {
|
||||
return this._by;
|
||||
}
|
||||
|
||||
private _desc?: boolean;
|
||||
get desc() {
|
||||
return this._desc;
|
||||
}
|
||||
|
||||
private _label?: string;
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
private _selected?: boolean;
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
constructor(public readonly parent?: Filter) {}
|
||||
|
||||
readonly changes = new Subject<{ keys: (keyof IOrderBy)[]; orderBy: OrderBy }>();
|
||||
|
||||
setSelected(value: boolean, { emitEvent }: { emitEvent: boolean } = { emitEvent: true }) {
|
||||
if (this.selected !== value) {
|
||||
this._selected = value;
|
||||
|
||||
if (emitEvent) {
|
||||
this.changes.next({ keys: ['selected'], orderBy: this });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toObject(): IOrderBy {
|
||||
return {
|
||||
by: this.by,
|
||||
desc: this.desc,
|
||||
label: this.label,
|
||||
selected: this.selected,
|
||||
};
|
||||
}
|
||||
|
||||
static create(orderBy: IOrderBy, parent?: Filter) {
|
||||
const target = new OrderBy(parent);
|
||||
|
||||
target._by = orderBy?.by;
|
||||
target._desc = orderBy?.desc;
|
||||
target._label = orderBy?.label;
|
||||
target._selected = orderBy?.selected;
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
0
apps/shared/components/filter/src/public-api.ts
Normal file
0
apps/shared/components/filter/src/public-api.ts
Normal file
6
apps/shared/components/searchbox/ng-package.json
Normal file
6
apps/shared/components/searchbox/ng-package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="searchbox-input-wrapper">
|
||||
<input
|
||||
class="searchbox-input"
|
||||
#input
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[(ngModel)]="query"
|
||||
(ngModelChange)="setQuery($event, true, true)"
|
||||
(focus)="clearHint(); focused.emit(true)"
|
||||
(blur)="focused.emit(false)"
|
||||
(keyup)="onKeyup($event)"
|
||||
/>
|
||||
<button tabindex="-1" *ngIf="input.value" class="searchbox-clear-btn" type="button">
|
||||
<ui-icon icon="close" size="1rem"></ui-icon>
|
||||
</button>
|
||||
<ng-container *ngIf="!loading">
|
||||
<button
|
||||
tabindex="0"
|
||||
class="searchbox-search-btn"
|
||||
type="button"
|
||||
*ngIf="!canScan"
|
||||
(click)="emitSearch()"
|
||||
[disabled]="completeValue !== query"
|
||||
>
|
||||
<ui-icon icon="search" size="1.5rem"></ui-icon>
|
||||
</button>
|
||||
<button tabindex="0" class="searchbox-scan-btn" type="button" *ngIf="canScan" (click)="startScan()">
|
||||
<ui-svg-icon icon="barcode-scan" [size]="32"></ui-svg-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div *ngIf="loading" class="searchbox-load-indicator">
|
||||
<ui-icon icon="spinner" size="32px"></ui-icon>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content select="ui-autocomplete"></ng-content>
|
||||
@@ -0,0 +1,25 @@
|
||||
:host {
|
||||
@apply inline-block bg-surface text-surface-content rounded-button;
|
||||
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
|
||||
}
|
||||
|
||||
.searchbox-input-wrapper {
|
||||
@apply flex flex-row h-14 items-stretch;
|
||||
}
|
||||
|
||||
.searchbox-input {
|
||||
@apply w-full bg-transparent outline-none pl-4 flex-shrink flex-grow text-lg caret-brand font-bold;
|
||||
}
|
||||
|
||||
.searchbox-search-btn,
|
||||
.searchbox-scan-btn {
|
||||
@apply w-14 h-14 bg-brand text-white rounded-button grid items-center justify-center flex-shrink-0 flex-grow-0;
|
||||
}
|
||||
|
||||
.searchbox-clear-btn {
|
||||
@apply w-14 h-14 bg-transparent text-black rounded-button grid items-center justify-center flex-shrink-0 flex-grow-0;
|
||||
}
|
||||
|
||||
.searchbox-load-indicator {
|
||||
@apply animate-spin w-14 h-14 grid items-center justify-center flex-shrink-0 flex-grow-0;
|
||||
}
|
||||
239
apps/shared/components/searchbox/src/lib/searchbox.component.ts
Normal file
239
apps/shared/components/searchbox/src/lib/searchbox.component.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
AfterViewInit,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
ContentChild,
|
||||
ChangeDetectorRef,
|
||||
HostBinding,
|
||||
AfterContentInit,
|
||||
HostListener,
|
||||
forwardRef,
|
||||
Optional,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SearchboxComponent), multi: true }],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchboxComponent extends UiFormControlDirective<any>
|
||||
implements AfterViewInit, OnDestroy, AfterContentInit, ControlValueAccessor {
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
autocomplete: UiAutocompleteComponent;
|
||||
|
||||
@Input()
|
||||
focusAfterViewInit: boolean = true;
|
||||
|
||||
@Input()
|
||||
placeholder: string = '';
|
||||
|
||||
private _query = '';
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
return this._query;
|
||||
}
|
||||
set query(value: string) {
|
||||
this._query = value;
|
||||
this.completeValue = value;
|
||||
}
|
||||
|
||||
@Output()
|
||||
queryChange = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
search = new EventEmitter<string>();
|
||||
|
||||
completeValue = this.query;
|
||||
@Output()
|
||||
complete = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
scan = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
loading = false;
|
||||
|
||||
@Input()
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint: string = '';
|
||||
|
||||
@Input()
|
||||
autocompleteValueSelector: (item: any) => string = (item: any) => item;
|
||||
|
||||
get valueEmpty(): boolean {
|
||||
return !!this.query;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
|
||||
get canScan() {
|
||||
return !this.query && this.scanner && this.scanAdapterService?.isReady();
|
||||
}
|
||||
|
||||
get canClear() {
|
||||
return !!this.query;
|
||||
}
|
||||
|
||||
get showHint() {
|
||||
return !!this.hint;
|
||||
}
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
onChange = (_: any) => {};
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(
|
||||
private cdr: ChangeDetectorRef,
|
||||
private elementRef: ElementRef<HTMLElement>,
|
||||
@Optional() private scanAdapterService: ScanAdapterService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.setQuery(obj, false);
|
||||
this.completeValue = obj;
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.focusAfterViewInit) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (this.autocomplete) {
|
||||
this.subscriptions.add(
|
||||
this.autocomplete?.selectItem.subscribe((item) => {
|
||||
this.setQuery(item);
|
||||
this.search.emit(item);
|
||||
this.autocomplete?.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
startScan() {
|
||||
if (!!this.scanAdapterService?.isReady()) {
|
||||
this.subscriptions.add(
|
||||
this.scanAdapterService.scan().subscribe((result) => {
|
||||
this.scan.emit(result);
|
||||
this.setQuery(result);
|
||||
this.autocomplete?.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setQuery(query: string, emitEvent: boolean = true, complete?: boolean) {
|
||||
this._query = query;
|
||||
if (emitEvent) {
|
||||
this.queryChange.emit(query);
|
||||
this.onChange(query);
|
||||
this.onTouched();
|
||||
}
|
||||
if (complete) {
|
||||
this.completeValue = query;
|
||||
this.complete.emit(query);
|
||||
}
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.input?.nativeElement?.focus();
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.focused.emit(true);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
}
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleArrowUpDownEvent(event: KeyboardEvent) {
|
||||
this.autocomplete?.handleKeyboardEvent(event);
|
||||
if (this.autocomplete?.activeItem) {
|
||||
const query = this.autocompleteValueSelector(this.autocomplete.activeItem.item);
|
||||
this.setQuery(query, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (this.autocomplete?.opend && !containsElement(this.elementRef.nativeElement, event.target as Element)) {
|
||||
this.autocomplete?.close();
|
||||
}
|
||||
}
|
||||
|
||||
emitSearch() {
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
13
apps/shared/components/searchbox/src/lib/searchbox.module.ts
Normal file
13
apps/shared/components/searchbox/src/lib/searchbox.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { SearchboxComponent } from './searchbox.component';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule, FormsModule],
|
||||
exports: [SearchboxComponent],
|
||||
declarations: [SearchboxComponent],
|
||||
})
|
||||
export class SearchboxModule {}
|
||||
3
apps/shared/components/searchbox/src/public-api.ts
Normal file
3
apps/shared/components/searchbox/src/public-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './lib/searchbox.component';
|
||||
export * from './lib/searchbox.module';
|
||||
export * from './lib/searchbox.component';
|
||||
Reference in New Issue
Block a user