@brickclay-org/ui 0.0.39 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSETS_SETUP.md +59 -0
- package/ng-package.json +29 -0
- package/package.json +15 -26
- package/src/lib/assets/icons.ts +8 -0
- package/src/lib/badge/badge.html +24 -0
- package/src/lib/badge/badge.ts +42 -0
- package/src/lib/brickclay-lib.spec.ts +23 -0
- package/src/lib/brickclay-lib.ts +15 -0
- package/src/lib/button-group/button-group.html +12 -0
- package/src/lib/button-group/button-group.ts +73 -0
- package/src/lib/calender/calendar.module.ts +35 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.css +698 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.html +230 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.spec.ts +23 -0
- package/src/lib/calender/components/custom-calendar/custom-calendar.component.ts +1554 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.css +373 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.html +210 -0
- package/src/lib/calender/components/scheduled-date-picker/scheduled-date-picker.component.ts +361 -0
- package/src/lib/calender/components/time-picker/time-picker.component.css +174 -0
- package/src/lib/calender/components/time-picker/time-picker.component.html +60 -0
- package/src/lib/calender/components/time-picker/time-picker.component.ts +283 -0
- package/src/lib/calender/services/calendar-manager.service.ts +45 -0
- package/src/lib/checkbox/checkbox.html +42 -0
- package/src/lib/checkbox/checkbox.ts +67 -0
- package/src/lib/chips/chips.html +74 -0
- package/src/lib/chips/chips.ts +222 -0
- package/src/lib/grid/components/grid/grid.html +97 -0
- package/src/lib/grid/components/grid/grid.ts +139 -0
- package/src/lib/grid/models/grid.model.ts +20 -0
- package/src/lib/input/input.html +127 -0
- package/src/lib/input/input.ts +394 -0
- package/src/lib/pill/pill.html +24 -0
- package/src/lib/pill/pill.ts +39 -0
- package/src/lib/radio/radio.html +58 -0
- package/src/lib/radio/radio.ts +72 -0
- package/src/lib/select/select.html +111 -0
- package/src/lib/select/select.ts +401 -0
- package/src/lib/spinner/spinner.html +5 -0
- package/src/lib/spinner/spinner.ts +22 -0
- package/src/lib/tabs/tabs.html +28 -0
- package/src/lib/tabs/tabs.ts +48 -0
- package/src/lib/textarea/textarea.html +80 -0
- package/src/lib/textarea/textarea.ts +172 -0
- package/src/lib/toggle/toggle.html +24 -0
- package/src/lib/toggle/toggle.ts +62 -0
- package/src/lib/ui-button/ui-button.html +25 -0
- package/src/lib/ui-button/ui-button.ts +55 -0
- package/src/lib/ui-icon-button/ui-icon-button.html +7 -0
- package/src/lib/ui-icon-button/ui-icon-button.ts +38 -0
- package/src/public-api.ts +43 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +15 -0
- package/fesm2022/brickclay-org-ui.mjs +0 -4035
- package/fesm2022/brickclay-org-ui.mjs.map +0 -1
- package/index.d.ts +0 -857
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { BrickclayIcons } from '../../../assets/icons';
|
|
2
|
+
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, AfterViewInit, QueryList, ViewChildren, ElementRef, HostListener, SimpleChanges } from '@angular/core';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'bk-time-picker',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
templateUrl: './time-picker.component.html',
|
|
10
|
+
styleUrls: ['./time-picker.component.css']
|
|
11
|
+
})
|
|
12
|
+
export class BkTimePicker implements OnInit, OnChanges, AfterViewInit {
|
|
13
|
+
@Input() value: string = '1:00 AM'; // Time in format "H:MM AM/PM"
|
|
14
|
+
@Input() label: string = 'Time';
|
|
15
|
+
@Input() placeholder: string = 'Select time';
|
|
16
|
+
@Input() position: 'left' | 'right' = 'left';
|
|
17
|
+
@Input() pickerId: string = ''; // Unique ID for this picker
|
|
18
|
+
@Input() closePicker: number = 0; // Close counter from parent (triggers close when changed)
|
|
19
|
+
@Input() timeFormat: 12 | 24 = 12; // Visual mode: 12h or 24h
|
|
20
|
+
@Input() showSeconds = false; // Whether to show/edit seconds
|
|
21
|
+
@Output() timeChange = new EventEmitter<string>();
|
|
22
|
+
@Output() pickerOpened = new EventEmitter<string>(); // Notify parent when opened
|
|
23
|
+
@Output() pickerClosed = new EventEmitter<string>(); // Notify parent when closed
|
|
24
|
+
|
|
25
|
+
@ViewChildren('timeScroll') timeScrollElements!: QueryList<ElementRef>;
|
|
26
|
+
|
|
27
|
+
showPicker = false;
|
|
28
|
+
currentHour = 1;
|
|
29
|
+
currentMinute = 0;
|
|
30
|
+
currentAMPM = 'AM';
|
|
31
|
+
currentSecond = 0;
|
|
32
|
+
|
|
33
|
+
brickclayIcons=BrickclayIcons;
|
|
34
|
+
|
|
35
|
+
ngOnInit() {
|
|
36
|
+
this.parseTimeValue();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ngAfterViewInit() {
|
|
40
|
+
if (this.showPicker) {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
this.scrollToSelectedTimes();
|
|
43
|
+
}, 100);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
parseTimeValue() {
|
|
48
|
+
const parsed = this.parseTimeStringToComponents(this.value);
|
|
49
|
+
this.currentHour = parsed.hour;
|
|
50
|
+
this.currentMinute = parsed.minute;
|
|
51
|
+
this.currentSecond = parsed.second;
|
|
52
|
+
this.currentAMPM = parsed.ampm;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getHours(): number[] {
|
|
56
|
+
// 12-hour: 1-12, 24-hour: 0-23
|
|
57
|
+
if (this.timeFormat === 24) {
|
|
58
|
+
return Array.from({ length: 24 }, (_, i) => i);
|
|
59
|
+
}
|
|
60
|
+
return Array.from({ length: 12 }, (_, i) => i + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getMinutes(): number[] {
|
|
64
|
+
return Array.from({ length: 60 }, (_, i) => i);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getSeconds(): number[] {
|
|
68
|
+
return Array.from({ length: 60 }, (_, i) => i);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getAMPMOptions(): string[] {
|
|
72
|
+
return ['AM', 'PM'];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parseTimeStringToComponents(timeStr: string): { hour: number; minute: number; second: number; ampm: string } {
|
|
76
|
+
// Supports:
|
|
77
|
+
// - "H:MM AM/PM"
|
|
78
|
+
// - "H:MM:SS AM/PM"
|
|
79
|
+
// - "HH:MM" (24h)
|
|
80
|
+
// - "HH:MM:SS" (24h)
|
|
81
|
+
if (!timeStr) {
|
|
82
|
+
return {
|
|
83
|
+
hour: this.timeFormat === 24 ? 0 : 12,
|
|
84
|
+
minute: 0,
|
|
85
|
+
second: 0,
|
|
86
|
+
ampm: this.timeFormat === 24 ? '' : 'AM'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parts = timeStr.trim().split(' ');
|
|
91
|
+
const timePart = parts[0] || (this.timeFormat === 24 ? '00:00' : '12:00');
|
|
92
|
+
let ampm = (parts[1] || '').toUpperCase();
|
|
93
|
+
|
|
94
|
+
const [hoursStr, minutesStr, secondsStr] = timePart.split(':');
|
|
95
|
+
let hour = parseInt(hoursStr || (this.timeFormat === 24 ? '0' : '12'), 10);
|
|
96
|
+
const minute = parseInt(minutesStr || '0', 10);
|
|
97
|
+
const second = parseInt(secondsStr || '0', 10);
|
|
98
|
+
|
|
99
|
+
if (this.timeFormat === 24) {
|
|
100
|
+
// In 24-hour mode we ignore AM/PM and keep hour as 0-23
|
|
101
|
+
return {
|
|
102
|
+
hour: isNaN(hour) ? 0 : Math.min(Math.max(hour, 0), 23),
|
|
103
|
+
minute: isNaN(minute) ? 0 : Math.min(Math.max(minute, 0), 59),
|
|
104
|
+
second: isNaN(second) ? 0 : Math.min(Math.max(second, 0), 59),
|
|
105
|
+
ampm: ''
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 12-hour mode: normalize AM/PM and convert 24h inputs if needed
|
|
110
|
+
let ampmValue = ampm === 'PM' || ampm === 'AM' ? ampm : '';
|
|
111
|
+
if (!ampmValue) {
|
|
112
|
+
// No AM/PM provided -> interpret as 24-hour and convert to 12-hour with AM/PM
|
|
113
|
+
if (hour >= 12) {
|
|
114
|
+
ampmValue = 'PM';
|
|
115
|
+
if (hour > 12) hour -= 12;
|
|
116
|
+
} else {
|
|
117
|
+
ampmValue = 'AM';
|
|
118
|
+
if (hour === 0) hour = 12;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clamp to 1-12 range
|
|
123
|
+
if (hour < 1) hour = 1;
|
|
124
|
+
if (hour > 12) hour = 12;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
hour,
|
|
128
|
+
minute: isNaN(minute) ? 0 : Math.min(Math.max(minute, 0), 59),
|
|
129
|
+
second: isNaN(second) ? 0 : Math.min(Math.max(second, 0), 59),
|
|
130
|
+
ampm: ampmValue
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
formatTimeFromComponents(hour: number, minute: number, second: number, ampm: string): string {
|
|
135
|
+
const hStr = hour.toString().padStart(2, '0');
|
|
136
|
+
const minuteStr = minute.toString().padStart(2, '0');
|
|
137
|
+
const secondStr = second.toString().padStart(2, '0');
|
|
138
|
+
|
|
139
|
+
if (this.timeFormat === 24) {
|
|
140
|
+
// "HH:mm" or "HH:mm:ss"
|
|
141
|
+
return this.showSeconds
|
|
142
|
+
? `${hStr}:${minuteStr}:${secondStr}`
|
|
143
|
+
: `${hStr}:${minuteStr}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 12-hour: "H:MM" or "H:MM:SS" with AM/PM
|
|
147
|
+
const displayHour = hour; // already 1-12
|
|
148
|
+
return this.showSeconds
|
|
149
|
+
? `${displayHour}:${minuteStr}:${secondStr} ${ampm}`
|
|
150
|
+
: `${displayHour}:${minuteStr} ${ampm}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
togglePicker() {
|
|
154
|
+
if (!this.showPicker) {
|
|
155
|
+
this.showPicker = true;
|
|
156
|
+
this.parseTimeValue();
|
|
157
|
+
this.pickerOpened.emit(this.pickerId);
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
this.scrollToSelectedTimes();
|
|
160
|
+
}, 100);
|
|
161
|
+
} else {
|
|
162
|
+
this.showPicker = false;
|
|
163
|
+
this.pickerClosed.emit(this.pickerId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
onHourChange(hour: number) {
|
|
168
|
+
this.currentHour = hour;
|
|
169
|
+
this.updateTime();
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
this.scrollToSelectedTimes();
|
|
172
|
+
}, 50);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onMinuteChange(minute: number) {
|
|
176
|
+
this.currentMinute = minute;
|
|
177
|
+
this.updateTime();
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
this.scrollToSelectedTimes();
|
|
180
|
+
}, 50);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onSecondChange(second: number) {
|
|
184
|
+
this.currentSecond = second;
|
|
185
|
+
this.updateTime();
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
this.scrollToSelectedTimes();
|
|
188
|
+
}, 50);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onAMPMChange(ampm: string) {
|
|
192
|
+
this.currentAMPM = ampm;
|
|
193
|
+
this.updateTime();
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
this.scrollToSelectedTimes();
|
|
196
|
+
}, 50);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
updateTime() {
|
|
200
|
+
const newTime = this.formatTimeFromComponents(
|
|
201
|
+
this.currentHour,
|
|
202
|
+
this.currentMinute,
|
|
203
|
+
this.currentSecond,
|
|
204
|
+
this.currentAMPM
|
|
205
|
+
);
|
|
206
|
+
this.value = newTime;
|
|
207
|
+
this.timeChange.emit(newTime);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
scrollToSelectedTimes() {
|
|
211
|
+
this.timeScrollElements.forEach((elementRef) => {
|
|
212
|
+
const element = elementRef.nativeElement;
|
|
213
|
+
const selectedItem = element.querySelector('.time-item.selected');
|
|
214
|
+
if (selectedItem) {
|
|
215
|
+
const scrollTop = selectedItem.offsetTop - element.offsetHeight / 40 + selectedItem.offsetHeight / 40;
|
|
216
|
+
element.scrollTop = scrollTop;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@HostListener('document:click', ['$event'])
|
|
222
|
+
onDocumentClick(event: MouseEvent) {
|
|
223
|
+
const target = event.target as HTMLElement;
|
|
224
|
+
if (!target.closest('.time-picker-wrapper') && this.showPicker) {
|
|
225
|
+
this.showPicker = false;
|
|
226
|
+
this.pickerClosed.emit(this.pickerId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private previousCloseCounter: number = 0;
|
|
231
|
+
|
|
232
|
+
ngOnChanges(changes: SimpleChanges) {
|
|
233
|
+
if (changes['value'] && changes['value'].currentValue) {
|
|
234
|
+
this.parseTimeValue();
|
|
235
|
+
}
|
|
236
|
+
if (changes['closePicker'] && this.showPicker) {
|
|
237
|
+
const newCounter = changes['closePicker'].currentValue;
|
|
238
|
+
// If counter increased, close the picker
|
|
239
|
+
if (newCounter > this.previousCloseCounter) {
|
|
240
|
+
this.showPicker = false;
|
|
241
|
+
this.pickerClosed.emit(this.pickerId);
|
|
242
|
+
this.previousCloseCounter = newCounter;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Basic keyboard support on the input (combobox behavior)
|
|
248
|
+
onInputKeydown(event: KeyboardEvent) {
|
|
249
|
+
const key = event.key;
|
|
250
|
+
|
|
251
|
+
if (key === 'Enter' || key === ' ') {
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
this.togglePicker();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (key === 'Escape' && this.showPicker) {
|
|
258
|
+
this.showPicker = false;
|
|
259
|
+
this.pickerClosed.emit(this.pickerId);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Simple hour increment/decrement when closed
|
|
264
|
+
if (!this.showPicker && (key === 'ArrowUp' || key === 'ArrowDown')) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
if (this.timeFormat === 24) {
|
|
267
|
+
if (key === 'ArrowUp') {
|
|
268
|
+
this.currentHour = (this.currentHour + 1) % 24;
|
|
269
|
+
} else {
|
|
270
|
+
this.currentHour = this.currentHour <= 0 ? 23 : this.currentHour - 1;
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
if (key === 'ArrowUp') {
|
|
274
|
+
this.currentHour = this.currentHour >= 12 ? 1 : this.currentHour + 1;
|
|
275
|
+
} else {
|
|
276
|
+
this.currentHour = this.currentHour <= 1 ? 12 : this.currentHour - 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.updateTime();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
@Injectable({
|
|
5
|
+
providedIn: 'root'
|
|
6
|
+
})
|
|
7
|
+
export class CalendarManagerService {
|
|
8
|
+
private calendarInstances: Set<() => void> = new Set();
|
|
9
|
+
private closeAllSubject = new Subject<void>();
|
|
10
|
+
|
|
11
|
+
closeAll$ = this.closeAllSubject.asObservable();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a calendar instance with its close function
|
|
15
|
+
*/
|
|
16
|
+
register(closeFn: () => void): () => void {
|
|
17
|
+
this.calendarInstances.add(closeFn);
|
|
18
|
+
|
|
19
|
+
// Return unregister function
|
|
20
|
+
return () => {
|
|
21
|
+
this.calendarInstances.delete(closeFn);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Close all calendars except the one being opened
|
|
27
|
+
*/
|
|
28
|
+
closeAllExcept(exceptCloseFn: () => void): void {
|
|
29
|
+
this.calendarInstances.forEach(closeFn => {
|
|
30
|
+
if (closeFn !== exceptCloseFn) {
|
|
31
|
+
closeFn();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Close all calendars
|
|
38
|
+
*/
|
|
39
|
+
closeAll(): void {
|
|
40
|
+
this.closeAllSubject.next();
|
|
41
|
+
this.calendarInstances.forEach(closeFn => closeFn());
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="inline-flex items-center gap-2 cursor-pointer group outline-none"
|
|
3
|
+
(click)="toggle()"
|
|
4
|
+
(keydown.enter)="toggle()"
|
|
5
|
+
(keydown.space)="$event.preventDefault(); toggle()"
|
|
6
|
+
tabindex="0"
|
|
7
|
+
[attr.aria-disabled]="disabled">
|
|
8
|
+
<div
|
|
9
|
+
class="relative flex items-center justify-center border-2 transition-all duration-200 ease-in-out rounded group-focus-visible:ring-2 group-focus-visible:ring-blue-600 group-focus-visible:ring-offset-2 checkbox"
|
|
10
|
+
[ngClass]="[
|
|
11
|
+
checkboxClass,
|
|
12
|
+
isChecked && !disabled ? 'bg-black border-black' : '',
|
|
13
|
+
!isChecked && !disabled ? 'bg-white border-gray-300 group-hover:border-gray-400' : '',
|
|
14
|
+
disabled && isChecked ? 'bg-gray-300 border-gray-300' : '',
|
|
15
|
+
disabled && !isChecked ? 'bg-gray-100 border-gray-200' : '',
|
|
16
|
+
disabled ? 'cursor-not-allowed' : ''
|
|
17
|
+
]"
|
|
18
|
+
>
|
|
19
|
+
<svg
|
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
stroke-width="3.5"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
class="text-white pointer-events-none transition-opacity duration-200"
|
|
28
|
+
[class.opacity-0]="!isChecked"
|
|
29
|
+
[class.opacity-100]="isChecked"
|
|
30
|
+
>
|
|
31
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
@if(label){
|
|
35
|
+
<span
|
|
36
|
+
[ngClass]="disabled ? 'text-gray-400' : ''"
|
|
37
|
+
class="font-medium text-xs text-[#1B223A] select-none {{labelClass}}"
|
|
38
|
+
>
|
|
39
|
+
{{ label }}
|
|
40
|
+
</span>
|
|
41
|
+
}
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, forwardRef, ViewEncapsulation } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'bk-checkbox',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
encapsulation: ViewEncapsulation.None,
|
|
10
|
+
templateUrl: './checkbox.html',
|
|
11
|
+
styleUrls: ['./checkbox.css'],
|
|
12
|
+
providers: [
|
|
13
|
+
{
|
|
14
|
+
provide: NG_VALUE_ACCESSOR,
|
|
15
|
+
useExisting: forwardRef(() => BkCheckbox),
|
|
16
|
+
multi: true
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
export class BkCheckbox implements ControlValueAccessor {
|
|
21
|
+
|
|
22
|
+
@Input() checkboxClass: string = '';
|
|
23
|
+
@Input() label: string = '';
|
|
24
|
+
@Input() labelClass: string = '';
|
|
25
|
+
@Input() disabled: boolean = false;
|
|
26
|
+
|
|
27
|
+
@Output() change = new EventEmitter<boolean>();
|
|
28
|
+
|
|
29
|
+
// This is the value bound via ngModel
|
|
30
|
+
isChecked: boolean = false;
|
|
31
|
+
|
|
32
|
+
// ControlValueAccessor callbacks
|
|
33
|
+
private onChange = (_: any) => {};
|
|
34
|
+
private onTouched = () => {};
|
|
35
|
+
|
|
36
|
+
// Toggle function for click / keyboard
|
|
37
|
+
toggle() {
|
|
38
|
+
if (this.disabled) return;
|
|
39
|
+
|
|
40
|
+
this.isChecked = !this.isChecked;
|
|
41
|
+
|
|
42
|
+
// Update ngModel value
|
|
43
|
+
this.onChange(this.isChecked);
|
|
44
|
+
this.onTouched();
|
|
45
|
+
|
|
46
|
+
// Emit the change event
|
|
47
|
+
this.change.emit(this.isChecked);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** ------------------ ControlValueAccessor methods ------------------ */
|
|
51
|
+
|
|
52
|
+
writeValue(value: boolean): void {
|
|
53
|
+
this.isChecked = value ?? false; // handle null/undefined safely
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
registerOnChange(fn: any): void {
|
|
57
|
+
this.onChange = fn;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerOnTouched(fn: any): void {
|
|
61
|
+
this.onTouched = fn;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setDisabledState(isDisabled: boolean): void {
|
|
65
|
+
this.disabled = isDisabled;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="input-badge-container">
|
|
2
|
+
@if (label) {
|
|
3
|
+
<label
|
|
4
|
+
[for]="id"
|
|
5
|
+
class="input-badge-label">
|
|
6
|
+
{{ label }}
|
|
7
|
+
@if (required) {
|
|
8
|
+
<span class="input-badge-label-required">*</span>
|
|
9
|
+
}
|
|
10
|
+
</label>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
<div class="input-badge-wrapper">
|
|
14
|
+
<div
|
|
15
|
+
#fieldWrapper
|
|
16
|
+
class="input-badge-field-wrapper"
|
|
17
|
+
[ngClass]="{
|
|
18
|
+
'input-badge-field-wrapper--default': inputState === 'default',
|
|
19
|
+
'input-badge-field-wrapper--focused': inputState === 'focused',
|
|
20
|
+
'input-badge-field-wrapper--filled': inputState === 'filled',
|
|
21
|
+
'input-badge-field-wrapper--error': inputState === 'error',
|
|
22
|
+
'input-badge-field-wrapper--disabled': inputState === 'disabled',
|
|
23
|
+
'input-badge-field-wrapper--scrollable': needsScroll
|
|
24
|
+
}"
|
|
25
|
+
(click)="focusInput()"
|
|
26
|
+
>
|
|
27
|
+
<!-- Badges -->
|
|
28
|
+
@for (badge of badges; track badge; let i = $index) {
|
|
29
|
+
<div class="input-badge-item">
|
|
30
|
+
<span class="input-badge-item-text">{{ badge }}</span>
|
|
31
|
+
@if (!disabled) {
|
|
32
|
+
<button type="button" (click)="removeBadge(i); $event.stopPropagation()"class="input-badge-item-close" >
|
|
33
|
+
<img src="../../../../assets/images/icons/global/badge-close.svg" alt="Remove" class="input-badge-item-close-icon"/>
|
|
34
|
+
</button>
|
|
35
|
+
}
|
|
36
|
+
</div>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
<!-- Input Field -->
|
|
40
|
+
<input
|
|
41
|
+
#badgeInput
|
|
42
|
+
type="text"
|
|
43
|
+
[id]="id"
|
|
44
|
+
[name]="name"
|
|
45
|
+
[value]="inputValue"
|
|
46
|
+
(keydown)="onKeyDown($event)"
|
|
47
|
+
(input)="handleInput($event)"
|
|
48
|
+
(focus)="handleFocus($event)"
|
|
49
|
+
(blur)="handleBlur($event)"
|
|
50
|
+
[placeholder]="badges.length === 0 ? placeholder : ''"
|
|
51
|
+
[disabled]="disabled"
|
|
52
|
+
[readOnly]="readOnly"
|
|
53
|
+
class="input-badge-input"
|
|
54
|
+
[ngClass]="{
|
|
55
|
+
'input-badge-input--default': inputState === 'default',
|
|
56
|
+
'input-badge-input--focused': inputState === 'focused',
|
|
57
|
+
'input-badge-input--filled': inputState === 'filled',
|
|
58
|
+
'input-badge-input--error': inputState === 'error',
|
|
59
|
+
'input-badge-input--disabled': inputState === 'disabled'
|
|
60
|
+
}"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="input-badge-footer">
|
|
66
|
+
<div class="input-badge-footer-content">
|
|
67
|
+
@if (hasError) {
|
|
68
|
+
<span class="input-badge-error"> {{ errorMessage }}</span>
|
|
69
|
+
} @else if (hint) {
|
|
70
|
+
<span class="input-badge-hint"> {{ hint }}</span>
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|