Skip to content
This repository was archived by the owner on Mar 8, 2024. It is now read-only.

Shared component dropdown it #17

Merged
merged 3 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/app/base/base.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PercentRoundPipe } from './pipes/decimal-round.pipe';
import { HeaderComponent } from './components/header/header.component';
import { DropdownComponent } from './components/dropdown/dropdown.component';
import { DropdownIterativeComponent } from './components/dropdown-iterative/dropdown-iterative.component';
import { DropdownItComponent } from './components/dropdown-it/dropdown-it.component';

@NgModule({
declarations: [
Expand All @@ -30,7 +31,8 @@ import { DropdownIterativeComponent } from './components/dropdown-iterative/drop
PercentRoundPipe,
HeaderComponent,
DropdownComponent,
DropdownIterativeComponent
DropdownIterativeComponent,
DropdownItComponent
],
imports: [CommonModule, AppRoutingModule, HighlightModule],
exports: [
Expand All @@ -53,7 +55,8 @@ import { DropdownIterativeComponent } from './components/dropdown-iterative/drop
PercentRoundPipe,
HeaderComponent,
DropdownComponent,
DropdownIterativeComponent
DropdownIterativeComponent,
DropdownItComponent
],
providers: [
{
Expand Down
26 changes: 26 additions & 0 deletions src/app/base/components/dropdown-it/dropdown-it-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FormArray } from "@angular/forms";

/**
* Optionset for kern dropdown
* @optionArray {string[] | FormArray[] | any[]} - Can be any array. string array is just used, FormArray or any object array tries to use "name" property then "text" last first string property
* @buttonCaption {string, optional} - used as caption for the button, if not given the first / current value is used
* @valuePropertyPath {string, optional} - if undefined option text is returned, else (e.g. name.tmp.xyz) the path is split and used to access the object property
* @keepDropdownOpen {boolean, optional} - stops the event propagation of the click event and therfore keeps the menu open
* @buttonTooltip {string, optional} - adds a tooltip if defined
* @isDisabled {boolean, optional} - disables the dropdown
* @isOptionDisabled {boolean[], optional} - disables the dropdown option (needs to be the exact same length as the optionArray)
* @optionIcons {string[], optional} - displays a predfined icon if set for the index (needs to be the exact same length as the optionArray)
* @hasCheckboxes {boolean, optional} - helper for checkbox like dropdowns (e.g. data browser)
*/
export type DropdownOptions = {
optionArray: string[] | FormArray[] | any[];
buttonCaption?: string;
valuePropertyPath?: string;
keepDropdownOpen?: boolean;
buttonTooltip?: string;
isDisabled?: boolean;
isOptionDisabled?: boolean[];
optionIcons?: string[];
hasCheckboxes?: boolean;
};

144 changes: 144 additions & 0 deletions src/app/base/components/dropdown-it/dropdown-it.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<div class="relative">
<button #dropdownOpenButton type="button" [attr.data-tip]="dropdownOptions.buttonTooltip"
class="inline-flex rounded-md border border-gray-300 shadow-sm px-4 py-1.5 items-center bg-white text-xs font-semibold text-gray-700 cursor-pointer focus:ring-offset-2 focus:ring-offset-gray-400"
id="menu-button" aria-expanded="true" aria-haspopup="true" [disabled]="dropdownOptions.isDisabled"
[ngClass]="buttonClassList" (isMenuOpen)="toggleVisible($event, dropdownOptionsDiv)" appDropdown>
{{ dropdownOptions.hasCheckboxes?'':dropdownOptions.buttonCaption }}
<ng-template [ngIf]="dropdownOptions.hasCheckboxes">
<label class="truncate cursor-pointer">{{getDropdownDisplayText(dropdownOptions.optionArray,"EMPTY")}}
<span style="color:#2563eb">{{getDropdownDisplayText(dropdownOptions.optionArray,"NOT_NEGATED")}}</span>
<span style="color:#ef4444">{{getDropdownDisplayText(dropdownOptions.optionArray,"NEGATED")}}</span>
</label>
</ng-template>
<svg class="-mr-1 ml-auto h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
<div #dropdownOptionsDiv
class="origin-top-right absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none hidden"
[ngClass]="dropdownClassList" role="menu" aria-orientation="vertical" aria-labelledby="menu-button"
[style.min-width.px]="dropdownOpenButton.offsetWidth" tabindex="-1">
<div class="py-1 cursor-pointer" role="none">
<div *ngFor="let caption of dropdownOptionCaptions;let i = index" (click)="performActionOnOption($event,i)"
[ngClass]="[dropdownOptions.isOptionDisabled?.length && dropdownOptions.isOptionDisabled[i]?'opacity-50 cursor-not-allowed':'',!dropdownOptions.hasCheckboxes ? 'hover:bg-gray-700 hover:text-white':'']"
class="text-gray-700 block px-2 py-1.5 text-xs flex flex-row flex-nowrap items-center gap-x-2"
role="menuitem" tabindex="-1" id="menu-item-0">
<ng-template [ngIf]="dropdownOptions.hasCheckboxes">
<div class="h-4 w-4 border-gray-300 border rounded hover:bg-gray-200"
[ngStyle]="{'background-color':getActiveNegateGroupColor(dropdownOptions.optionArray[i]), 'border-color':getActiveNegateGroupColor(dropdownOptions.optionArray[i])}">
</div>
</ng-template>
<ng-template [ngIf]="dropdownOptions.optionIcons?.length">
<ng-container [ngTemplateOutlet]="icons"
[ngTemplateOutletContext]="{iconName:dropdownOptions.optionIcons[i]}">
</ng-container>
</ng-template>
{{ caption }}
</div>
</div>
</div>
</div>


<ng-template #icons let-iconName="iconName">
<ng-container [ngSwitch]="iconName">
<ng-template ngSwitchDefault>X
</ng-template>
<ng-template ngSwitchCase="clickbait">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-fish-hook" width="20"
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M16 9v6a5 5 0 0 1 -10 0v-4l3 3"></path>
<circle cx="16" cy="7" r="2"></circle>
<path d="M16 5v-2"></path>
</svg>
</ng-template>
<ng-template ngSwitchCase="conversational-ai">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-circle" width="20"
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 20l1.3 -3.9a9 8 0 1 1 3.4 2.9l-4.7 1"></path>
<line x1="12" y1="12" x2="12" y2="12.01"></line>
<line x1="8" y1="12" x2="8" y2="12.01"></line>
<line x1="16" y1="12" x2="16" y2="12.01"></line>
</svg>
</ng-template>
<ng-template ngSwitchCase="ag-news">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-news" width="20" height="20"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M16 6h3a1 1 0 0 1 1 1v11a2 2 0 0 1 -4 0v-13a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1v12a3 3 0 0 0 3 3h11">
</path>
<line x1="8" y1="8" x2="12" y2="8"></line>
<line x1="8" y1="12" x2="12" y2="12"></line>
<line x1="8" y1="16" x2="12" y2="16"></line>
</svg>
</ng-template>
<ng-template ngSwitchCase="select-all">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-check inline-block"
width="20" height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
<path d="M9 12l2 2l4 -4"></path>
</svg>
</ng-template>
<ng-template ngSwitchCase="deselect-all">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square inline-block" width="20"
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
</svg>
</ng-template>
<ng-template ngSwitchCase="run">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
clip-rule="evenodd" />
</svg>
</ng-template>
<ng-template ngSwitchCase="edit-term">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd" />
</svg>
</ng-template>
<ng-template ngSwitchCase="remove-term">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd" />
</svg>
</ng-template>
<ng-template ngSwitchCase="whitelist-term">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</ng-template>
<ng-template ngSwitchCase="blacklist-term">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z"
clip-rule="evenodd" />
</svg>
</ng-template>
</ng-container>

</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tooltip::before {
z-index: 50;
}
25 changes: 25 additions & 0 deletions src/app/base/components/dropdown-it/dropdown-it.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { DropdownItComponent } from './dropdown-it.component';

describe('DropdownItComponent', () => {
let component: DropdownItComponent;
let fixture: ComponentFixture<DropdownItComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DropdownItComponent]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(DropdownItComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
162 changes: 162 additions & 0 deletions src/app/base/components/dropdown-it/dropdown-it.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { DropdownOptions } from './dropdown-it-helper';

@Component({
selector: 'kern-dropdown-it',
templateUrl: './dropdown-it.component.html',
styleUrls: ['./dropdown-it.component.scss']
})
export class DropdownItComponent implements OnChanges {

@Input() dropdownOptions: DropdownOptions;
@Output() optionClicked = new EventEmitter<string | any>();

hasInputErrors: string;
buttonClassList: string;
dropdownClassList: string;
dropdownOptionCaptions: string[];
useValueAsCaption: boolean = false;

constructor() { }
ngOnChanges(changes: SimpleChanges): void {
this.dropdownOptionCaptions = this.getTextArray(this.dropdownOptions.optionArray);
this.runInputChecks();
this.buildHelperValues();
}

private getTextArray(arr: string[] | any[]): string[] {
if (!arr) return [];
if (arr.length == 0) return [];
if (typeof arr[0] == 'string') return arr as string[];
let valueArray = arr;
if (arr[0].value && typeof arr[0].value == 'object') valueArray = arr.map(x => x.getRawValue());
if (valueArray[0].name) return valueArray.map(a => a.name);
if (valueArray[0].text) return valueArray.map(a => a.text);

let firstStringKey = "";

for (const key of Object.keys(valueArray[0])) {
if (typeof valueArray[0][key] == 'string') {
firstStringKey = key;
break;
}
}
if (!firstStringKey) throw new Error("Cant find text in given array - dropdown");
return valueArray.map(a => a[firstStringKey]);
}

private buildHelperValues() {
this.buttonClassList = "";
this.buttonClassList += this.dropdownOptions.isDisabled ? 'opacity-50 cursor-not-allowed' : 'opacity-100 cursor-pointer';
this.buttonClassList += this.dropdownOptions.buttonTooltip ? ' tooltip tooltip-right' : '';
this.dropdownClassList = this.dropdownOptions.hasCheckboxes ? ' w-80' : ' w-auto';
this.buttonClassList += this.dropdownClassList;
}

private runInputChecks() {
this.hasInputErrors = "";
if (!this.dropdownOptions) this.hasInputErrors = "no dropdown options provided\n";
if (!this.dropdownOptions.optionArray || this.dropdownOptions.optionArray.length == 0) this.hasInputErrors = "no text provided\n";
if (!this.dropdownOptions.buttonCaption && this.dropdownOptionCaptions.length > 0) {
this.dropdownOptions.buttonCaption = this.dropdownOptionCaptions[0];
this.useValueAsCaption = true;
}
if (this.dropdownOptions.isOptionDisabled && this.dropdownOptions.isOptionDisabled.length != this.dropdownOptions.optionArray.length) this.hasInputErrors = "array options != isOptionDisabled length\n";
if (this.dropdownOptions.optionIcons && this.dropdownOptions.optionIcons.length != this.dropdownOptions.optionIcons.length) this.hasInputErrors = "array options != optionIcons length\n";


if (this.hasInputErrors) console.log(this.hasInputErrors);

}

toggleVisible(isVisible: boolean, dropdownOptions: HTMLDivElement): void {
if (isVisible) {
dropdownOptions.classList.remove('hidden');
dropdownOptions.classList.add('block');
dropdownOptions.classList.add('z-10');
} else {
dropdownOptions.classList.remove('z-10');
dropdownOptions.classList.remove('block');
dropdownOptions.classList.add('hidden');
}
}

performActionOnOption(event: MouseEvent, clickIndex: number) {
if (this.dropdownOptions.isOptionDisabled?.length && this.dropdownOptions.isOptionDisabled[clickIndex]) return;
if (this.dropdownOptions.keepDropdownOpen) event.stopPropagation();

if (clickIndex >= this.dropdownOptions.optionArray.length) {
console.log("something is wrong in the click action of the dropdown component");
return;
}

if (!this.dropdownOptions.valuePropertyPath) {
if (this.useValueAsCaption) this.dropdownOptions.buttonCaption = this.dropdownOptionCaptions[clickIndex];
if (this.dropdownOptions.hasCheckboxes) this.optionClicked.emit(this.dropdownOptions.optionArray[clickIndex]);
else this.optionClicked.emit(this.dropdownOptionCaptions[clickIndex]);
return;
}

const splittedPath = this.dropdownOptions.valuePropertyPath.split(".");

let tmp = this.dropdownOptions.optionArray[clickIndex];
for (const key of splittedPath) {
tmp = tmp[key];
}
if (typeof tmp != "string") {
console.log("something is wrong in the click action of the dropdown component - property path");
return;
}
if (this.useValueAsCaption) this.dropdownOptions.buttonCaption = tmp;
this.optionClicked.emit(tmp);

}

getActiveNegateGroupColor(group: FormGroup) {
if (!group.get('active').value) return null;
if (group.contains('negate'))
return group.get('negate').value ? '#ef4444' : '#2563eb';
return '#2563eb';
}


getDropdownDisplayText(
formControls: AbstractControl[],
labelFor: string
): string {
let text = '';
let atLeastOneNegated: boolean = false;
for (let c of formControls) {
const hasNegate = Boolean(c.get('negate'));
if (labelFor == 'EMPTY' && c.get('active').value) return '';
else if (
labelFor == 'NOT_NEGATED' &&
c.get('active').value &&
(!hasNegate || (hasNegate && !c.get('negate').value))
) {
text += (text == '' ? '' : ', ') + c.get('name').value;
} else if (
labelFor == 'NEGATED' &&
c.get('active').value &&
hasNegate &&
c.get('negate').value
) {
text += (text == '' ? '' : ', ') + c.get('name').value;
}
if (
!atLeastOneNegated &&
c.get('active').value &&
hasNegate &&
c.get('negate').value
)
atLeastOneNegated = true;
}
if (labelFor == 'EMPTY') return 'None Selected';

if (labelFor == 'NOT_NEGATED' && atLeastOneNegated && text != '')
return text + ', ';

return text;
}
}
Loading