commit
251d981b04
@ -0,0 +1,5 @@
|
||||
<div class="menu-anchor" [matMenuTriggerFor]="contextMenu" [style.left]="x" [style.top]="y"></div>
|
||||
<mat-menu #contextMenu="matMenu">
|
||||
<ng-content select="mat-menu-item"></ng-content>
|
||||
<ng-content ></ng-content>
|
||||
</mat-menu>
|
@ -0,0 +1,4 @@
|
||||
.menu-anchor {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ContextMenuComponent } from './context-menu.component';
|
||||
|
||||
describe('ContextMenuComponent', () => {
|
||||
let component: ContextMenuComponent;
|
||||
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ContextMenuComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ContextMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import {MatMenuTrigger} from "@angular/material/menu";
|
||||
|
||||
@Component({
|
||||
selector: 'app-context-menu',
|
||||
templateUrl: './context-menu.component.html',
|
||||
styleUrls: ['./context-menu.component.scss']
|
||||
})
|
||||
export class ContextMenuComponent {
|
||||
|
||||
|
||||
public x: string = "0";
|
||||
public y: string = "0";
|
||||
|
||||
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public onContextMenu(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.x = event.clientX + "px";
|
||||
this.y = event.clientY + "px";
|
||||
this.menuTrigger.openMenu();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<h1 mat-dialog-title>Filters</h1>
|
||||
<div mat-dialog-content fxLayout="column" class="filter-dialog-content">
|
||||
<div fxFlex fxFlexFill class="filter-dialog-list">
|
||||
<mat-list>
|
||||
<mat-list-item class="filter-list-item" *ngFor="let expression of filters" [class.selected]="expression.selected">
|
||||
<app-tag-filter-list-item (querySelect)="this.addToSelection($event)" (queryUnselect)="this.removeFromSelection($event)" (removeClicked)="this.removeFilter($event)"
|
||||
(contextmenu)="contextMenu.onContextMenu($event)" [expression]="expression"></app-tag-filter-list-item>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
<mat-divider fxFlex="10px"></mat-divider>
|
||||
<app-tag-input fxFlex="5em" class="tag-input" [allowNegation]="true" [availableTags]="this.availableTags"
|
||||
(tagAdded)="this.addFilter($event)"></app-tag-input>
|
||||
</div>
|
||||
<div class="dialog-actions" mat-dialog-actions>
|
||||
<button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button>
|
||||
<button mat-stroked-button color="accent" (click)="cancelFilter()">Cancel</button>
|
||||
</div>
|
||||
<app-context-menu #contextMenu>
|
||||
<button mat-menu-item (click)="this.convertSelectionToOrExpression()">Copy to OR-Expression</button>
|
||||
<button mat-menu-item (click)="this.convertSelectionToAndExpression()">Copy to AND-Expression</button>
|
||||
<button mat-menu-item (click)="this.invertSelection()">Invert</button>
|
||||
</app-context-menu>
|
@ -0,0 +1,41 @@
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-list-item.filter-list-item {
|
||||
height: 100%;
|
||||
padding: 0.5em 0;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
app-tag-filter-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
.filter-dialog-content {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-dialog-list {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterDialogComponent } from './filter-dialog.component';
|
||||
|
||||
describe('FilterDialogComponent', () => {
|
||||
let component: FilterDialogComponent;
|
||||
let fixture: ComponentFixture<FilterDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,167 @@
|
||||
import {Component, HostListener, Inject, ViewChildren} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
|
||||
import {
|
||||
FilterExpression,
|
||||
OrFilterExpression,
|
||||
SingleFilterExpression
|
||||
} from "../../../models/FilterExpression";
|
||||
import {TagQuery} from "../../../models/TagQuery";
|
||||
import {Tag} from "../../../models/Tag";
|
||||
import {TagFilterListItemComponent} from "./tag-filter-list-item/tag-filter-list-item.component";
|
||||
import {Selectable} from "../../../models/Selectable";
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-dialog',
|
||||
templateUrl: './filter-dialog.component.html',
|
||||
styleUrls: ['./filter-dialog.component.scss']
|
||||
})
|
||||
export class FilterDialogComponent {
|
||||
|
||||
public filters: Selectable<FilterExpression>[];
|
||||
public availableTags: Tag[] = [];
|
||||
public mode: "AND" | "OR" = "AND";
|
||||
|
||||
@ViewChildren(
|
||||
TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[];
|
||||
|
||||
private selectedQueries: TagQuery[] = [];
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
|
||||
MAT_DIALOG_DATA) data: any) {
|
||||
this.filters = data.filterEntries.map(
|
||||
(f: FilterExpression) => new Selectable<FilterExpression>(f,
|
||||
false)) ?? [];
|
||||
this.availableTags = data.availableTags ?? [];
|
||||
}
|
||||
|
||||
public cancelFilter(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public confirmFilter(): void {
|
||||
this.dialogRef.close(this.filters.map(f => f.data));
|
||||
}
|
||||
|
||||
public removeFilter(event: TagFilterListItemComponent): void {
|
||||
const filter = event.expression;
|
||||
const index = this.filters.findIndex(f => f === filter);
|
||||
if (index >= 0) {
|
||||
this.filters.splice(index, 1);
|
||||
}
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
public addFilter(tag: string) {
|
||||
const query = TagQuery.fromString(tag);
|
||||
|
||||
if (this.mode === "AND" || this.filters.length === 0) {
|
||||
this.filters.push(
|
||||
new Selectable<FilterExpression>(new SingleFilterExpression(query),
|
||||
false));
|
||||
tag = tag.replace(/^-/g, '');
|
||||
|
||||
if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) {
|
||||
const index = this.filters.findIndex(t => t.data.partiallyEq(tag));
|
||||
this.filters.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
let queryList = this.filters.pop()?.data.queryList() ?? [];
|
||||
|
||||
queryList.push(query);
|
||||
const filterExpression = new OrFilterExpression(queryList);
|
||||
filterExpression.removeDuplicates();
|
||||
this.filters.push(
|
||||
new Selectable<FilterExpression>(filterExpression,
|
||||
false));
|
||||
}
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
public addToSelection(query: TagQuery): void {
|
||||
this.selectedQueries.push(query);
|
||||
}
|
||||
|
||||
public removeFromSelection(query: TagQuery): void {
|
||||
const index = this.selectedQueries.indexOf(query);
|
||||
if (index > 0) {
|
||||
this.selectedQueries.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public unselectAll() {
|
||||
this.filters.forEach(filter => filter.selected = false);
|
||||
this.selectedQueries = [];
|
||||
this.filterListItems.forEach(i => i.selectedIndices = []);
|
||||
}
|
||||
|
||||
public convertSelectionToAndExpression(): void {
|
||||
for (const query of this.selectedQueries) {
|
||||
this.filters.push(new Selectable<FilterExpression>(new SingleFilterExpression(query), false));
|
||||
}
|
||||
this.removeFilterDuplicates();
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
public convertSelectionToOrExpression(): void {
|
||||
const queries = this.selectedQueries;
|
||||
const expression = new OrFilterExpression(queries);
|
||||
this.filters.push(new Selectable<FilterExpression>(expression, false));
|
||||
this.removeFilterDuplicates();
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
public invertSelection(): void {
|
||||
this.selectedQueries.forEach(query => query.negate = !query.negate);
|
||||
}
|
||||
|
||||
private removeFilterDuplicates() {
|
||||
const filters = this.filters;
|
||||
let newFilters: Selectable<FilterExpression>[] = [];
|
||||
|
||||
for (const filterItem of filters) {
|
||||
if (filterItem.data.filter_type == "OrExpression") {
|
||||
(filterItem.data as OrFilterExpression).removeDuplicates();
|
||||
}
|
||||
if (newFilters.findIndex(f => FilterDialogComponent.checkFiltersEqual(f.data, filterItem.data)) < 0) {
|
||||
if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) {
|
||||
filterItem.data = new SingleFilterExpression(filterItem.data.queryList()[0]);
|
||||
}
|
||||
newFilters.push(filterItem);
|
||||
}
|
||||
}
|
||||
this.filters = newFilters;
|
||||
}
|
||||
|
||||
private static checkFiltersEqual(l: FilterExpression, r: FilterExpression): boolean {
|
||||
const lTags = l.queryList().map(q => q.getNormalizedTag()).sort();
|
||||
const rTags = r.queryList().map(q => q.getNormalizedTag()).sort();
|
||||
let match = false;
|
||||
|
||||
if (lTags.length == rTags.length) {
|
||||
match = true;
|
||||
|
||||
for (const tag of lTags) {
|
||||
match = rTags.includes(tag);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
@HostListener("window:keydown", ["$event"])
|
||||
private async handleKeydownEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Shift") {
|
||||
this.mode = "OR";
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener("window:keyup", ["$event"])
|
||||
private async handleKeyupEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Shift") {
|
||||
this.mode = "AND";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<div *ngIf="expression.data.filter_type === 'Query'" (click)="onSelect()">
|
||||
{{expression.data.getDisplayName()}}
|
||||
<button mat-button class="remove-button" (click)="this.removeClicked.emit(this)">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="expression.data.filter_type === 'OrExpression'">
|
||||
<mat-list>
|
||||
<mat-list-item class="or-filter-list-item" *ngFor="let entry of enumerate(this.expression.data.queryList())"
|
||||
(mousedown)="$event.button === 0 && this.selectInnerIndex(entry[0])" [class.selected]="this.selectedIndices.includes(entry[0])">
|
||||
<span class="or-span" *ngIf="entry[0] > 0">OR</span>
|
||||
{{entry[1].getNormalizedTag()}}
|
||||
<button mat-button class="remove-button-inner-list" (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])">
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
@ -0,0 +1,41 @@
|
||||
.remove-button, .remove-button-inner-list {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
top: calc(0.5em - 15px);
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
mat-list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
background-color: #353535;
|
||||
padding: 0;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
mat-list-item.or-filter-list-item {
|
||||
padding: 0.5em 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
::ng-deep .mat-list-item-content {
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.or-span {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #5c5c5c;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagFilterListItemComponent } from './tag-filter-list-item.component';
|
||||
|
||||
describe('TagFilterListItemComponent', () => {
|
||||
let component: TagFilterListItemComponent;
|
||||
let fixture: ComponentFixture<TagFilterListItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ TagFilterListItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagFilterListItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,75 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input, OnChanges,
|
||||
OnInit,
|
||||
Output, SimpleChanges
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FilterExpression,
|
||||
OrFilterExpression, SingleFilterExpression
|
||||
} from "../../../../models/FilterExpression";
|
||||
import {TagQuery} from "../../../../models/TagQuery";
|
||||
import {Selectable} from "../../../../models/Selectable";
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-filter-list-item',
|
||||
templateUrl: './tag-filter-list-item.component.html',
|
||||
styleUrls: ['./tag-filter-list-item.component.scss']
|
||||
})
|
||||
export class TagFilterListItemComponent implements OnChanges {
|
||||
|
||||
@Input() expression!: Selectable<FilterExpression>;
|
||||
@Output() removeClicked = new EventEmitter<TagFilterListItemComponent>();
|
||||
@Output() querySelect = new EventEmitter<TagQuery>();
|
||||
@Output() queryUnselect = new EventEmitter<TagQuery>();
|
||||
|
||||
public selectedIndices: number[] = [];
|
||||
|
||||
constructor(private changeDetector: ChangeDetectorRef) { }
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["expression"]) {
|
||||
this.selectedIndices = [];
|
||||
}
|
||||
}
|
||||
|
||||
public enumerate<T>(items: T[]): [number, T][] {
|
||||
return items.map((value, index) => [index, value]);
|
||||
}
|
||||
|
||||
public removeOrExpression(index: number) {
|
||||
const expression = this.expression.data as OrFilterExpression;
|
||||
expression.removeQueryEntry(index);
|
||||
|
||||
if (expression.filter.length == 0) {
|
||||
this.removeClicked.emit(this);
|
||||
} else if (expression.filter.length == 1) {
|
||||
this.expression.data = new SingleFilterExpression(expression.filter[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public selectInnerIndex(index: number): void {
|
||||
const expression = this.expression.data as OrFilterExpression;
|
||||
|
||||
if (this.selectedIndices.includes(index)) {
|
||||
const elementIndex = this.selectedIndices.indexOf(index);
|
||||
this.selectedIndices.splice(elementIndex, 1);
|
||||
this.queryUnselect.emit(expression.filter[index]);
|
||||
} else {
|
||||
this.selectedIndices.push(index);
|
||||
this.querySelect.emit(expression.filter[index]);
|
||||
}
|
||||
}
|
||||
|
||||
public onSelect(): void {
|
||||
this.expression.selected = !this.expression.selected;
|
||||
if (this.expression.selected) {
|
||||
this.querySelect.emit(this.expression.data.filter as TagQuery);
|
||||
} else {
|
||||
this.queryUnselect.emit(this.expression.data.filter as TagQuery);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
Enter a tag
|
||||
</mat-label>
|
||||
<input #tagInput
|
||||
[formControl]="formControl"
|
||||
matInput
|
||||
(keydown.enter)="addTagByInput($event)"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete #auto (optionSelected)="addTagByAutocomplete($event)">
|
||||
<mat-option *ngFor="let tag of autosuggestTags | async" [value]="tag">
|
||||
{{tag}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
@ -0,0 +1,3 @@
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagInputComponent } from './tag-input.component';
|
||||
|
||||
describe('TagInputComponent', () => {
|
||||
let component: TagInputComponent;
|
||||
let fixture: ComponentFixture<TagInputComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ TagInputComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,114 @@
|
||||
import {
|
||||
Component, ElementRef,
|
||||
EventEmitter,
|
||||
Input, OnChanges,
|
||||
OnInit,
|
||||
Output, SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {Tag} from "../../../models/Tag";
|
||||
import {FormControl} from "@angular/forms";
|
||||
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
|
||||
import {Observable} from "rxjs";
|
||||
import {debounceTime, delay, map, startWith} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-input',
|
||||
templateUrl: './tag-input.component.html',
|
||||
styleUrls: ['./tag-input.component.scss']
|
||||
})
|
||||
export class TagInputComponent implements OnChanges{
|
||||
|
||||
@Input() availableTags: Tag[] = [];
|
||||
@Input() allowNegation: boolean = false;
|
||||
@Input() allowInvalid: boolean = false;
|
||||
@Output() tagAdded = new EventEmitter<string>();
|
||||
|
||||
@ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>;
|
||||
public formControl = new FormControl();
|
||||
public autosuggestTags: Observable<string[]>;
|
||||
private tagsForAutocomplete: string[] = [];
|
||||
|
||||
constructor() {
|
||||
this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput());
|
||||
this.autosuggestTags = this.formControl.valueChanges.pipe(
|
||||
startWith(null),
|
||||
debounceTime(250),
|
||||
map((tag: string | null) => tag ? this.filterSuggestionTag(tag) : this.tagsForAutocomplete.slice(0, 20)));
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["availableTags"]) {
|
||||
this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput());
|
||||
}
|
||||
}
|
||||
|
||||
public addTagByInput(event: any): void {
|
||||
this.addTag(this.formControl.value);
|
||||
}
|
||||
|
||||
public addTagByAutocomplete(event: MatAutocompleteSelectedEvent): void {
|
||||
this.addTag(event.option.value);
|
||||
}
|
||||
|
||||
private addTag(value: string) {
|
||||
const tag = this.normalizeTag(value);
|
||||
if (tag.length > 0 && (this.allowInvalid || this.checkTagValid(tag))) {
|
||||
this.tagAdded.emit(tag);
|
||||
this.formControl.setValue("");
|
||||
this.tagInput.nativeElement.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
private filterSuggestionTag(tag: string) {
|
||||
let normalizedTag = this.normalizeTag(tag);
|
||||
const negated = normalizedTag.startsWith("-") && this.allowNegation;
|
||||
normalizedTag = this.allowNegation? normalizedTag.replace(/^-/, "") : normalizedTag;
|
||||
|
||||
return this.tagsForAutocomplete.filter(
|
||||
t => t.includes(normalizedTag))
|
||||
.map(t => negated ? "-" + t: t)
|
||||
.sort((l, r) => this.compareSuggestionTags(normalizedTag, l, r))
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
private checkTagValid(tag: string): boolean {
|
||||
if (this.allowNegation) {
|
||||
tag = tag.replace(/^-/, "");
|
||||
}
|
||||
return this.tagsForAutocomplete.includes(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the tag by removing whitespaces
|
||||
* @param {string} tag
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
private normalizeTag(tag: string): string {
|
||||
let normalizedTag = tag.trim();
|
||||
let parts = normalizedTag.split(":");
|
||||
|
||||
if (parts.length > 1) {
|
||||
const namespace = parts.shift()!.trim();
|
||||
const name = parts.join(":").trim();
|
||||
return namespace + ":" + name;
|
||||
} else {
|
||||
return normalizedTag;
|
||||
}
|
||||
}
|
||||
|
||||
private compareSuggestionTags(query: string, l: string, r: string): number {
|
||||
if (l.startsWith(query) && !r.startsWith(query)) {
|
||||
return -1;
|
||||
} else if (!l.startsWith(query) && r.startsWith(query)) {
|
||||
return 1;
|
||||
} else if (l.length < r.length) {
|
||||
return -1;
|
||||
} else if (l.length > r.length) {
|
||||
return 1;
|
||||
} else {
|
||||
return l.localeCompare(r)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue