Add context menu to switch filters from or to and

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 349d1dfc31
commit 69c188d288

@ -57,6 +57,7 @@ import {MatCheckboxModule} from "@angular/material/checkbox";
import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.component'; import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.component';
import { TagFilterListItemComponent } from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component'; import { TagFilterListItemComponent } from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component';
import { TagInputComponent } from './components/inputs/tag-input/tag-input.component'; import { TagInputComponent } from './components/inputs/tag-input/tag-input.component';
import { ContextMenuComponent } from './components/context-menu/context-menu.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -84,6 +85,7 @@ import { TagInputComponent } from './components/inputs/tag-input/tag-input.compo
FilterDialogComponent, FilterDialogComponent,
TagFilterListItemComponent, TagFilterListItemComponent,
TagInputComponent, TagInputComponent,
ContextMenuComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

@ -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();
}
}

@ -1,8 +1,9 @@
<h1 mat-dialog-title>Filters</h1> <h1 mat-dialog-title>Filters</h1>
<div mat-dialog-content> <div mat-dialog-content>
<mat-list> <mat-list>
<mat-list-item class="filter-list-item" *ngFor="let expression of filters"> <mat-list-item class="filter-list-item" *ngFor="let expression of filters" [class.selected]="expression.selected">
<app-tag-filter-list-item (removeClicked)="this.removeFilter($event)" [expression]="expression"></app-tag-filter-list-item> <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-item>
</mat-list> </mat-list>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -13,3 +14,7 @@
<button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button> <button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button>
<button mat-stroked-button color="accent" (click)="cancelFilter()">Cancel</button> <button mat-stroked-button color="accent" (click)="cancelFilter()">Cancel</button>
</div> </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>
</app-context-menu>

@ -15,8 +15,14 @@
mat-list-item.filter-list-item { mat-list-item.filter-list-item {
height: 100%; height: 100%;
padding: 0.5em 0; padding: 0.5em 0;
user-select: none;
cursor: pointer;
} }
app-tag-filter-list-item { app-tag-filter-list-item {
width: 100%; width: 100%;
} }
.selected {
background-color: #5c5c5c;
}

