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

Commit c788e7d

Browse files
authored
Merge pull request #17 from code-kern-ai/shared-component-dropdown-it
Shared component dropdown it
2 parents 379df82 + 361714f commit c788e7d

File tree

9 files changed

+451
-48
lines changed

9 files changed

+451
-48
lines changed

src/app/base/base.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { PercentRoundPipe } from './pipes/decimal-round.pipe';
1818
import { HeaderComponent } from './components/header/header.component';
1919
import { DropdownComponent } from './components/dropdown/dropdown.component';
2020
import { DropdownIterativeComponent } from './components/dropdown-iterative/dropdown-iterative.component';
21+
import { DropdownItComponent } from './components/dropdown-it/dropdown-it.component';
2122

2223
@NgModule({
2324
declarations: [
@@ -30,7 +31,8 @@ import { DropdownIterativeComponent } from './components/dropdown-iterative/drop
3031
PercentRoundPipe,
3132
HeaderComponent,
3233
DropdownComponent,
33-
DropdownIterativeComponent
34+
DropdownIterativeComponent,
35+
DropdownItComponent
3436
],
3537
imports: [CommonModule, AppRoutingModule, HighlightModule],
3638
exports: [
@@ -53,7 +55,8 @@ import { DropdownIterativeComponent } from './components/dropdown-iterative/drop
5355
PercentRoundPipe,
5456
HeaderComponent,
5557
DropdownComponent,
56-
DropdownIterativeComponent
58+
DropdownIterativeComponent,
59+
DropdownItComponent
5760
],
5861
providers: [
5962
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { FormArray } from "@angular/forms";
2+
3+
/**
4+
* Optionset for kern dropdown
5+
* @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
6+
* @buttonCaption {string, optional} - used as caption for the button, if not given the first / current value is used
7+
* @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
8+
* @keepDropdownOpen {boolean, optional} - stops the event propagation of the click event and therfore keeps the menu open
9+
* @buttonTooltip {string, optional} - adds a tooltip if defined
10+
* @isDisabled {boolean, optional} - disables the dropdown
11+
* @isOptionDisabled {boolean[], optional} - disables the dropdown option (needs to be the exact same length as the optionArray)
12+
* @optionIcons {string[], optional} - displays a predfined icon if set for the index (needs to be the exact same length as the optionArray)
13+
* @hasCheckboxes {boolean, optional} - helper for checkbox like dropdowns (e.g. data browser)
14+
*/
15+
export type DropdownOptions = {
16+
optionArray: string[] | FormArray[] | any[];
17+
buttonCaption?: string;
18+
valuePropertyPath?: string;
19+
keepDropdownOpen?: boolean;
20+
buttonTooltip?: string;
21+
isDisabled?: boolean;
22+
isOptionDisabled?: boolean[];
23+
optionIcons?: string[];
24+
hasCheckboxes?: boolean;
25+
};
26+
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<div class="relative">
2+
<button #dropdownOpenButton type="button" [attr.data-tip]="dropdownOptions.buttonTooltip"
3+
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"
4+
id="menu-button" aria-expanded="true" aria-haspopup="true" [disabled]="dropdownOptions.isDisabled"
5+
[ngClass]="buttonClassList" (isMenuOpen)="toggleVisible($event, dropdownOptionsDiv)" appDropdown>
6+
{{ dropdownOptions.hasCheckboxes?'':dropdownOptions.buttonCaption }}
7+
<ng-template [ngIf]="dropdownOptions.hasCheckboxes">
8+
<label class="truncate cursor-pointer">{{getDropdownDisplayText(dropdownOptions.optionArray,"EMPTY")}}
9+
<span style="color:#2563eb">{{getDropdownDisplayText(dropdownOptions.optionArray,"NOT_NEGATED")}}</span>
10+
<span style="color:#ef4444">{{getDropdownDisplayText(dropdownOptions.optionArray,"NEGATED")}}</span>
11+
</label>
12+
</ng-template>
13+
<svg class="-mr-1 ml-auto h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
14+
aria-hidden="true">
15+
<path fill-rule="evenodd"
16+
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"
17+
clip-rule="evenodd" />
18+
</svg>
19+
</button>
20+
<div #dropdownOptionsDiv
21+
class="origin-top-right absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none hidden"
22+
[ngClass]="dropdownClassList" role="menu" aria-orientation="vertical" aria-labelledby="menu-button"
23+
[style.min-width.px]="dropdownOpenButton.offsetWidth" tabindex="-1">
24+
<div class="py-1 cursor-pointer" role="none">
25+
<div *ngFor="let caption of dropdownOptionCaptions;let i = index" (click)="performActionOnOption($event,i)"
26+
[ngClass]="[dropdownOptions.isOptionDisabled?.length && dropdownOptions.isOptionDisabled[i]?'opacity-50 cursor-not-allowed':'',!dropdownOptions.hasCheckboxes ? 'hover:bg-gray-700 hover:text-white':'']"
27+
class="text-gray-700 block px-2 py-1.5 text-xs flex flex-row flex-nowrap items-center gap-x-2"
28+
role="menuitem" tabindex="-1" id="menu-item-0">
29+
<ng-template [ngIf]="dropdownOptions.hasCheckboxes">
30+
<div class="h-4 w-4 border-gray-300 border rounded hover:bg-gray-200"
31+
[ngStyle]="{'background-color':getActiveNegateGroupColor(dropdownOptions.optionArray[i]), 'border-color':getActiveNegateGroupColor(dropdownOptions.optionArray[i])}">
32+
</div>
33+
</ng-template>
34+
<ng-template [ngIf]="dropdownOptions.optionIcons?.length">
35+
<ng-container [ngTemplateOutlet]="icons"
36+
[ngTemplateOutletContext]="{iconName:dropdownOptions.optionIcons[i]}">
37+
</ng-container>
38+
</ng-template>
39+
{{ caption }}
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
45+
46+
<ng-template #icons let-iconName="iconName">
47+
<ng-container [ngSwitch]="iconName">
48+
<ng-template ngSwitchDefault>X
49+
</ng-template>
50+
<ng-template ngSwitchCase="clickbait">
51+
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-fish-hook" width="20"
52+
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
53+
stroke-linecap="round" stroke-linejoin="round">
54+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
55+
<path d="M16 9v6a5 5 0 0 1 -10 0v-4l3 3"></path>
56+
<circle cx="16" cy="7" r="2"></circle>
57+
<path d="M16 5v-2"></path>
58+
</svg>
59+
</ng-template>
60+
<ng-template ngSwitchCase="conversational-ai">
61+
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-circle" width="20"
62+
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
63+
stroke-linecap="round" stroke-linejoin="round">
64+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
65+
<path d="M3 20l1.3 -3.9a9 8 0 1 1 3.4 2.9l-4.7 1"></path>
66+
<line x1="12" y1="12" x2="12" y2="12.01"></line>
67+
<line x1="8" y1="12" x2="8" y2="12.01"></line>
68+
<line x1="16" y1="12" x2="16" y2="12.01"></line>
69+
</svg>
70+
</ng-template>
71+
<ng-template ngSwitchCase="ag-news">
72+
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-news" width="20" height="20"
73+
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
74+
stroke-linejoin="round">
75+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
76+
<path
77+
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">
78+
</path>
79+
<line x1="8" y1="8" x2="12" y2="8"></line>
80+
<line x1="8" y1="12" x2="12" y2="12"></line>
81+
<line x1="8" y1="16" x2="12" y2="16"></line>
82+
</svg>
83+
</ng-template>
84+
<ng-template ngSwitchCase="select-all">
85+
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-check inline-block"
86+
width="20" height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
87+
stroke-linecap="round" stroke-linejoin="round">
88+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
89+
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
90+
<path d="M9 12l2 2l4 -4"></path>
91+
</svg>
92+
</ng-template>
93+
<ng-template ngSwitchCase="deselect-all">
94+
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square inline-block" width="20"
95+
height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
96+
stroke-linecap="round" stroke-linejoin="round">
97+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
98+
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
99+
</svg>
100+
</ng-template>
101+
<ng-template ngSwitchCase="run">
102+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
103+
fill="currentColor">
104+
<path fill-rule="evenodd"
105+
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"
106+
clip-rule="evenodd" />
107+
</svg>
108+
</ng-template>
109+
<ng-template ngSwitchCase="edit-term">
110+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
111+
fill="currentColor">
112+
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
113+
<path fill-rule="evenodd"
114+
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"
115+
clip-rule="evenodd" />
116+
</svg>
117+
</ng-template>
118+
<ng-template ngSwitchCase="remove-term">
119+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
120+
fill="currentColor">
121+
<path fill-rule="evenodd"
122+
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"
123+
clip-rule="evenodd" />
124+
</svg>
125+
</ng-template>
126+
<ng-template ngSwitchCase="whitelist-term">
127+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
128+
fill="currentColor">
129+
<path fill-rule="evenodd"
130+
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"
131+
clip-rule="evenodd" />
132+
</svg>
133+
</ng-template>
134+
<ng-template ngSwitchCase="blacklist-term">
135+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" viewBox="0 0 20 20"
136+
fill="currentColor">
137+
<path fill-rule="evenodd"
138+
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"
139+
clip-rule="evenodd" />
140+
</svg>
141+
</ng-template>
142+
</ng-container>
143+
144+
</ng-template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.tooltip::before {
2+
z-index: 50;
3+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { DropdownItComponent } from './dropdown-it.component';
4+
5+
describe('DropdownItComponent', () => {
6+
let component: DropdownItComponent;
7+
let fixture: ComponentFixture<DropdownItComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [DropdownItComponent]
12+
})
13+
.compileComponents();
14+
});
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(DropdownItComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
2+
import { AbstractControl, FormGroup } from '@angular/forms';
3+
import { DropdownOptions } from './dropdown-it-helper';
4+
5+
@Component({
6+
selector: 'kern-dropdown-it',
7+
templateUrl: './dropdown-it.component.html',
8+
styleUrls: ['./dropdown-it.component.scss']
9+
})
10+
export class DropdownItComponent implements OnChanges {
11+
12+
@Input() dropdownOptions: DropdownOptions;
13+
@Output() optionClicked = new EventEmitter<string | any>();
14+
15+
hasInputErrors: string;
16+
buttonClassList: string;
17+
dropdownClassList: string;
18+
dropdownOptionCaptions: string[];
19+
useValueAsCaption: boolean = false;
20+
21+
constructor() { }
22+
ngOnChanges(changes: SimpleChanges): void {
23+
this.dropdownOptionCaptions = this.getTextArray(this.dropdownOptions.optionArray);
24+
this.runInputChecks();
25+
this.buildHelperValues();
26+
}
27+
28+
private getTextArray(arr: string[] | any[]): string[] {
29+
if (!arr) return [];
30+
if (arr.length == 0) return [];
31+
if (typeof arr[0] == 'string') return arr as string[];
32+
let valueArray = arr;
33+
if (arr[0].value && typeof arr[0].value == 'object') valueArray = arr.map(x => x.getRawValue());
34+
if (valueArray[0].name) return valueArray.map(a => a.name);
35+
if (valueArray[0].text) return valueArray.map(a => a.text);
36+
37+
let firstStringKey = "";
38+
39+
for (const key of Object.keys(valueArray[0])) {
40+
if (typeof valueArray[0][key] == 'string') {
41+
firstStringKey = key;
42+
break;
43+
}
44+
}
45+
if (!firstStringKey) throw new Error("Cant find text in given array - dropdown");
46+
return valueArray.map(a => a[firstStringKey]);
47+
}
48+
49+
private buildHelperValues() {
50+
this.buttonClassList = "";
51+
this.buttonClassList += this.dropdownOptions.isDisabled ? 'opacity-50 cursor-not-allowed' : 'opacity-100 cursor-pointer';
52+
this.buttonClassList += this.dropdownOptions.buttonTooltip ? ' tooltip tooltip-right' : '';
53+
this.dropdownClassList = this.dropdownOptions.hasCheckboxes ? ' w-80' : ' w-auto';
54+
this.buttonClassList += this.dropdownClassList;
55+
}
56+
57+
private runInputChecks() {
58+
this.hasInputErrors = "";
59+
if (!this.dropdownOptions) this.hasInputErrors = "no dropdown options provided\n";
60+
if (!this.dropdownOptions.optionArray || this.dropdownOptions.optionArray.length == 0) this.hasInputErrors = "no text provided\n";
61+
if (!this.dropdownOptions.buttonCaption && this.dropdownOptionCaptions.length > 0) {
62+
this.dropdownOptions.buttonCaption = this.dropdownOptionCaptions[0];
63+
this.useValueAsCaption = true;
64+
}
65+
if (this.dropdownOptions.isOptionDisabled && this.dropdownOptions.isOptionDisabled.length != this.dropdownOptions.optionArray.length) this.hasInputErrors = "array options != isOptionDisabled length\n";
66+
if (this.dropdownOptions.optionIcons && this.dropdownOptions.optionIcons.length != this.dropdownOptions.optionIcons.length) this.hasInputErrors = "array options != optionIcons length\n";
67+
68+
69+
if (this.hasInputErrors) console.log(this.hasInputErrors);
70+
71+
}
72+
73+
toggleVisible(isVisible: boolean, dropdownOptions: HTMLDivElement): void {
74+
if (isVisible) {
75+
dropdownOptions.classList.remove('hidden');
76+
dropdownOptions.classList.add('block');
77+
dropdownOptions.classList.add('z-10');
78+
} else {
79+
dropdownOptions.classList.remove('z-10');
80+
dropdownOptions.classList.remove('block');
81+
dropdownOptions.classList.add('hidden');
82+
}
83+
}
84+
85+
performActionOnOption(event: MouseEvent, clickIndex: number) {
86+
if (this.dropdownOptions.isOptionDisabled?.length && this.dropdownOptions.isOptionDisabled[clickIndex]) return;
87+
if (this.dropdownOptions.keepDropdownOpen) event.stopPropagation();
88+
89+
if (clickIndex >= this.dropdownOptions.optionArray.length) {
90+
console.log("something is wrong in the click action of the dropdown component");
91+
return;
92+
}
93+
94+
if (!this.dropdownOptions.valuePropertyPath) {
95+
if (this.useValueAsCaption) this.dropdownOptions.buttonCaption = this.dropdownOptionCaptions[clickIndex];
96+
if (this.dropdownOptions.hasCheckboxes) this.optionClicked.emit(this.dropdownOptions.optionArray[clickIndex]);
97+
else this.optionClicked.emit(this.dropdownOptionCaptions[clickIndex]);
98+
return;
99+
}
100+
101+
const splittedPath = this.dropdownOptions.valuePropertyPath.split(".");
102+
103+
let tmp = this.dropdownOptions.optionArray[clickIndex];
104+
for (const key of splittedPath) {
105+
tmp = tmp[key];
106+
}
107+
if (typeof tmp != "string") {
108+
console.log("something is wrong in the click action of the dropdown component - property path");
109+
return;
110+
}
111+
if (this.useValueAsCaption) this.dropdownOptions.buttonCaption = tmp;
112+
this.optionClicked.emit(tmp);
113+
114+
}
115+
116+
getActiveNegateGroupColor(group: FormGroup) {
117+
if (!group.get('active').value) return null;
118+
if (group.contains('negate'))
119+
return group.get('negate').value ? '#ef4444' : '#2563eb';
120+
return '#2563eb';
121+
}
122+
123+
124+
getDropdownDisplayText(
125+
formControls: AbstractControl[],
126+
labelFor: string
127+
): string {
128+
let text = '';
129+
let atLeastOneNegated: boolean = false;
130+
for (let c of formControls) {
131+
const hasNegate = Boolean(c.get('negate'));
132+
if (labelFor == 'EMPTY' && c.get('active').value) return '';
133+
else if (
134+
labelFor == 'NOT_NEGATED' &&
135+
c.get('active').value &&
136+
(!hasNegate || (hasNegate && !c.get('negate').value))
137+
) {
138+
text += (text == '' ? '' : ', ') + c.get('name').value;
139+
} else if (
140+
labelFor == 'NEGATED' &&
141+
c.get('active').value &&
142+
hasNegate &&
143+
c.get('negate').value
144+
) {
145+
text += (text == '' ? '' : ', ') + c.get('name').value;
146+
}
147+
if (
148+
!atLeastOneNegated &&
149+
c.get('active').value &&
150+
hasNegate &&
151+
c.get('negate').value
152+
)
153+
atLeastOneNegated = true;
154+
}
155+
if (labelFor == 'EMPTY') return 'None Selected';
156+
157+
if (labelFor == 'NOT_NEGATED' && atLeastOneNegated && text != '')
158+
return text + ', ';
159+
160+
return text;
161+
}
162+
}

0 commit comments

Comments
 (0)