@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,222 @@
|
|
|
1
|
+
import { Component, Input, forwardRef, ViewChild, ElementRef, AfterViewInit, EventEmitter, Output } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'bk-chips',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule, FormsModule],
|
|
9
|
+
templateUrl: './chips.html',
|
|
10
|
+
styleUrl: './chips.css',
|
|
11
|
+
providers: [
|
|
12
|
+
{
|
|
13
|
+
provide: NG_VALUE_ACCESSOR,
|
|
14
|
+
useExisting: forwardRef(() => BkChips),
|
|
15
|
+
multi: true
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
})
|
|
19
|
+
export class BkChips implements ControlValueAccessor, AfterViewInit {
|
|
20
|
+
@ViewChild('badgeInput') badgeInput!: ElementRef<HTMLInputElement>;
|
|
21
|
+
@ViewChild('fieldWrapper') fieldWrapper!: ElementRef<HTMLDivElement>;
|
|
22
|
+
|
|
23
|
+
// --- Configuration Inputs ---
|
|
24
|
+
@Input() id!: string ;
|
|
25
|
+
@Input() name!: string ;
|
|
26
|
+
@Input() label: string = '';
|
|
27
|
+
@Input() placeholder: string = '';
|
|
28
|
+
@Input() hint: string = '';
|
|
29
|
+
@Input() required: boolean = false;
|
|
30
|
+
@Input() disabled: boolean = false;
|
|
31
|
+
@Input() readOnly: boolean = false;
|
|
32
|
+
/**
|
|
33
|
+
* If true, displays the component in an error state (red border).
|
|
34
|
+
* It also replaces the hint text with the error message.
|
|
35
|
+
*/
|
|
36
|
+
@Input() hasError: boolean = false;
|
|
37
|
+
@Input() errorMessage: string = 'This is a error message';
|
|
38
|
+
// =================== Output Emitter ===================
|
|
39
|
+
@Output() input = new EventEmitter<Event>();
|
|
40
|
+
@Output() change = new EventEmitter<string[]>();
|
|
41
|
+
|
|
42
|
+
@Output() focus = new EventEmitter<Event>();
|
|
43
|
+
@Output() blur = new EventEmitter<Event>();
|
|
44
|
+
// --- State Properties ---
|
|
45
|
+
badges: string[] = [];
|
|
46
|
+
inputValue: string = '';
|
|
47
|
+
isFocused: boolean = false;
|
|
48
|
+
needsScroll: boolean = false;
|
|
49
|
+
|
|
50
|
+
// --- ControlValueAccessor Methods ---
|
|
51
|
+
onChange: any = () => {};
|
|
52
|
+
onTouched: any = () => {};
|
|
53
|
+
|
|
54
|
+
// Get input state for styling variants
|
|
55
|
+
get inputState(): 'default' | 'focused' | 'filled' | 'error' | 'disabled' {
|
|
56
|
+
if (this.disabled) return 'disabled';
|
|
57
|
+
if (this.hasError) return 'error';
|
|
58
|
+
if (this.isFocused) return 'focused';
|
|
59
|
+
if (this.badges.length > 0 || this.inputValue.length > 0) return 'filled';
|
|
60
|
+
return 'default';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// Handle keydown events (Enter to add badge, Backspace to remove)
|
|
66
|
+
onKeyDown(event: KeyboardEvent): void {
|
|
67
|
+
if (this.disabled) return;
|
|
68
|
+
|
|
69
|
+
// Add badge on Enter or comma
|
|
70
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
this.addBadge();
|
|
73
|
+
}
|
|
74
|
+
// Remove last badge on Backspace when input is empty
|
|
75
|
+
else if (event.key === 'Backspace' && this.inputValue === '' && this.badges.length > 0) {
|
|
76
|
+
this.removeBadge(this.badges.length - 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add a badge from the current input value
|
|
81
|
+
addBadge(): void {
|
|
82
|
+
const trimmedValue = this.inputValue.trim();
|
|
83
|
+
if (trimmedValue && !this.badges.includes(trimmedValue)) {
|
|
84
|
+
this.badges.push(trimmedValue);
|
|
85
|
+
this.inputValue = '';
|
|
86
|
+
this.updateValue();
|
|
87
|
+
// Check if scroll is needed after adding badge (with delay for DOM update)
|
|
88
|
+
setTimeout(() => this.checkScrollNeeded(), 10);
|
|
89
|
+
} else if (trimmedValue && this.badges.includes(trimmedValue)) {
|
|
90
|
+
// Optionally show a message that badge already exists
|
|
91
|
+
this.inputValue = '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Remove a badge at the given index
|
|
96
|
+
removeBadge(index: number): void {
|
|
97
|
+
if (this.disabled) return;
|
|
98
|
+
this.badges.splice(index, 1);
|
|
99
|
+
this.updateValue();
|
|
100
|
+
// Check if scroll is needed after removing badge (with delay for DOM update)
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
this.checkScrollNeeded();
|
|
103
|
+
if (this.badgeInput) {
|
|
104
|
+
this.badgeInput.nativeElement.focus();
|
|
105
|
+
}
|
|
106
|
+
}, 10);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if scrolling is needed (when content wraps)
|
|
110
|
+
checkScrollNeeded(): void {
|
|
111
|
+
if (this.fieldWrapper && this.fieldWrapper.nativeElement) {
|
|
112
|
+
const wrapper = this.fieldWrapper.nativeElement;
|
|
113
|
+
|
|
114
|
+
// Get all badge items
|
|
115
|
+
const badgeItems = wrapper.querySelectorAll('.input-badge-item');
|
|
116
|
+
|
|
117
|
+
if (badgeItems.length === 0) {
|
|
118
|
+
// No badges, no scroll needed
|
|
119
|
+
this.needsScroll = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get the first badge's top position
|
|
124
|
+
const firstBadge = badgeItems[0] as HTMLElement;
|
|
125
|
+
const firstBadgeTop = firstBadge.offsetTop;
|
|
126
|
+
|
|
127
|
+
// Check if any badge is on a different line (different top position)
|
|
128
|
+
let hasWrapped = false;
|
|
129
|
+
for (let i = 1; i < badgeItems.length; i++) {
|
|
130
|
+
const badge = badgeItems[i] as HTMLElement;
|
|
131
|
+
// If a badge's top position is different, it means it wrapped to a new line
|
|
132
|
+
if (Math.abs(badge.offsetTop - firstBadgeTop) > 5) {
|
|
133
|
+
hasWrapped = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Also check if the input field is on a different line
|
|
139
|
+
if (!hasWrapped && this.badgeInput && this.badgeInput.nativeElement) {
|
|
140
|
+
const inputTop = this.badgeInput.nativeElement.offsetTop;
|
|
141
|
+
if (Math.abs(inputTop - firstBadgeTop) > 5) {
|
|
142
|
+
hasWrapped = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Only enable scroll if content actually wrapped
|
|
147
|
+
this.needsScroll = hasWrapped;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update the value and notify Angular Forms
|
|
152
|
+
updateValue(): void {
|
|
153
|
+
this.onChange([...this.badges]);
|
|
154
|
+
this.change.emit([...this.badges]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Focus the input field
|
|
158
|
+
focusInput(): void {
|
|
159
|
+
if (!this.disabled && this.badgeInput) {
|
|
160
|
+
this.badgeInput.nativeElement.focus();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ngAfterViewInit(): void {
|
|
165
|
+
// Check scroll after view initializes
|
|
166
|
+
setTimeout(() => this.checkScrollNeeded(), 10);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Called when Angular writes a value TO the component (e.g. initial value)
|
|
170
|
+
writeValue(value: any): void {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
this.badges = [...value];
|
|
173
|
+
} else {
|
|
174
|
+
this.badges = [];
|
|
175
|
+
}
|
|
176
|
+
this.inputValue = '';
|
|
177
|
+
// Check scroll after value is written (with delay for DOM update)
|
|
178
|
+
setTimeout(() => this.checkScrollNeeded(), 10);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Register function to call when value changes
|
|
182
|
+
registerOnChange(fn: any): void {
|
|
183
|
+
this.onChange = fn;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Register function to call when component is touched/blurred
|
|
187
|
+
registerOnTouched(fn: any): void {
|
|
188
|
+
this.onTouched = fn;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Called when the component is disabled via the form control
|
|
192
|
+
setDisabledState(disabled: boolean): void {
|
|
193
|
+
this.disabled = disabled;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =================== Event Handlers ===================
|
|
197
|
+
|
|
198
|
+
// Called when the value in the UI changes (user types)
|
|
199
|
+
handleInput(event: Event): void {
|
|
200
|
+
const input = event.target as HTMLInputElement;
|
|
201
|
+
this.inputValue = input.value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
handleFocus(event: Event): void {
|
|
206
|
+
if (!this.disabled) {
|
|
207
|
+
this.isFocused = true;
|
|
208
|
+
}
|
|
209
|
+
this.onChange([...this.badges]);
|
|
210
|
+
this.focus.emit(event);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
handleBlur(event:Event): void {
|
|
214
|
+
this.isFocused = false;
|
|
215
|
+
this.onTouched();
|
|
216
|
+
this.onChange([...this.badges]);
|
|
217
|
+
this.blur.emit(event);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<div
|
|
2
|
+
#tableScrollContainer
|
|
3
|
+
cdkScrollable
|
|
4
|
+
class="h-[calc(100vh-260px)] overflow-y-auto">
|
|
5
|
+
|
|
6
|
+
<table class="min-w-full text-sm text-left text-gray-800 table-auto border-collapse">
|
|
7
|
+
|
|
8
|
+
<!-- ================= HEADER ================= -->
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
@for (col of columns; track col.header;let i=$index;) {
|
|
12
|
+
@if (isColumnVisible(col)) {
|
|
13
|
+
<th
|
|
14
|
+
class="grid-header sticky top-0 cursor-pointer"
|
|
15
|
+
[class.action-sticky]="col.sticky"
|
|
16
|
+
[class.z-10]="col.sticky"
|
|
17
|
+
[ngClass]="col.headerClass"
|
|
18
|
+
[ngClass]="col.cellClass"
|
|
19
|
+
(click)="sort(col,i)"
|
|
20
|
+
>
|
|
21
|
+
<span class="flex items-center gap-1"
|
|
22
|
+
[ngClass]="sortColumn === col.field
|
|
23
|
+
? (sortDirection === 'asc' ? 'grid-asc' : 'grid-desc')
|
|
24
|
+
: ''">
|
|
25
|
+
{{ col.header }}
|
|
26
|
+
@if (col.sortable) {
|
|
27
|
+
<span class="grid-sort-icon"></span>
|
|
28
|
+
}
|
|
29
|
+
</span>
|
|
30
|
+
</th>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@if (actions.length) {
|
|
35
|
+
<th class="grid-header sticky top-0 action-sticky z-10 !bg-[#FBFBFC] w-20">
|
|
36
|
+
Action
|
|
37
|
+
</th>
|
|
38
|
+
}
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
|
|
42
|
+
<!-- ================= BODY ================= -->
|
|
43
|
+
<tbody
|
|
44
|
+
cdkDropList
|
|
45
|
+
[cdkDropListDisabled]="!draggable"
|
|
46
|
+
[cdkDropListData]="result || []"
|
|
47
|
+
(cdkDropListDropped)="dropList($event)">
|
|
48
|
+
|
|
49
|
+
@for (row of result; track row; let rowIndex = $index) {
|
|
50
|
+
<tr
|
|
51
|
+
cdkDrag
|
|
52
|
+
cdkDragLockAxis="y"
|
|
53
|
+
[cdkDragDisabled]="!draggable"
|
|
54
|
+
(cdkDragStarted)="onDragStart($event)"
|
|
55
|
+
(cdkDragMoved)="onDragMoved($event)"
|
|
56
|
+
class="" [ngClass]="{ 'cursor-move ': draggable }">
|
|
57
|
+
|
|
58
|
+
@for (col of columns; track col.header; let colIndex = $index) {
|
|
59
|
+
@if (isColumnVisible(col)) {
|
|
60
|
+
<td class="grid-cell text-nowrap" [ngClass]="col.cellClass">
|
|
61
|
+
@if (draggable && colIndex === firstVisibleColumnIndex) {
|
|
62
|
+
<span cdkDragHandle class="mr-2 text-gray-400" [ngClass]="{ 'cursor-move': draggable }">☰</span>
|
|
63
|
+
}
|
|
64
|
+
{{ getCellValue(row, col) }}
|
|
65
|
+
</td>
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@if (actions.length) {
|
|
70
|
+
<td class="grid-cell action-sticky text-center">
|
|
71
|
+
<div class="flex items-center justify-center gap-1.5">
|
|
72
|
+
@for (action of actions; track action.name) {
|
|
73
|
+
@if (action.hasPermission) {
|
|
74
|
+
<!-- appTooltip="{{ action.tooltip }}"
|
|
75
|
+
appTooltipPosition="top" -->
|
|
76
|
+
<button
|
|
77
|
+
class="size-6 flex items-center justify-center rounded hover:bg-[#F8F8FA]"
|
|
78
|
+
(click)="emitAction(action, row)"
|
|
79
|
+
|
|
80
|
+
>
|
|
81
|
+
<img
|
|
82
|
+
[src]="action.icon"
|
|
83
|
+
width="14"
|
|
84
|
+
height="14"
|
|
85
|
+
alt="action-icon"
|
|
86
|
+
/>
|
|
87
|
+
</button>
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</div>
|
|
91
|
+
</td>
|
|
92
|
+
}
|
|
93
|
+
</tr>
|
|
94
|
+
}
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
|
2
|
+
import { TableColumn, TableAction } from '../../models/grid.model';
|
|
3
|
+
import { CdkDragDrop, moveItemInArray, CdkDragMove, CdkDragStart, DragDropModule } from '@angular/cdk/drag-drop';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { ScrollingModule } from '@angular/cdk/scrolling';
|
|
6
|
+
export type SortDirection = 'asc' | 'desc';
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'bk-grid',
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [CommonModule,DragDropModule,ScrollingModule],
|
|
11
|
+
templateUrl: './grid.html',
|
|
12
|
+
styleUrl: './grid.css',
|
|
13
|
+
})
|
|
14
|
+
export class BkGrid<T = any> {
|
|
15
|
+
@Input() draggable: boolean = false;
|
|
16
|
+
@Input() columns: TableColumn<T>[] = [];
|
|
17
|
+
@Input() result!: T[];
|
|
18
|
+
@Input() actions: TableAction[] = [];
|
|
19
|
+
|
|
20
|
+
@Output() actionClick = new EventEmitter<{
|
|
21
|
+
action: string;
|
|
22
|
+
row: T;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
@Output() sortChange = new EventEmitter<{
|
|
26
|
+
columnIndex: number;
|
|
27
|
+
column: TableColumn<T>;
|
|
28
|
+
direction: SortDirection;
|
|
29
|
+
}>();
|
|
30
|
+
@Output() dragDropChange = new EventEmitter<T[]>();
|
|
31
|
+
sortColumn?: keyof T;
|
|
32
|
+
sortDirection:SortDirection = 'asc';
|
|
33
|
+
|
|
34
|
+
@ViewChild('tableScrollContainer', { static: false })
|
|
35
|
+
tableScrollContainer!: ElementRef<HTMLDivElement>;
|
|
36
|
+
|
|
37
|
+
get firstVisibleColumnIndex(): number {
|
|
38
|
+
const index = this.columns.findIndex(col => col.visible !== false);
|
|
39
|
+
return index >= 0 ? index : 0;
|
|
40
|
+
}
|
|
41
|
+
/* ---------- Sorting ---------- */
|
|
42
|
+
sort(column: TableColumn<T>, index: number) {
|
|
43
|
+
if (!column.sortable || !column.field) return;
|
|
44
|
+
|
|
45
|
+
// Toggle sort direction
|
|
46
|
+
this.sortDirection =
|
|
47
|
+
this.sortColumn === column.field ? (this.sortDirection === 'asc' ? 'desc' : 'asc') : 'asc';
|
|
48
|
+
|
|
49
|
+
this.sortColumn = column.field;
|
|
50
|
+
|
|
51
|
+
// Emit sort change separately
|
|
52
|
+
this.sortChange.emit({
|
|
53
|
+
columnIndex: index,
|
|
54
|
+
column,
|
|
55
|
+
direction: this.sortDirection,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ---------- Visibility ---------- */
|
|
60
|
+
isColumnVisible(column: TableColumn<T>): boolean {
|
|
61
|
+
return column.visible !== false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---------- Cell Value ---------- */
|
|
65
|
+
getCellValue(row: T, column: TableColumn<T>): string {
|
|
66
|
+
if (column.formatter) {
|
|
67
|
+
return column.formatter(row);
|
|
68
|
+
}
|
|
69
|
+
if (column.field) {
|
|
70
|
+
return String(row[column.field] ?? '');
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---------- Actions ---------- */
|
|
76
|
+
emitAction(action: TableAction, row: T) {
|
|
77
|
+
this.actionClick.emit({
|
|
78
|
+
action: action.name,
|
|
79
|
+
row,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dropList(event: CdkDragDrop<T[]>) {
|
|
84
|
+
if (!this.draggable || !this.result) return;
|
|
85
|
+
|
|
86
|
+
moveItemInArray(this.result, event.previousIndex, event.currentIndex);
|
|
87
|
+
|
|
88
|
+
// Update existing sortOrder on T
|
|
89
|
+
this.result.forEach((item: any, index) => {
|
|
90
|
+
item.sortOrder = index + 1;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Emit reordered list
|
|
94
|
+
this.dragDropChange.emit(this.result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onDragMoved(event: CdkDragMove<any>) {
|
|
98
|
+
if (!this.tableScrollContainer) return;
|
|
99
|
+
|
|
100
|
+
const container = this.tableScrollContainer.nativeElement;
|
|
101
|
+
const rect = container.getBoundingClientRect();
|
|
102
|
+
const pointerY = event.pointerPosition.y - rect.top;
|
|
103
|
+
|
|
104
|
+
const threshold = 80;
|
|
105
|
+
const maxSpeed = 25;
|
|
106
|
+
|
|
107
|
+
if (pointerY < threshold) {
|
|
108
|
+
const intensity = 1 - pointerY / threshold;
|
|
109
|
+
container.scrollTop -= Math.min(maxSpeed, intensity * maxSpeed);
|
|
110
|
+
} else if (pointerY > rect.height - threshold) {
|
|
111
|
+
const intensity = 1 - (rect.height - pointerY) / threshold;
|
|
112
|
+
container.scrollTop += Math.min(maxSpeed, intensity * maxSpeed);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onDragStart(event: CdkDragStart<any>) {
|
|
117
|
+
const row = event.source.element.nativeElement as HTMLElement;
|
|
118
|
+
const cells = Array.from(row.querySelectorAll('td'));
|
|
119
|
+
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
|
|
122
|
+
|
|
123
|
+
if (!preview) return;
|
|
124
|
+
|
|
125
|
+
const previewCells = preview.querySelectorAll('td');
|
|
126
|
+
|
|
127
|
+
cells.forEach((cell, index) => {
|
|
128
|
+
const width = cell.getBoundingClientRect().width + 'px';
|
|
129
|
+
const previewCell = previewCells[index] as HTMLElement;
|
|
130
|
+
|
|
131
|
+
if (previewCell) {
|
|
132
|
+
previewCell.style.width = width;
|
|
133
|
+
previewCell.style.minWidth = width;
|
|
134
|
+
previewCell.style.maxWidth = width;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface TableColumn<T = any> {
|
|
2
|
+
header: string;
|
|
3
|
+
field?: keyof T;
|
|
4
|
+
width?: string;
|
|
5
|
+
sticky?: boolean;
|
|
6
|
+
sortable?: boolean;
|
|
7
|
+
headerClass?: string;
|
|
8
|
+
cellClass?: string;
|
|
9
|
+
formatter?: (row: T) => string;
|
|
10
|
+
|
|
11
|
+
/** show / hide both th + td */
|
|
12
|
+
visible?: boolean; // default: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TableAction{
|
|
16
|
+
name: string; // e.g. edit, delete
|
|
17
|
+
icon: string;
|
|
18
|
+
tooltip: string;
|
|
19
|
+
hasPermission: boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<div class="input-container">
|
|
2
|
+
@if(label){
|
|
3
|
+
<label [for]="id" class="input-label">
|
|
4
|
+
{{ label }}
|
|
5
|
+
@if(required){
|
|
6
|
+
<span class="input-label-required">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
<div class="input-wrapper" [ngClass]="{
|
|
12
|
+
'input-wrapper--url': type === 'url',
|
|
13
|
+
'input-wrapper--phone': phone,
|
|
14
|
+
'input-wrapper--password': password,
|
|
15
|
+
'input-wrapper--icon': iconSrc && showIcon
|
|
16
|
+
}">
|
|
17
|
+
|
|
18
|
+
<input
|
|
19
|
+
#inputField
|
|
20
|
+
[type]="currentInputType"
|
|
21
|
+
[id]="id"
|
|
22
|
+
[name]="name"
|
|
23
|
+
[disabled]="disabled"
|
|
24
|
+
[tabindex]="tabIndex"
|
|
25
|
+
[readOnly]="readOnly"
|
|
26
|
+
[attr.maxlength]="maxlength"
|
|
27
|
+
[attr.minlength]="minlength"
|
|
28
|
+
[autocomplete]="autoComplete"
|
|
29
|
+
[autocapitalize]="autoCapitalize"
|
|
30
|
+
|
|
31
|
+
[attr.max]="max"
|
|
32
|
+
[attr.min]="min"
|
|
33
|
+
[attr.step]="step"
|
|
34
|
+
|
|
35
|
+
[placeholder]="placeHolderText"
|
|
36
|
+
[pattern]="pattern"
|
|
37
|
+
[autocomplete]="autoComplete"
|
|
38
|
+
[value]="inputValue"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
[mask]="maskValue"
|
|
43
|
+
[prefix]="maskPrefixValue"
|
|
44
|
+
[showMaskTyped]="false"
|
|
45
|
+
[dropSpecialCharacters]="false"
|
|
46
|
+
(change)="handleChange($event)"
|
|
47
|
+
(input)="handleInput($event)"
|
|
48
|
+
(focus)="handleFocus($event)"
|
|
49
|
+
(blur)="handleBlur($event)"
|
|
50
|
+
class="input-field"
|
|
51
|
+
|
|
52
|
+
[ngClass]="{
|
|
53
|
+
'input-field--url': type === 'url',
|
|
54
|
+
'input-field--phone': phone,
|
|
55
|
+
'input-field--icon-left': iconSrc && showIcon && iconOrientation === 'left',
|
|
56
|
+
'input-field--icon-right': iconSrc && showIcon && iconOrientation === 'right',
|
|
57
|
+
'input-field--password': password,
|
|
58
|
+
'input-field--default': inputState === 'default',
|
|
59
|
+
'input-field--focused': inputState === 'focused',
|
|
60
|
+
'input-field--filled': inputState === 'filled',
|
|
61
|
+
'input-field--error': inputState === 'error',
|
|
62
|
+
'input-field--disabled': inputState === 'disabled'
|
|
63
|
+
}">
|
|
64
|
+
|
|
65
|
+
@if(iconSrc && showIcon){
|
|
66
|
+
<img (click)="handleIconClick($event)" [src]="iconSrc" [alt]="iconAlt" [ngClass]="{
|
|
67
|
+
'input-search-icon--left': iconOrientation === 'left',
|
|
68
|
+
'input-search-icon--right': iconOrientation === 'right',
|
|
69
|
+
'cursor-pointer': !disabled && !readOnly
|
|
70
|
+
}" class="input-search-icon">
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@if(showErrorIcon){
|
|
74
|
+
<img src="../../assets/images/icons/global/info-circle.svg" class="input-search-icon input-search-icon--right">
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@if(password){
|
|
78
|
+
<button type="button" (click)="togglePasswordVisibility($event)" class="input-password-toggle" [disabled]="disabled" tabindex="-1">
|
|
79
|
+
<img [src]="showPassword ? '../../assets/images/icons/global/eye-slash-icon.svg' : '../../assets/images/icons/global/eye-icon.svg'" [alt]="showPassword ? 'Hide password' : 'Show password'" class="input-password-icon">
|
|
80
|
+
</button>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@if(phone){
|
|
84
|
+
<div #selectRef class="input-phone-selector" [ngClass]="{
|
|
85
|
+
'input-phone-selector--default': inputState === 'default',
|
|
86
|
+
'input-phone-selector--focused': inputState === 'focused',
|
|
87
|
+
'input-phone-selector--filled': inputState === 'filled',
|
|
88
|
+
'input-phone-selector--error': inputState === 'error',
|
|
89
|
+
'input-phone-selector--disabled': inputState === 'disabled'
|
|
90
|
+
}" (click)="toggleDropdown($event)">
|
|
91
|
+
<span class="input-phone-selector-text">{{ selectedCountry.name }}</span>
|
|
92
|
+
<img src="../../assets/images/icons/global/input-arrow-down.svg" alt="Dropdown" class="input-phone-selector-arrow" [ngClass]="{'input-phone-selector-arrow--open': isDropdownOpen}">
|
|
93
|
+
</div>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@if(phone && isDropdownOpen){
|
|
97
|
+
<div #dropdownRef class="input-phone-dropdown" (click)="$event.stopPropagation()">
|
|
98
|
+
<button *ngFor="let country of countryOptions" type="button" class="input-phone-dropdown-item" [ngClass]="{'input-phone-dropdown-item--active': selectedCountry.code === country.code}" (click)="selectCountry(country); $event.stopPropagation()">
|
|
99
|
+
{{ country.name }}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@if(type === 'url'){
|
|
107
|
+
<span class="input-url-prefix" [ngClass]="{
|
|
108
|
+
'input-url-prefix--default': inputState === 'default',
|
|
109
|
+
'input-url-prefix--focused': inputState === 'focused',
|
|
110
|
+
'input-url-prefix--filled': inputState === 'filled',
|
|
111
|
+
'input-url-prefix--error': inputState === 'error',
|
|
112
|
+
'input-url-prefix--disabled': inputState === 'disabled'
|
|
113
|
+
}">https</span>
|
|
114
|
+
}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
@if(hasError){
|
|
118
|
+
@if (errorMessage) {
|
|
119
|
+
<p class="input-error">{{ errorMessage }}</p>
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
@if(!hasError){
|
|
123
|
+
@if(hint){
|
|
124
|
+
<p class="input-hint">{{ hint }}</p>
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</div>
|