@ -1,14 +1,9 @@
import { import {Component, HostListener, Inject, ViewChildren} from '@angular/core';
Component,
ElementRef,
HostListener,
Inject,
ViewChild
} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component"; import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
import { import {
FilterExpression, OrFilterExpression, FilterExpression,
OrFilterExpression,
SingleFilterExpression SingleFilterExpression
} from "../../../models/FilterExpression"; } from "../../../models/FilterExpression";
import {TagQuery} from "../../../models/TagQuery"; import {TagQuery} from "../../../models/TagQuery";
@ -27,9 +22,16 @@ export class FilterDialogComponent {
public availableTags: Tag[] = []; public availableTags: Tag[] = [];
public mode: "AND" | "OR" = "AND"; public mode: "AND" | "OR" = "AND";
@ViewChildren(
TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[];
private selectedQueries: TagQuery[] = [];
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject( constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
MAT_DIALOG_DATA) data: any) { MAT_DIALOG_DATA) data: any) {
this.filters = data.filterEntries.map((f: FilterExpression) => new Selectable<FilterExpression>(f, false)) ?? []; this.filters = data.filterEntries.map(
(f: FilterExpression) => new Selectable<FilterExpression>(f,
false)) ?? [];
this.availableTags = data.availableTags ?? []; this.availableTags = data.availableTags ?? [];
} }
@ -47,13 +49,16 @@ export class FilterDialogComponent {
if (index >= 0) { if (index >= 0) {
this.filters.splice(index, 1); this.filters.splice(index, 1);
} }
this.unselectAll();
} }
public addFilter(tag: string) { public addFilter(tag: string) {
const query = TagQuery.fromString(tag); const query = TagQuery.fromString(tag);
if (this.mode === "AND" || this.filters.length === 0) { if (this.mode === "AND" || this.filters.length === 0) {
this.filters.push(new Selectable<FilterExpression>(new SingleFilterExpression(query), false)); this.filters.push(
new Selectable<FilterExpression>(new SingleFilterExpression(query),
false));
tag = tag.replace(/^-/g, ''); tag = tag.replace(/^-/g, '');
if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) { if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) {
@ -64,8 +69,82 @@ export class FilterDialogComponent {
let queryList = this.filters.pop()?.data.queryList() ?? []; let queryList = this.filters.pop()?.data.queryList() ?? [];
queryList.push(query); queryList.push(query);
this.filters.push(new Selectable<FilterExpression>(new OrFilterExpression(queryList), false)); 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();
}
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"]) @HostListener("window:keydown", ["$event"])

@ -1,15 +1,16 @@
<span *ngIf="expression.data.filter_type === 'Query'" [class.selected]="this.expression.selected"> <div *ngIf="expression.data.filter_type === 'Query'" (click)="onSelect()">
{{expression.data.getDisplayName()}} {{expression.data.getDisplayName()}}
<button mat-button class="remove-button" (click)="this.removeClicked.emit(this)"> <button mat-button class="remove-button" (click)="this.removeClicked.emit(this)">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
</button> </button>
</span> </div>
<div *ngIf="expression.data.filter_type === 'OrExpression'"> <div *ngIf="expression.data.filter_type === 'OrExpression'">
<mat-list> <mat-list>
<mat-list-item class="or-filter-list-item" *ngFor="let entry of enumerate(this.expression.data.queryList())"> <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> <span class="or-span" *ngIf="entry[0] > 0">OR</span>
{{entry[1].getNormalizedTag()}} {{entry[1].getNormalizedTag()}}
<button mat-button class="remove-button-inner-list" (mousedown)="this.removeOrExpression(entry[0])"> <button mat-button class="remove-button-inner-list" (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
</button> </button>
</mat-list-item> </mat-list-item>

@ -14,6 +14,7 @@ mat-list {
width: 100%; width: 100%;
display: block; display: block;
background-color: #353535; background-color: #353535;
padding: 0;
border-radius: 0.25em; border-radius: 0.25em;
} }
@ -21,6 +22,9 @@ mat-list-item.or-filter-list-item {
padding: 0.5em 0; padding: 0.5em 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
border-collapse: collapse;
cursor: pointer;
user-select: none;
::ng-deep .mat-list-item-content { ::ng-deep .mat-list-item-content {
padding-right: 0; padding-right: 0;
@ -31,3 +35,7 @@ mat-list-item.or-filter-list-item {
.or-span { .or-span {
margin-right: 0.5em; margin-right: 0.5em;
} }
.selected {
background-color: #5c5c5c;
}

@ -3,9 +3,9 @@ import {
Component, Component,
EventEmitter, EventEmitter,
Inject, Inject,
Input, Input, OnChanges,
OnInit, OnInit,
Output Output, SimpleChanges
} from '@angular/core'; } from '@angular/core';
import { import {
FilterExpression, FilterExpression,
@ -19,12 +19,22 @@ import {Selectable} from "../../../../models/Selectable";
templateUrl: './tag-filter-list-item.component.html', templateUrl: './tag-filter-list-item.component.html',
styleUrls: ['./tag-filter-list-item.component.scss'] styleUrls: ['./tag-filter-list-item.component.scss']
}) })
export class TagFilterListItemComponent { export class TagFilterListItemComponent implements OnChanges {
@Input() expression!: Selectable<FilterExpression>; @Input() expression!: Selectable<FilterExpression>;
@Output() removeClicked = new EventEmitter<TagFilterListItemComponent>(); @Output() removeClicked = new EventEmitter<TagFilterListItemComponent>();
@Output() querySelect = new EventEmitter<TagQuery>();
@Output() queryUnselect = new EventEmitter<TagQuery>();
constructor() { } public selectedIndices: number[] = [];
constructor(private changeDetector: ChangeDetectorRef) { }
public ngOnChanges(changes: SimpleChanges): void {
if (changes["expression"]) {
this.selectedIndices = [];
}
}
public enumerate<T>(items: T[]): [number, T][] { public enumerate<T>(items: T[]): [number, T][] {
return items.map((value, index) => [index, value]); return items.map((value, index) => [index, value]);
@ -40,4 +50,26 @@ export class TagFilterListItemComponent {
this.expression.data = new SingleFilterExpression(expression.filter[0]); 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);
}
}
} }

@ -47,6 +47,18 @@ export class OrFilterExpression implements FilterExpression{
public removeQueryEntry(index: number) { public removeQueryEntry(index: number) {
this.filter.splice(index, 1); this.filter.splice(index, 1);
} }
public removeDuplicates() {
const filters = this.filter.reverse();
let newEntries: TagQuery[] = [];
for (const entry of filters) {
if (newEntries.findIndex(f => f.tag === entry.tag) < 0) {
newEntries.push(entry);
}
}
this.filter = newEntries.reverse();
}
} }
export class SingleFilterExpression implements FilterExpression { export class SingleFilterExpression implements FilterExpression {

Loading…
Cancel
Save