@dso-design-system/ui 0.0.2 → 0.1.0
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/esm2022/lib/alert/alert.component.mjs +54 -0
- package/esm2022/lib/badge/badge.component.mjs +22 -0
- package/esm2022/lib/breadcrumb/breadcrumb.component.mjs +29 -0
- package/esm2022/lib/checkbox/checkbox.component.mjs +82 -0
- package/esm2022/lib/datepicker/datepicker.component.mjs +161 -0
- package/esm2022/lib/dialog/dialog.component.mjs +25 -0
- package/esm2022/lib/directives/truncate.directive.mjs +71 -0
- package/esm2022/lib/dropdown-list/dropdown-list.component.mjs +89 -0
- package/esm2022/lib/file-upload-items/file-upload-items.component.mjs +65 -0
- package/esm2022/lib/file-upload-multiple/file-upload-multiple.component.mjs +232 -0
- package/esm2022/lib/file-upload-multiple/upload-item.model.mjs +2 -0
- package/esm2022/lib/file-upload-multiple/upload-simulator.service.mjs +76 -0
- package/esm2022/lib/file-upload-single/file-upload-single.component.mjs +100 -0
- package/esm2022/lib/input-text/input-text.component.mjs +93 -0
- package/esm2022/lib/pagination/pagination.component.mjs +115 -0
- package/esm2022/lib/progress-bar/progress-bar.component.mjs +25 -0
- package/esm2022/lib/radio/radio.component.mjs +41 -0
- package/esm2022/lib/select-dropdown/select-dropdown.component.mjs +228 -0
- package/esm2022/lib/service/toast.service.mjs +20 -0
- package/esm2022/lib/side-navigation-bar/side-navigation-bar.component.mjs +113 -0
- package/esm2022/lib/spinner/spinner.component.mjs +60 -0
- package/esm2022/lib/table/table.component.mjs +136 -0
- package/esm2022/lib/tabs/tab.component.mjs +20 -0
- package/esm2022/lib/tabs/tabs.component.mjs +40 -0
- package/esm2022/lib/tag/tag.component.mjs +27 -0
- package/esm2022/lib/text-area/text-area.component.mjs +74 -0
- package/esm2022/lib/toast/toast.component.mjs +36 -0
- package/esm2022/lib/tooltip/tooltip.component.mjs +38 -0
- package/esm2022/lib/tooltip/tooltip.directive.mjs +105 -0
- package/esm2022/lib/top-navigation-bar/top-navigation-bar.component.mjs +24 -0
- package/esm2022/public-api.mjs +27 -2
- package/fesm2022/dso-design-system-ui.mjs +2056 -3
- package/fesm2022/dso-design-system-ui.mjs.map +1 -1
- package/lib/alert/alert.component.d.ts +20 -0
- package/lib/badge/badge.component.d.ts +8 -0
- package/lib/breadcrumb/breadcrumb.component.d.ts +15 -0
- package/lib/checkbox/checkbox.component.d.ts +42 -0
- package/lib/datepicker/datepicker.component.d.ts +48 -0
- package/lib/dialog/dialog.component.d.ts +10 -0
- package/lib/directives/truncate.directive.d.ts +23 -0
- package/lib/dropdown-list/dropdown-list.component.d.ts +33 -0
- package/lib/file-upload-items/file-upload-items.component.d.ts +27 -0
- package/lib/file-upload-multiple/file-upload-multiple.component.d.ts +44 -0
- package/lib/file-upload-multiple/upload-item.model.d.ts +7 -0
- package/lib/file-upload-multiple/upload-simulator.service.d.ts +34 -0
- package/lib/file-upload-single/file-upload-single.component.d.ts +28 -0
- package/lib/input-text/input-text.component.d.ts +24 -0
- package/lib/pagination/pagination.component.d.ts +31 -0
- package/lib/progress-bar/progress-bar.component.d.ts +11 -0
- package/lib/radio/radio.component.d.ts +14 -0
- package/lib/select-dropdown/select-dropdown.component.d.ts +78 -0
- package/lib/service/toast.service.d.ts +16 -0
- package/lib/side-navigation-bar/side-navigation-bar.component.d.ts +74 -0
- package/lib/spinner/spinner.component.d.ts +23 -0
- package/lib/table/table.component.d.ts +43 -0
- package/lib/tabs/tab.component.d.ts +9 -0
- package/lib/tabs/tabs.component.d.ts +15 -0
- package/lib/tag/tag.component.d.ts +10 -0
- package/lib/text-area/text-area.component.d.ts +21 -0
- package/lib/toast/toast.component.d.ts +13 -0
- package/lib/tooltip/tooltip.component.d.ts +15 -0
- package/lib/tooltip/tooltip.directive.d.ts +19 -0
- package/lib/top-navigation-bar/top-navigation-bar.component.d.ts +16 -0
- package/package.json +1 -1
- package/public-api.d.ts +25 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import * as i1 from '@angular/common';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { CommonModule, DatePipe } from '@angular/common';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Component, Input, EventEmitter, Output } from '@angular/core';
|
|
4
|
+
import { Component, Input, EventEmitter, Output, ViewChild, HostListener, Directive, Injectable, ContentChildren } from '@angular/core';
|
|
5
|
+
import * as i2$1 from '@angular/router';
|
|
6
|
+
import { RouterLink, RouterModule } from '@angular/router';
|
|
7
|
+
import * as i2 from '@angular/forms';
|
|
8
|
+
import { FormsModule } from '@angular/forms';
|
|
9
|
+
import lottie from 'lottie-web';
|
|
10
|
+
import { ReplaySubject } from 'rxjs';
|
|
5
11
|
|
|
6
12
|
class IconComponent {
|
|
7
13
|
/** Name of the icon to display (matches sprite ID) */
|
|
@@ -122,9 +128,2056 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
122
128
|
type: Output
|
|
123
129
|
}] } });
|
|
124
130
|
|
|
131
|
+
class AlertComponent {
|
|
132
|
+
type = 'info';
|
|
133
|
+
message = '';
|
|
134
|
+
showActions = false;
|
|
135
|
+
isDismissable = true;
|
|
136
|
+
actions = [];
|
|
137
|
+
isVisible = true;
|
|
138
|
+
isHiding = false;
|
|
139
|
+
close() {
|
|
140
|
+
this.isHiding = true;
|
|
141
|
+
// Wait for fadeOut animation to finish before removing the alert
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
this.isVisible = false;
|
|
144
|
+
}, 300); // match duration in CSS
|
|
145
|
+
}
|
|
146
|
+
// Pick the icon name based on alert type
|
|
147
|
+
get iconName() {
|
|
148
|
+
switch (this.type) {
|
|
149
|
+
case 'success': return 'icon-slanted-check';
|
|
150
|
+
case 'error': return 'icon-left-align';
|
|
151
|
+
case 'warning': return 'icon-van';
|
|
152
|
+
default: return 'icon-notification';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Handle when a button inside the alert is clicked */
|
|
156
|
+
handleAction(action) {
|
|
157
|
+
if (action.onClick) {
|
|
158
|
+
action.onClick();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AlertComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
162
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: AlertComponent, isStandalone: true, selector: "dso-alert", inputs: { type: "type", message: "message", showActions: "showActions", isDismissable: "isDismissable", actions: "actions" }, ngImport: i0, template: "\r\n<div\r\n*ngIf=\"isVisible\"\r\nclass=\"alert\"\r\n[ngClass]=\"[type, isHiding ? 'hide' : '']\"> \r\n <div class=\"positioning\">\r\n <dso-icon [iconName]=\"iconName\" [size]=\"'medium'\"></dso-icon>\r\n </div>\r\n <span [innerHTML]=\"message\"></span>\r\n\r\n <div class=\"positioning\" *ngIf=\"actions && actions.length > 0 || isDismissable\">\r\n <div class=\"button-wrapper\">\r\n <dso-button\r\n *ngFor=\"let action of actions\"\r\n [btnLabel]=\"action.btnLabel\"\r\n [btnType]=\"action.btnType || 'contrast'\" \r\n [btnSize]=\"action.btnSize || 'mdBtn'\"\r\n [btnIconName]=\"action.btnIconName ?? null\"\r\n [isDisabled]=\"action.isDisabled || false\"\r\n (click)=\"handleAction(action)\">\r\n </dso-button>\r\n </div>\r\n <button (click)=\"close()\" *ngIf=\"isDismissable\" class=\"close-btn\">×</button>\r\n </div>\r\n \r\n</div>", styles: ["@charset \"UTF-8\";@keyframes fadeIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.alert{padding:12px 16px;border-radius:5px;gap:16px;display:flex;align-items:stretch;font-size:16px;animation:fadeIn .4s ease-in}.alert.hide{animation:fadeOut .4s ease-out forwards}.success{background-color:#77c98a;color:#fff}.error{background-color:#dc3545;color:#fff}.info{background-color:#17a2b8;color:#fff}.warning{background-color:#ffc107;color:#000}.close-btn{width:24px;height:24px;padding:0;margin:0;display:flex;align-items:center;justify-content:center;background:none;border:none;font-size:24px;line-height:1;color:inherit;cursor:pointer}.close-btn:hover{opacity:.7}.content-wrapper{display:flex;align-items:center;flex:1;gap:12px}.positioning{display:flex;align-items:flex-start;align-self:stretch}span{flex-grow:1;line-height:1.4;max-height:7em;overflow-y:auto}.button-wrapper{display:flex;flex-direction:column;gap:8px;padding-right:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
|
|
163
|
+
}
|
|
164
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AlertComponent, decorators: [{
|
|
165
|
+
type: Component,
|
|
166
|
+
args: [{ selector: 'dso-alert', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent], template: "\r\n<div\r\n*ngIf=\"isVisible\"\r\nclass=\"alert\"\r\n[ngClass]=\"[type, isHiding ? 'hide' : '']\"> \r\n <div class=\"positioning\">\r\n <dso-icon [iconName]=\"iconName\" [size]=\"'medium'\"></dso-icon>\r\n </div>\r\n <span [innerHTML]=\"message\"></span>\r\n\r\n <div class=\"positioning\" *ngIf=\"actions && actions.length > 0 || isDismissable\">\r\n <div class=\"button-wrapper\">\r\n <dso-button\r\n *ngFor=\"let action of actions\"\r\n [btnLabel]=\"action.btnLabel\"\r\n [btnType]=\"action.btnType || 'contrast'\" \r\n [btnSize]=\"action.btnSize || 'mdBtn'\"\r\n [btnIconName]=\"action.btnIconName ?? null\"\r\n [isDisabled]=\"action.isDisabled || false\"\r\n (click)=\"handleAction(action)\">\r\n </dso-button>\r\n </div>\r\n <button (click)=\"close()\" *ngIf=\"isDismissable\" class=\"close-btn\">×</button>\r\n </div>\r\n \r\n</div>", styles: ["@charset \"UTF-8\";@keyframes fadeIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.alert{padding:12px 16px;border-radius:5px;gap:16px;display:flex;align-items:stretch;font-size:16px;animation:fadeIn .4s ease-in}.alert.hide{animation:fadeOut .4s ease-out forwards}.success{background-color:#77c98a;color:#fff}.error{background-color:#dc3545;color:#fff}.info{background-color:#17a2b8;color:#fff}.warning{background-color:#ffc107;color:#000}.close-btn{width:24px;height:24px;padding:0;margin:0;display:flex;align-items:center;justify-content:center;background:none;border:none;font-size:24px;line-height:1;color:inherit;cursor:pointer}.close-btn:hover{opacity:.7}.content-wrapper{display:flex;align-items:center;flex:1;gap:12px}.positioning{display:flex;align-items:flex-start;align-self:stretch}span{flex-grow:1;line-height:1.4;max-height:7em;overflow-y:auto}.button-wrapper{display:flex;flex-direction:column;gap:8px;padding-right:8px}\n"] }]
|
|
167
|
+
}], propDecorators: { type: [{
|
|
168
|
+
type: Input
|
|
169
|
+
}], message: [{
|
|
170
|
+
type: Input
|
|
171
|
+
}], showActions: [{
|
|
172
|
+
type: Input
|
|
173
|
+
}], isDismissable: [{
|
|
174
|
+
type: Input
|
|
175
|
+
}], actions: [{
|
|
176
|
+
type: Input
|
|
177
|
+
}] } });
|
|
178
|
+
|
|
179
|
+
class BadgeComponent {
|
|
180
|
+
label = null;
|
|
181
|
+
color = 'primary';
|
|
182
|
+
get isDot() {
|
|
183
|
+
return this.label === null || this.label === '' || this.label === undefined;
|
|
184
|
+
}
|
|
185
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
186
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: BadgeComponent, isStandalone: true, selector: "dso-badge", inputs: { label: "label", color: "color" }, ngImport: i0, template: "<span class=\"badge\" [ngClass]=\"[color, isDot ? 'dot' : '']\">\r\n <ng-container *ngIf=\"!isDot\">{{ label }}</ng-container>\r\n</span>", styles: [".badge{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;border-radius:1rem;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#fff;line-height:1;min-width:1.25rem}.primary{background-color:#007bff}.success{background-color:#28a745}.warning{background-color:#ffc107;color:#000}.danger{background-color:#dc3545}.badge.dot{padding:0;width:.5rem;height:.5rem;min-width:0;border-radius:50%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
187
|
+
}
|
|
188
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BadgeComponent, decorators: [{
|
|
189
|
+
type: Component,
|
|
190
|
+
args: [{ selector: 'dso-badge', standalone: true, imports: [CommonModule], template: "<span class=\"badge\" [ngClass]=\"[color, isDot ? 'dot' : '']\">\r\n <ng-container *ngIf=\"!isDot\">{{ label }}</ng-container>\r\n</span>", styles: [".badge{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;border-radius:1rem;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#fff;line-height:1;min-width:1.25rem}.primary{background-color:#007bff}.success{background-color:#28a745}.warning{background-color:#ffc107;color:#000}.danger{background-color:#dc3545}.badge.dot{padding:0;width:.5rem;height:.5rem;min-width:0;border-radius:50%}\n"] }]
|
|
191
|
+
}], propDecorators: { label: [{
|
|
192
|
+
type: Input
|
|
193
|
+
}], color: [{
|
|
194
|
+
type: Input
|
|
195
|
+
}] } });
|
|
196
|
+
|
|
197
|
+
class BreadcrumbComponent {
|
|
198
|
+
items = [];
|
|
199
|
+
itemSelected = new EventEmitter();
|
|
200
|
+
// Default separator → you can allow this to be set via @Input() later
|
|
201
|
+
separator = '/';
|
|
202
|
+
onItemClicked(item) {
|
|
203
|
+
this.itemSelected.emit(item);
|
|
204
|
+
console.log('breadcrumb emitted -> ', item);
|
|
205
|
+
}
|
|
206
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
207
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: BreadcrumbComponent, isStandalone: true, selector: "dso-breadcrumb", inputs: { items: "items", separator: "separator" }, outputs: { itemSelected: "itemSelected" }, ngImport: i0, template: "<nav aria-label=\"Breadcrumb\" class=\"breadcrumb\">\r\n <ol>\r\n <li *ngFor=\"let item of items; let last = last\" class=\"crumb\">\r\n\r\n <!-- Normal breadcrumb link -->\r\n <ng-container *ngIf=\"!last && item.url; else lastItem\">\r\n\r\n <dso-button\r\n btnType=\"text\"\r\n btnSize=\"smBtn\"\r\n [btnLabel]=\"item.label\"\r\n [isDisabled]=\"false\"\r\n (onClick)=\"onItemClicked(item)\"\r\n [routerLink]=\"item.url\">\r\n </dso-button>\r\n\r\n </ng-container>\r\n\r\n <!-- Last breadcrumb item (non-clickable) -->\r\n <ng-template #lastItem>\r\n <span class=\"current\" aria-current=\"page\">\r\n {{ item.label }}\r\n </span>\r\n </ng-template>\r\n\r\n <!-- Separator -->\r\n <span class=\"separator\" *ngIf=\"!last\">{{ separator }}</span>\r\n\r\n </li>\r\n </ol>\r\n <!-- <dso-button btnType=\"text\" btnLabel=\"Go Home\" [routerLink]=\"['/page1']\"></dso-button> -->\r\n\r\n</nav>\r\n\r\n\r\n", styles: [".breadcrumb{font-size:14px;color:var(--ui-text-muted, #666)}.breadcrumb ol{list-style:none;padding:0;margin:0;display:flex;align-items:center;flex-wrap:wrap}.breadcrumb .crumb{display:flex;align-items:center}.breadcrumb .crumb a{color:var(--ui-primary, #4a5cff);text-decoration:none;font-weight:500}.breadcrumb .crumb a:hover{text-decoration:underline}.breadcrumb .crumb .current{color:var(--ui-text, #222);font-weight:600;padding:0 8px}.breadcrumb .crumb .separator{margin:0 8px;color:var(--ui-text-muted, #999);font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }] });
|
|
208
|
+
}
|
|
209
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BreadcrumbComponent, decorators: [{
|
|
210
|
+
type: Component,
|
|
211
|
+
args: [{ selector: 'dso-breadcrumb', standalone: true, imports: [CommonModule, RouterLink, ButtonComponent], template: "<nav aria-label=\"Breadcrumb\" class=\"breadcrumb\">\r\n <ol>\r\n <li *ngFor=\"let item of items; let last = last\" class=\"crumb\">\r\n\r\n <!-- Normal breadcrumb link -->\r\n <ng-container *ngIf=\"!last && item.url; else lastItem\">\r\n\r\n <dso-button\r\n btnType=\"text\"\r\n btnSize=\"smBtn\"\r\n [btnLabel]=\"item.label\"\r\n [isDisabled]=\"false\"\r\n (onClick)=\"onItemClicked(item)\"\r\n [routerLink]=\"item.url\">\r\n </dso-button>\r\n\r\n </ng-container>\r\n\r\n <!-- Last breadcrumb item (non-clickable) -->\r\n <ng-template #lastItem>\r\n <span class=\"current\" aria-current=\"page\">\r\n {{ item.label }}\r\n </span>\r\n </ng-template>\r\n\r\n <!-- Separator -->\r\n <span class=\"separator\" *ngIf=\"!last\">{{ separator }}</span>\r\n\r\n </li>\r\n </ol>\r\n <!-- <dso-button btnType=\"text\" btnLabel=\"Go Home\" [routerLink]=\"['/page1']\"></dso-button> -->\r\n\r\n</nav>\r\n\r\n\r\n", styles: [".breadcrumb{font-size:14px;color:var(--ui-text-muted, #666)}.breadcrumb ol{list-style:none;padding:0;margin:0;display:flex;align-items:center;flex-wrap:wrap}.breadcrumb .crumb{display:flex;align-items:center}.breadcrumb .crumb a{color:var(--ui-primary, #4a5cff);text-decoration:none;font-weight:500}.breadcrumb .crumb a:hover{text-decoration:underline}.breadcrumb .crumb .current{color:var(--ui-text, #222);font-weight:600;padding:0 8px}.breadcrumb .crumb .separator{margin:0 8px;color:var(--ui-text-muted, #999);font-size:12px}\n"] }]
|
|
212
|
+
}], propDecorators: { items: [{
|
|
213
|
+
type: Input
|
|
214
|
+
}], itemSelected: [{
|
|
215
|
+
type: Output
|
|
216
|
+
}], separator: [{
|
|
217
|
+
type: Input
|
|
218
|
+
}] } });
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Reusable Checkbox Component
|
|
222
|
+
*
|
|
223
|
+
* Features:
|
|
224
|
+
* - Optional label
|
|
225
|
+
* - Configurable size, error state, and disabled state
|
|
226
|
+
* - Emits changes to parent component
|
|
227
|
+
* - Supports custom icons
|
|
228
|
+
*/
|
|
229
|
+
class CheckboxComponent {
|
|
230
|
+
/** Text label displayed next to the checkbox */
|
|
231
|
+
label;
|
|
232
|
+
/** Whether the checkbox is checked */
|
|
233
|
+
isChecked = true;
|
|
234
|
+
/** Whether the checkbox is disabled */
|
|
235
|
+
disabled = false;
|
|
236
|
+
/** Size of the checkbox: small, medium, or large */
|
|
237
|
+
size = 'medium';
|
|
238
|
+
/** Error state for styling purposes */
|
|
239
|
+
error = false;
|
|
240
|
+
/** Whether the checkbox is required */
|
|
241
|
+
required = false;
|
|
242
|
+
/** Error message to display when error is true */
|
|
243
|
+
errorMessage = '';
|
|
244
|
+
/** Icon name for the checkbox (default is a checkmark) */
|
|
245
|
+
iconName = 'icon-slanted-check';
|
|
246
|
+
/** Emits the updated checked state whenever the checkbox is toggled */
|
|
247
|
+
change = new EventEmitter();
|
|
248
|
+
/**
|
|
249
|
+
* Toggles the checked state of the checkbox
|
|
250
|
+
* and emits the new value to the parent component
|
|
251
|
+
*/
|
|
252
|
+
toggleCheckbox() {
|
|
253
|
+
if (!this.disabled) {
|
|
254
|
+
this.isChecked = !this.isChecked;
|
|
255
|
+
this.change.emit(this.isChecked);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Maps the checkbox size to icon size
|
|
260
|
+
*/
|
|
261
|
+
getIconSize() {
|
|
262
|
+
switch (this.size) {
|
|
263
|
+
case 'large': return 'large';
|
|
264
|
+
case 'medium': return 'medium';
|
|
265
|
+
case 'small': return 'small';
|
|
266
|
+
default: return 'medium'; // fallback to medium
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
270
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CheckboxComponent, isStandalone: true, selector: "dso-checkbox", inputs: { label: "label", isChecked: "isChecked", disabled: "disabled", size: "size", error: "error", required: "required", errorMessage: "errorMessage", iconName: "iconName" }, outputs: { change: "change" }, ngImport: i0, template: "<!--\r\n Checkbox Component Template\r\n --------------------------\r\n Features:\r\n - Clickable wrapper to toggle checkbox (disabled-safe)\r\n - Custom checkbox with icon\r\n - Label support\r\n - Error state styling\r\n - Size-based styling: small, medium, large\r\n-->\r\n\r\n<div \r\n class=\"checkbox-wrapper\"\r\n (click)=\"!disabled && toggleCheckbox()\"\r\n [ngClass]=\"{\r\n 'small': size === 'small', \r\n 'medium': size === 'medium', \r\n 'large': size === 'large',\r\n 'disabled': disabled\r\n }\">\r\n\r\n <!-- Hidden native checkbox for accessibility and form integration -->\r\n <input\r\n class=\"checkbox-input\"\r\n type=\"checkbox\"\r\n [ngModel]=\"isChecked\"\r\n [disabled]=\"disabled\"/>\r\n\r\n <!-- Custom checkbox UI -->\r\n <span \r\n class=\"custom-checkbox\" \r\n [class.checked]=\"isChecked\" \r\n [class.error]=\"error\">\r\n \r\n <!-- Display check icon only when checked -->\r\n <dso-icon \r\n *ngIf=\"isChecked\" \r\n [iconName]=\"iconName\" \r\n [size]=\"getIconSize()\">\r\n </dso-icon>\r\n\r\n </span> \r\n\r\n <!-- Optional label -->\r\n <span class=\"checkbox-label\">\r\n {{ label }}\r\n </span>\r\n\r\n</div>\r\n", styles: [".checkbox-wrapper{display:inline-flex;align-items:center;gap:8px;cursor:pointer;vertical-align:middle}.checkbox-wrapper.error .custom-checkbox{border-color:red}.checkbox-wrapper.disabled{cursor:not-allowed}.checkbox-wrapper.disabled .custom-checkbox{pointer-events:none;background-color:#f0f0f0;border-color:#d0d0d0}.checkbox-wrapper.disabled .custom-checkbox.checked{background-color:#00f;border-color:#00f;opacity:.3}.checkbox-input{width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-checkbox{display:inline-flex;justify-content:center;border:1px solid #ccc;border-radius:4px;cursor:pointer;width:24px;height:24px}.custom-checkbox.checked{border-color:#00f;background-color:#00f;color:#fff}.checkbox-wrapper:not(.disabled):hover .custom-checkbox{background-color:#eeeff9;border-color:#0000ffc0;cursor:pointer}.checkbox-wrapper:not(.disabled):hover .custom-checkbox.checked{background-color:#0000ffa9;border-color:#0000ffc0}.custom-checkbox.small .checkbox-custom{width:12px;height:12px}.custom-checkbox.medium .checkbox-custom{width:16px;height:16px}.custom-checkbox.large .checkbox-custom{width:24px;height:24px}.checkbox-label{font-size:14px;font-weight:400}.error-message{color:red;font-size:12px;margin-top:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
271
|
+
}
|
|
272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CheckboxComponent, decorators: [{
|
|
273
|
+
type: Component,
|
|
274
|
+
args: [{ selector: 'dso-checkbox', standalone: true, imports: [CommonModule, IconComponent, FormsModule], template: "<!--\r\n Checkbox Component Template\r\n --------------------------\r\n Features:\r\n - Clickable wrapper to toggle checkbox (disabled-safe)\r\n - Custom checkbox with icon\r\n - Label support\r\n - Error state styling\r\n - Size-based styling: small, medium, large\r\n-->\r\n\r\n<div \r\n class=\"checkbox-wrapper\"\r\n (click)=\"!disabled && toggleCheckbox()\"\r\n [ngClass]=\"{\r\n 'small': size === 'small', \r\n 'medium': size === 'medium', \r\n 'large': size === 'large',\r\n 'disabled': disabled\r\n }\">\r\n\r\n <!-- Hidden native checkbox for accessibility and form integration -->\r\n <input\r\n class=\"checkbox-input\"\r\n type=\"checkbox\"\r\n [ngModel]=\"isChecked\"\r\n [disabled]=\"disabled\"/>\r\n\r\n <!-- Custom checkbox UI -->\r\n <span \r\n class=\"custom-checkbox\" \r\n [class.checked]=\"isChecked\" \r\n [class.error]=\"error\">\r\n \r\n <!-- Display check icon only when checked -->\r\n <dso-icon \r\n *ngIf=\"isChecked\" \r\n [iconName]=\"iconName\" \r\n [size]=\"getIconSize()\">\r\n </dso-icon>\r\n\r\n </span> \r\n\r\n <!-- Optional label -->\r\n <span class=\"checkbox-label\">\r\n {{ label }}\r\n </span>\r\n\r\n</div>\r\n", styles: [".checkbox-wrapper{display:inline-flex;align-items:center;gap:8px;cursor:pointer;vertical-align:middle}.checkbox-wrapper.error .custom-checkbox{border-color:red}.checkbox-wrapper.disabled{cursor:not-allowed}.checkbox-wrapper.disabled .custom-checkbox{pointer-events:none;background-color:#f0f0f0;border-color:#d0d0d0}.checkbox-wrapper.disabled .custom-checkbox.checked{background-color:#00f;border-color:#00f;opacity:.3}.checkbox-input{width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-checkbox{display:inline-flex;justify-content:center;border:1px solid #ccc;border-radius:4px;cursor:pointer;width:24px;height:24px}.custom-checkbox.checked{border-color:#00f;background-color:#00f;color:#fff}.checkbox-wrapper:not(.disabled):hover .custom-checkbox{background-color:#eeeff9;border-color:#0000ffc0;cursor:pointer}.checkbox-wrapper:not(.disabled):hover .custom-checkbox.checked{background-color:#0000ffa9;border-color:#0000ffc0}.custom-checkbox.small .checkbox-custom{width:12px;height:12px}.custom-checkbox.medium .checkbox-custom{width:16px;height:16px}.custom-checkbox.large .checkbox-custom{width:24px;height:24px}.checkbox-label{font-size:14px;font-weight:400}.error-message{color:red;font-size:12px;margin-top:4px}\n"] }]
|
|
275
|
+
}], propDecorators: { label: [{
|
|
276
|
+
type: Input
|
|
277
|
+
}], isChecked: [{
|
|
278
|
+
type: Input
|
|
279
|
+
}], disabled: [{
|
|
280
|
+
type: Input
|
|
281
|
+
}], size: [{
|
|
282
|
+
type: Input
|
|
283
|
+
}], error: [{
|
|
284
|
+
type: Input
|
|
285
|
+
}], required: [{
|
|
286
|
+
type: Input
|
|
287
|
+
}], errorMessage: [{
|
|
288
|
+
type: Input
|
|
289
|
+
}], iconName: [{
|
|
290
|
+
type: Input
|
|
291
|
+
}], change: [{
|
|
292
|
+
type: Output
|
|
293
|
+
}] } });
|
|
294
|
+
|
|
295
|
+
class DatepickerComponent {
|
|
296
|
+
/** Label displayed above the input */
|
|
297
|
+
inputTextLabel = 'Datepicker Title';
|
|
298
|
+
/** Disable the datepicker input */
|
|
299
|
+
isDisabled = false;
|
|
300
|
+
/** Currently selected date */
|
|
301
|
+
selectedDate = null;
|
|
302
|
+
/** Whether the calendar popup is visible */
|
|
303
|
+
calendarVisible = false;
|
|
304
|
+
/** Current month displayed in the calendar */
|
|
305
|
+
currentMonth = new Date();
|
|
306
|
+
/** View mode: day, month, or year */
|
|
307
|
+
viewMode = 'day';
|
|
308
|
+
/** Minimum and maximum year range */
|
|
309
|
+
minYear = 1990;
|
|
310
|
+
maxYear = 2099;
|
|
311
|
+
/** Month names for display */
|
|
312
|
+
months = [
|
|
313
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
314
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
315
|
+
];
|
|
316
|
+
yearListRef;
|
|
317
|
+
calendarWrapper;
|
|
318
|
+
// --------------------------
|
|
319
|
+
// Calendar Visibility & Toggle
|
|
320
|
+
// --------------------------
|
|
321
|
+
/** Toggle the calendar popup */
|
|
322
|
+
toggleCalendar() {
|
|
323
|
+
this.calendarVisible = !this.calendarVisible;
|
|
324
|
+
this.viewMode = 'day';
|
|
325
|
+
}
|
|
326
|
+
/** Close calendar if clicking outside */
|
|
327
|
+
handleClickOutside(event) {
|
|
328
|
+
if (!this.calendarWrapper)
|
|
329
|
+
return;
|
|
330
|
+
const target = event.target;
|
|
331
|
+
if (this.calendarVisible && !this.calendarWrapper.nativeElement.contains(target)) {
|
|
332
|
+
this.calendarVisible = false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// --------------------------
|
|
336
|
+
// View Switching
|
|
337
|
+
// --------------------------
|
|
338
|
+
switchToMonthView() {
|
|
339
|
+
this.viewMode = 'month';
|
|
340
|
+
}
|
|
341
|
+
switchToYearView() {
|
|
342
|
+
this.viewMode = 'year';
|
|
343
|
+
setTimeout(() => this.scrollToCurrentYear(), 50);
|
|
344
|
+
}
|
|
345
|
+
// --------------------------
|
|
346
|
+
// Navigation (Month / Year)
|
|
347
|
+
// --------------------------
|
|
348
|
+
changeMonth(direction) {
|
|
349
|
+
this.currentMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth() + direction, 1);
|
|
350
|
+
}
|
|
351
|
+
changeYear(direction) {
|
|
352
|
+
const newYear = this.currentMonth.getFullYear() + direction;
|
|
353
|
+
if (newYear >= this.minYear && newYear <= this.maxYear) {
|
|
354
|
+
this.currentMonth = new Date(newYear, this.currentMonth.getMonth(), 1);
|
|
355
|
+
setTimeout(() => this.scrollToCurrentYear(), 0);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// --------------------------
|
|
359
|
+
// Helpers
|
|
360
|
+
// --------------------------
|
|
361
|
+
/** Returns array of days in the current month, with leading nulls for empty grid cells */
|
|
362
|
+
getDaysInMonth() {
|
|
363
|
+
const month = this.currentMonth.getMonth();
|
|
364
|
+
const year = this.currentMonth.getFullYear();
|
|
365
|
+
const firstDay = new Date(year, month, 1).getDay();
|
|
366
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
367
|
+
const daysArray = Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
368
|
+
return Array(firstDay).fill(null).concat(daysArray);
|
|
369
|
+
}
|
|
370
|
+
/** Returns array of years in the defined range */
|
|
371
|
+
getYearsRange() {
|
|
372
|
+
const years = [];
|
|
373
|
+
for (let y = this.minYear; y <= this.maxYear; y++)
|
|
374
|
+
years.push(y);
|
|
375
|
+
return years;
|
|
376
|
+
}
|
|
377
|
+
/** Scroll the year list to show the current year */
|
|
378
|
+
scrollToCurrentYear() {
|
|
379
|
+
if (!this.yearListRef)
|
|
380
|
+
return;
|
|
381
|
+
const container = this.yearListRef.nativeElement;
|
|
382
|
+
const currentYear = this.currentMonth.getFullYear();
|
|
383
|
+
const yearIndex = currentYear - this.minYear;
|
|
384
|
+
const columns = 4; // 4 columns grid layout
|
|
385
|
+
const rowIndex = Math.floor(yearIndex / columns);
|
|
386
|
+
const yearCell = container.querySelector('.year-cell');
|
|
387
|
+
if (!yearCell)
|
|
388
|
+
return;
|
|
389
|
+
const scrollTop = rowIndex * yearCell.offsetHeight;
|
|
390
|
+
container.scrollTop = scrollTop;
|
|
391
|
+
}
|
|
392
|
+
// --------------------------
|
|
393
|
+
// Selection Handlers
|
|
394
|
+
// --------------------------
|
|
395
|
+
selectDate(day) {
|
|
396
|
+
if (day === null)
|
|
397
|
+
return;
|
|
398
|
+
this.selectedDate = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth(), day);
|
|
399
|
+
this.calendarVisible = false;
|
|
400
|
+
}
|
|
401
|
+
selectMonth(monthIndex) {
|
|
402
|
+
this.currentMonth = new Date(this.currentMonth.getFullYear(), monthIndex, 1);
|
|
403
|
+
this.viewMode = 'day';
|
|
404
|
+
}
|
|
405
|
+
selectYear(year) {
|
|
406
|
+
this.currentMonth = new Date(year, this.currentMonth.getMonth(), 1);
|
|
407
|
+
this.viewMode = 'month';
|
|
408
|
+
}
|
|
409
|
+
/** Check if a day is currently selected */
|
|
410
|
+
isSelected(day) {
|
|
411
|
+
if (!this.selectedDate || day === null)
|
|
412
|
+
return false;
|
|
413
|
+
return (this.selectedDate.getDate() === day &&
|
|
414
|
+
this.selectedDate.getMonth() === this.currentMonth.getMonth() &&
|
|
415
|
+
this.selectedDate.getFullYear() === this.currentMonth.getFullYear());
|
|
416
|
+
}
|
|
417
|
+
// --------------------------
|
|
418
|
+
// Footer Actions
|
|
419
|
+
// --------------------------
|
|
420
|
+
/** Set today’s date */
|
|
421
|
+
setToday() {
|
|
422
|
+
const today = new Date();
|
|
423
|
+
this.selectedDate = today;
|
|
424
|
+
this.currentMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
425
|
+
this.calendarVisible = false;
|
|
426
|
+
}
|
|
427
|
+
/** Clear the selected date */
|
|
428
|
+
clearDate() {
|
|
429
|
+
this.selectedDate = null;
|
|
430
|
+
}
|
|
431
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatepickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
432
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DatepickerComponent, isStandalone: true, selector: "dso-datepicker", inputs: { inputTextLabel: "inputTextLabel", isDisabled: "isDisabled" }, host: { listeners: { "document:click": "handleClickOutside($event)" } }, viewQueries: [{ propertyName: "yearListRef", first: true, predicate: ["yearList"], descendants: true }, { propertyName: "calendarWrapper", first: true, predicate: ["calendarWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"datepicker-wrapper\" #calendarWrapper>\r\n \r\n <!-- Label above the input -->\r\n <div *ngIf=\"inputTextLabel\" class=\"datepicker-label\">{{ inputTextLabel }}</div>\r\n\r\n <!-- Input field (readonly) -->\r\n <input\r\n type=\"text\"\r\n class=\"datepicker-input\"\r\n [value]=\"selectedDate ? (selectedDate | date:'MMM d, y') : ''\"\r\n placeholder=\"Select a date\"\r\n readonly\r\n (click)=\"toggleCalendar()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n />\r\n\r\n <!-- Calendar popup -->\r\n <div class=\"calendar\" *ngIf=\"calendarVisible\">\r\n\r\n <!-- Header -->\r\n <div class=\"calendar-header\">\r\n <div class=\"header-row\">\r\n\r\n <!-- Year & Month Navigation Buttons -->\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(-1)\" class=\"nav-btn\">\u00AB</button>\r\n <button *ngIf=\"viewMode === 'day'\" \r\n (click)=\"changeMonth(-1)\" class=\"nav-btn\">\u2039</button>\r\n\r\n <!-- Calendar Title (Month / Year display) -->\r\n <span class=\"calendar-title\">\r\n\r\n <!-- Day view: show Month + Year -->\r\n <ng-container *ngIf=\"viewMode === 'day'\">\r\n <span class=\"month-label\" (click)=\"switchToMonthView()\">\r\n {{ currentMonth | date:'MMMM' }}\r\n </span>\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Month view: show Year -->\r\n <ng-container *ngIf=\"viewMode === 'month'\">\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Year view: show Year only -->\r\n <ng-container *ngIf=\"viewMode === 'year'\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </ng-container>\r\n\r\n </span>\r\n\r\n <!-- Forward navigation buttons -->\r\n <button *ngIf=\"viewMode === 'day'\" (click)=\"changeMonth(1)\" class=\"nav-btn\">\u203A</button>\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(1)\" class=\"nav-btn\">\u00BB</button>\r\n\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n DAY VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'day'\">\r\n\r\n <!-- Weekday Labels -->\r\n <div class=\"calendar-weekdays\">\r\n <div *ngFor=\"let day of ['S','M','T','W','T','F','S']\">{{ day }}</div>\r\n </div>\r\n\r\n <!-- Days Grid -->\r\n <div class=\"calendar-grid days-grid\">\r\n <div\r\n *ngFor=\"let day of getDaysInMonth()\"\r\n [class.empty]=\"day === null\"\r\n [class.selected]=\"isSelected(day)\"\r\n (click)=\"selectDate(day)\"\r\n >\r\n {{ day }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n MONTH VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'month'\" class=\"calendar-grid months-grid\">\r\n <div *ngFor=\"let m of months; let i = index\" \r\n (click)=\"selectMonth(i)\" \r\n class=\"month-cell\">\r\n {{ m }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n YEAR VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'year'\" \r\n class=\"calendar-grid years-grid scrollable\" \r\n #yearList>\r\n <div\r\n *ngFor=\"let y of getYearsRange()\"\r\n (click)=\"selectYear(y)\"\r\n class=\"year-cell\"\r\n [class.current-year]=\"y === currentMonth.getFullYear()\"\r\n >\r\n {{ y }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n Footer Buttons\r\n ---------------------- -->\r\n <div class=\"calendar-footer\">\r\n <button class=\"footer-btn clear-btn\" (click)=\"clearDate()\">Clear</button>\r\n <button class=\"footer-btn today-btn\" (click)=\"setToday()\">Today</button>\r\n </div>\r\n\r\n </div>\r\n</div>\r\n", styles: [".datepicker-wrapper{position:relative;display:flex;flex-direction:column;gap:8px}.datepicker-input{padding:10px;font-size:15px;width:190px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background-color:#fff}.datepicker-input:hover{border:1px solid #071d35}.datepicker-input.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.datepicker-input.disabled .selected-option{pointer-events:none;color:#aaa}.calendar{position:absolute;top:0;left:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 10px #0000001a;padding:10px;z-index:100;animation:fadeIn .2s ease-in-out}.calendar-header{margin-bottom:8px}.header-row{display:flex;justify-content:center;align-items:center;gap:4px}.nav-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:4px 6px;color:#333;border-radius:4px;transition:background-color .2s}.nav-btn:hover{background-color:#f0f0f0}.calendar-title{font-weight:600;font-size:16px;min-width:90px;text-align:center;display:flex;flex:1;gap:4px;align-items:center;justify-content:center}.month-label,.year-label{cursor:pointer;transition:color .2s}.month-label:hover,.year-label:hover{color:#007bff}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;margin-bottom:4px;color:#666}.calendar-grid{display:grid;gap:4px;text-align:center;min-height:200px}.days-grid{grid-template-columns:repeat(7,1fr)}.months-grid{grid-template-columns:repeat(3,1fr)}.years-grid{grid-template-columns:repeat(4,1fr)}.scrollable{max-height:160px;overflow-y:auto;padding-right:5px}.scrollable::-webkit-scrollbar{width:6px}.scrollable::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#999}.calendar-grid div{padding:8px 0;border-radius:4px;cursor:pointer;transition:background-color .2s}.calendar-grid div:hover{background-color:#f0f0f0}.selected{background-color:#007bff;color:#fff!important}.current-year{background-color:#007bff1a;font-weight:700}.empty{visibility:hidden}.calendar-footer{display:flex;justify-content:space-between;margin-top:8px;border-top:1px solid #eee;padding-top:8px}.footer-btn{flex:1;padding:6px 0;font-size:14px;border-radius:4px;cursor:pointer;transition:background .2s;border:none}.clear-btn{background-color:#f8f9fa;color:#333;margin-right:5px}.clear-btn:hover{background-color:#e9ecef}.today-btn{background-color:#007bff;color:#fff}.today-btn:hover{background-color:#0069d9}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
|
|
433
|
+
}
|
|
434
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatepickerComponent, decorators: [{
|
|
435
|
+
type: Component,
|
|
436
|
+
args: [{ selector: 'dso-datepicker', standalone: true, imports: [CommonModule, DatePipe], template: "<div class=\"datepicker-wrapper\" #calendarWrapper>\r\n \r\n <!-- Label above the input -->\r\n <div *ngIf=\"inputTextLabel\" class=\"datepicker-label\">{{ inputTextLabel }}</div>\r\n\r\n <!-- Input field (readonly) -->\r\n <input\r\n type=\"text\"\r\n class=\"datepicker-input\"\r\n [value]=\"selectedDate ? (selectedDate | date:'MMM d, y') : ''\"\r\n placeholder=\"Select a date\"\r\n readonly\r\n (click)=\"toggleCalendar()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n />\r\n\r\n <!-- Calendar popup -->\r\n <div class=\"calendar\" *ngIf=\"calendarVisible\">\r\n\r\n <!-- Header -->\r\n <div class=\"calendar-header\">\r\n <div class=\"header-row\">\r\n\r\n <!-- Year & Month Navigation Buttons -->\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(-1)\" class=\"nav-btn\">\u00AB</button>\r\n <button *ngIf=\"viewMode === 'day'\" \r\n (click)=\"changeMonth(-1)\" class=\"nav-btn\">\u2039</button>\r\n\r\n <!-- Calendar Title (Month / Year display) -->\r\n <span class=\"calendar-title\">\r\n\r\n <!-- Day view: show Month + Year -->\r\n <ng-container *ngIf=\"viewMode === 'day'\">\r\n <span class=\"month-label\" (click)=\"switchToMonthView()\">\r\n {{ currentMonth | date:'MMMM' }}\r\n </span>\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Month view: show Year -->\r\n <ng-container *ngIf=\"viewMode === 'month'\">\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Year view: show Year only -->\r\n <ng-container *ngIf=\"viewMode === 'year'\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </ng-container>\r\n\r\n </span>\r\n\r\n <!-- Forward navigation buttons -->\r\n <button *ngIf=\"viewMode === 'day'\" (click)=\"changeMonth(1)\" class=\"nav-btn\">\u203A</button>\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(1)\" class=\"nav-btn\">\u00BB</button>\r\n\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n DAY VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'day'\">\r\n\r\n <!-- Weekday Labels -->\r\n <div class=\"calendar-weekdays\">\r\n <div *ngFor=\"let day of ['S','M','T','W','T','F','S']\">{{ day }}</div>\r\n </div>\r\n\r\n <!-- Days Grid -->\r\n <div class=\"calendar-grid days-grid\">\r\n <div\r\n *ngFor=\"let day of getDaysInMonth()\"\r\n [class.empty]=\"day === null\"\r\n [class.selected]=\"isSelected(day)\"\r\n (click)=\"selectDate(day)\"\r\n >\r\n {{ day }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n MONTH VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'month'\" class=\"calendar-grid months-grid\">\r\n <div *ngFor=\"let m of months; let i = index\" \r\n (click)=\"selectMonth(i)\" \r\n class=\"month-cell\">\r\n {{ m }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n YEAR VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'year'\" \r\n class=\"calendar-grid years-grid scrollable\" \r\n #yearList>\r\n <div\r\n *ngFor=\"let y of getYearsRange()\"\r\n (click)=\"selectYear(y)\"\r\n class=\"year-cell\"\r\n [class.current-year]=\"y === currentMonth.getFullYear()\"\r\n >\r\n {{ y }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n Footer Buttons\r\n ---------------------- -->\r\n <div class=\"calendar-footer\">\r\n <button class=\"footer-btn clear-btn\" (click)=\"clearDate()\">Clear</button>\r\n <button class=\"footer-btn today-btn\" (click)=\"setToday()\">Today</button>\r\n </div>\r\n\r\n </div>\r\n</div>\r\n", styles: [".datepicker-wrapper{position:relative;display:flex;flex-direction:column;gap:8px}.datepicker-input{padding:10px;font-size:15px;width:190px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background-color:#fff}.datepicker-input:hover{border:1px solid #071d35}.datepicker-input.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.datepicker-input.disabled .selected-option{pointer-events:none;color:#aaa}.calendar{position:absolute;top:0;left:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 10px #0000001a;padding:10px;z-index:100;animation:fadeIn .2s ease-in-out}.calendar-header{margin-bottom:8px}.header-row{display:flex;justify-content:center;align-items:center;gap:4px}.nav-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:4px 6px;color:#333;border-radius:4px;transition:background-color .2s}.nav-btn:hover{background-color:#f0f0f0}.calendar-title{font-weight:600;font-size:16px;min-width:90px;text-align:center;display:flex;flex:1;gap:4px;align-items:center;justify-content:center}.month-label,.year-label{cursor:pointer;transition:color .2s}.month-label:hover,.year-label:hover{color:#007bff}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;margin-bottom:4px;color:#666}.calendar-grid{display:grid;gap:4px;text-align:center;min-height:200px}.days-grid{grid-template-columns:repeat(7,1fr)}.months-grid{grid-template-columns:repeat(3,1fr)}.years-grid{grid-template-columns:repeat(4,1fr)}.scrollable{max-height:160px;overflow-y:auto;padding-right:5px}.scrollable::-webkit-scrollbar{width:6px}.scrollable::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#999}.calendar-grid div{padding:8px 0;border-radius:4px;cursor:pointer;transition:background-color .2s}.calendar-grid div:hover{background-color:#f0f0f0}.selected{background-color:#007bff;color:#fff!important}.current-year{background-color:#007bff1a;font-weight:700}.empty{visibility:hidden}.calendar-footer{display:flex;justify-content:space-between;margin-top:8px;border-top:1px solid #eee;padding-top:8px}.footer-btn{flex:1;padding:6px 0;font-size:14px;border-radius:4px;cursor:pointer;transition:background .2s;border:none}.clear-btn{background-color:#f8f9fa;color:#333;margin-right:5px}.clear-btn:hover{background-color:#e9ecef}.today-btn{background-color:#007bff;color:#fff}.today-btn:hover{background-color:#0069d9}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n"] }]
|
|
437
|
+
}], propDecorators: { inputTextLabel: [{
|
|
438
|
+
type: Input
|
|
439
|
+
}], isDisabled: [{
|
|
440
|
+
type: Input
|
|
441
|
+
}], yearListRef: [{
|
|
442
|
+
type: ViewChild,
|
|
443
|
+
args: ['yearList']
|
|
444
|
+
}], calendarWrapper: [{
|
|
445
|
+
type: ViewChild,
|
|
446
|
+
args: ['calendarWrapper', { static: false }]
|
|
447
|
+
}], handleClickOutside: [{
|
|
448
|
+
type: HostListener,
|
|
449
|
+
args: ['document:click', ['$event']]
|
|
450
|
+
}] } });
|
|
451
|
+
|
|
452
|
+
class DialogComponent {
|
|
453
|
+
visible = false;
|
|
454
|
+
title = 'Dialog Title';
|
|
455
|
+
close = new EventEmitter();
|
|
456
|
+
onClose() {
|
|
457
|
+
this.close.emit();
|
|
458
|
+
}
|
|
459
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
460
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DialogComponent, isStandalone: true, selector: "dso-dialog", inputs: { visible: "visible", title: "title" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"dialog-overlay\" *ngIf=\"visible\" (click)=\"onClose()\">\r\n <div class=\"dialog\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"dialog-header\">{{ title }}</div>\r\n <div class=\"dialog-body\">\r\n <ng-content></ng-content> <!-- Projected content -->\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".dialog-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;padding:24px;border-radius:8px;width:400px;max-width:90%;box-shadow:0 2px 8px #00000040;position:relative;display:flex;flex-direction:column;gap:12px}.dialog-header{font-size:20px;font-weight:700}.dialog-close{background:none;border:none;font-size:1.5rem;line-height:1;cursor:pointer}.dialog-body{display:flex;flex-direction:column;gap:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
461
|
+
}
|
|
462
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DialogComponent, decorators: [{
|
|
463
|
+
type: Component,
|
|
464
|
+
args: [{ selector: 'dso-dialog', standalone: true, imports: [CommonModule], template: "<div class=\"dialog-overlay\" *ngIf=\"visible\" (click)=\"onClose()\">\r\n <div class=\"dialog\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"dialog-header\">{{ title }}</div>\r\n <div class=\"dialog-body\">\r\n <ng-content></ng-content> <!-- Projected content -->\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".dialog-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;padding:24px;border-radius:8px;width:400px;max-width:90%;box-shadow:0 2px 8px #00000040;position:relative;display:flex;flex-direction:column;gap:12px}.dialog-header{font-size:20px;font-weight:700}.dialog-close{background:none;border:none;font-size:1.5rem;line-height:1;cursor:pointer}.dialog-body{display:flex;flex-direction:column;gap:16px}\n"] }]
|
|
465
|
+
}], propDecorators: { visible: [{
|
|
466
|
+
type: Input
|
|
467
|
+
}], title: [{
|
|
468
|
+
type: Input
|
|
469
|
+
}], close: [{
|
|
470
|
+
type: Output
|
|
471
|
+
}] } });
|
|
472
|
+
|
|
473
|
+
class DsoTruncateDirective {
|
|
474
|
+
el;
|
|
475
|
+
/**
|
|
476
|
+
* Enable or disable truncation.
|
|
477
|
+
* Can be used as `<div dsoTruncate>` (enabled by default) or `[dsoTruncate]="false"`.
|
|
478
|
+
*/
|
|
479
|
+
enableTruncate = true;
|
|
480
|
+
/** Optional: override tooltip text when text is truncated */
|
|
481
|
+
truncateTooltipText;
|
|
482
|
+
constructor(el) {
|
|
483
|
+
this.el = el;
|
|
484
|
+
}
|
|
485
|
+
/** Initialize truncation after the view is loaded */
|
|
486
|
+
ngAfterViewInit() {
|
|
487
|
+
if (this.enableTruncate) {
|
|
488
|
+
this.applyTruncationStyles();
|
|
489
|
+
this.updateTooltip();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** Re-check overflow and update tooltip on window resize */
|
|
493
|
+
onResize() {
|
|
494
|
+
if (this.enableTruncate) {
|
|
495
|
+
this.updateTooltip();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// -------------------------
|
|
499
|
+
// PRIVATE METHODS
|
|
500
|
+
// -------------------------
|
|
501
|
+
/** Apply CSS styles necessary for truncation */
|
|
502
|
+
applyTruncationStyles() {
|
|
503
|
+
const element = this.el.nativeElement;
|
|
504
|
+
element.style.whiteSpace = 'nowrap';
|
|
505
|
+
element.style.overflow = 'hidden';
|
|
506
|
+
element.style.textOverflow = 'ellipsis';
|
|
507
|
+
element.style.display = 'inline-block';
|
|
508
|
+
element.style.maxWidth = '100%';
|
|
509
|
+
}
|
|
510
|
+
/** Update tooltip based on overflow state */
|
|
511
|
+
updateTooltip() {
|
|
512
|
+
const el = this.el.nativeElement;
|
|
513
|
+
const isOverflowing = el.scrollWidth > el.clientWidth;
|
|
514
|
+
if (isOverflowing) {
|
|
515
|
+
// Add or update tooltip with either the provided text or element content
|
|
516
|
+
el.setAttribute('dsoDirectiveTooltip', this.truncateTooltipText || el.textContent || '');
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
// Remove tooltip if content fits
|
|
520
|
+
el.removeAttribute('dsoDirectiveTooltip');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DsoTruncateDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
524
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: DsoTruncateDirective, isStandalone: true, selector: "[dsoTruncate]", inputs: { enableTruncate: ["dsoTruncate", "enableTruncate"], truncateTooltipText: "truncateTooltipText" }, host: { listeners: { "window:resize": "onResize()" } }, ngImport: i0 });
|
|
525
|
+
}
|
|
526
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DsoTruncateDirective, decorators: [{
|
|
527
|
+
type: Directive,
|
|
528
|
+
args: [{
|
|
529
|
+
selector: '[dsoTruncate]',
|
|
530
|
+
standalone: true
|
|
531
|
+
}]
|
|
532
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { enableTruncate: [{
|
|
533
|
+
type: Input,
|
|
534
|
+
args: ['dsoTruncate']
|
|
535
|
+
}], truncateTooltipText: [{
|
|
536
|
+
type: Input
|
|
537
|
+
}], onResize: [{
|
|
538
|
+
type: HostListener,
|
|
539
|
+
args: ['window:resize']
|
|
540
|
+
}] } });
|
|
541
|
+
|
|
542
|
+
class DropdownListComponent {
|
|
543
|
+
// -------------------------------
|
|
544
|
+
// ELEMENT REFERENCE
|
|
545
|
+
// -------------------------------
|
|
546
|
+
/** Reference to the root <div> to detect clicks inside/outside dropdown */
|
|
547
|
+
root;
|
|
548
|
+
// -------------------------------
|
|
549
|
+
// INPUT PROPERTIES
|
|
550
|
+
// -------------------------------
|
|
551
|
+
/** Options to display in the dropdown. Each option has a label and a value */
|
|
552
|
+
options = [];
|
|
553
|
+
/** Placeholder text when no option is selected */
|
|
554
|
+
placeholder = 'Select';
|
|
555
|
+
/** Currently selected value */
|
|
556
|
+
value = null;
|
|
557
|
+
/** Disable dropdown interaction */
|
|
558
|
+
disabled = false;
|
|
559
|
+
// -------------------------------
|
|
560
|
+
// OUTPUT EVENTS
|
|
561
|
+
// -------------------------------
|
|
562
|
+
/** Emits the selected value when user clicks an option */
|
|
563
|
+
selectionChange = new EventEmitter();
|
|
564
|
+
// -------------------------------
|
|
565
|
+
// INTERNAL STATE
|
|
566
|
+
// -------------------------------
|
|
567
|
+
/** Tracks whether the dropdown list is open or closed */
|
|
568
|
+
isOpen = false;
|
|
569
|
+
// -------------------------------
|
|
570
|
+
// METHODS
|
|
571
|
+
// -------------------------------
|
|
572
|
+
/** Toggle dropdown open/close when trigger is clicked */
|
|
573
|
+
toggle() {
|
|
574
|
+
if (!this.disabled) {
|
|
575
|
+
this.isOpen = !this.isOpen;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** Handle option selection */
|
|
579
|
+
select(option) {
|
|
580
|
+
this.value = option.value; // Update internal state
|
|
581
|
+
this.selectionChange.emit(this.value); // Notify parent component
|
|
582
|
+
this.isOpen = false; // Close dropdown
|
|
583
|
+
}
|
|
584
|
+
// -------------------------------
|
|
585
|
+
// HANDLE CLICKS OUTSIDE DROPDOWN
|
|
586
|
+
// -------------------------------
|
|
587
|
+
handleOutsideClick(event) {
|
|
588
|
+
if (!this.isOpen)
|
|
589
|
+
return;
|
|
590
|
+
const target = event.target;
|
|
591
|
+
if (!this.root.nativeElement.contains(target)) {
|
|
592
|
+
this.isOpen = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// -------------------------------
|
|
596
|
+
// HELPER GETTER
|
|
597
|
+
// -------------------------------
|
|
598
|
+
/** Returns label of selected value or placeholder if none selected */
|
|
599
|
+
get selectedLabel() {
|
|
600
|
+
const found = this.options.find(o => o.value === this.value);
|
|
601
|
+
return found ? found.label : this.placeholder;
|
|
602
|
+
}
|
|
603
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropdownListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
604
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DropdownListComponent, isStandalone: true, selector: "dso-dropdown-list", inputs: { options: "options", placeholder: "placeholder", value: "value", disabled: "disabled" }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "document:click": "handleOutsideClick($event)" } }, viewQueries: [{ propertyName: "root", first: true, predicate: ["root"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"dropdown\" #root [class.disabled]=\"disabled\">\r\n <!--\r\n #root: Linked to ViewChild in TS for detecting outside clicks\r\n [class.disabled]: Adds CSS class when dropdown is disabled\r\n -->\r\n\r\n <!-- Trigger area: clicking toggles dropdown open/close -->\r\n <div class=\"dropdown-trigger\" (click)=\"toggle()\">\r\n <!-- Display selected label or placeholder -->\r\n <span>{{ selectedLabel }}</span>\r\n <!-- Arrow icon that rotates when dropdown is open -->\r\n <span class=\"icon\" [class.open]=\"isOpen\">\u25BE</span>\r\n </div>\r\n\r\n <!-- Dropdown menu: visible only when isOpen is true -->\r\n <ul class=\"dropdown-menu\" *ngIf=\"isOpen\">\r\n <!-- Render each option -->\r\n <li \r\n *ngFor=\"let option of options\"\r\n (click)=\"select(option)\"\r\n [class.selected]=\"option.value === value\"> <!-- Highlight selected option -->\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n</div>\r\n", styles: [".dropdown{position:relative;display:inline-flex;font-family:inherit}.dropdown.disabled{opacity:.5;pointer-events:none}.dropdown-trigger{border:1px solid #ccc;padding:8px 12px;background:#fff;cursor:pointer;display:inline-flex;gap:8px;align-items:center;border-radius:4px}.dropdown-trigger:hover{border-color:#888}.dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid #ccc;border-radius:4px;list-style:none;padding:4px 0;margin:0;max-height:200px;overflow-y:auto;z-index:100}.dropdown-menu li{padding:8px 12px;cursor:pointer}.dropdown-menu li:hover{background:#f2f2f2}.dropdown-menu li.selected{background:#e6e6e6;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
605
|
+
}
|
|
606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropdownListComponent, decorators: [{
|
|
607
|
+
type: Component,
|
|
608
|
+
args: [{ selector: 'dso-dropdown-list', standalone: true, imports: [CommonModule], template: "<div class=\"dropdown\" #root [class.disabled]=\"disabled\">\r\n <!--\r\n #root: Linked to ViewChild in TS for detecting outside clicks\r\n [class.disabled]: Adds CSS class when dropdown is disabled\r\n -->\r\n\r\n <!-- Trigger area: clicking toggles dropdown open/close -->\r\n <div class=\"dropdown-trigger\" (click)=\"toggle()\">\r\n <!-- Display selected label or placeholder -->\r\n <span>{{ selectedLabel }}</span>\r\n <!-- Arrow icon that rotates when dropdown is open -->\r\n <span class=\"icon\" [class.open]=\"isOpen\">\u25BE</span>\r\n </div>\r\n\r\n <!-- Dropdown menu: visible only when isOpen is true -->\r\n <ul class=\"dropdown-menu\" *ngIf=\"isOpen\">\r\n <!-- Render each option -->\r\n <li \r\n *ngFor=\"let option of options\"\r\n (click)=\"select(option)\"\r\n [class.selected]=\"option.value === value\"> <!-- Highlight selected option -->\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n</div>\r\n", styles: [".dropdown{position:relative;display:inline-flex;font-family:inherit}.dropdown.disabled{opacity:.5;pointer-events:none}.dropdown-trigger{border:1px solid #ccc;padding:8px 12px;background:#fff;cursor:pointer;display:inline-flex;gap:8px;align-items:center;border-radius:4px}.dropdown-trigger:hover{border-color:#888}.dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid #ccc;border-radius:4px;list-style:none;padding:4px 0;margin:0;max-height:200px;overflow-y:auto;z-index:100}.dropdown-menu li{padding:8px 12px;cursor:pointer}.dropdown-menu li:hover{background:#f2f2f2}.dropdown-menu li.selected{background:#e6e6e6;font-weight:500}\n"] }]
|
|
609
|
+
}], propDecorators: { root: [{
|
|
610
|
+
type: ViewChild,
|
|
611
|
+
args: ['root', { static: true }]
|
|
612
|
+
}], options: [{
|
|
613
|
+
type: Input
|
|
614
|
+
}], placeholder: [{
|
|
615
|
+
type: Input
|
|
616
|
+
}], value: [{
|
|
617
|
+
type: Input
|
|
618
|
+
}], disabled: [{
|
|
619
|
+
type: Input
|
|
620
|
+
}], selectionChange: [{
|
|
621
|
+
type: Output
|
|
622
|
+
}], handleOutsideClick: [{
|
|
623
|
+
type: HostListener,
|
|
624
|
+
args: ['document:click', ['$event']]
|
|
625
|
+
}] } });
|
|
626
|
+
|
|
627
|
+
class ProgressBarComponent {
|
|
628
|
+
/** Progress value (0–100). If null, shows indeterminate animation. */
|
|
629
|
+
value = null;
|
|
630
|
+
/** Color variants */
|
|
631
|
+
color = 'primary';
|
|
632
|
+
/** Optional animation toggle */
|
|
633
|
+
animated = true;
|
|
634
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProgressBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
635
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ProgressBarComponent, isStandalone: true, selector: "dso-progress-bar", inputs: { value: "value", color: "color", animated: "animated" }, ngImport: i0, template: "<div class=\"progress-bar-container\">\r\n <div\r\n class=\"progress-bar-fill\"\r\n [ngClass]=\"[\r\n color,\r\n animated ? 'animated' : '',\r\n value === null ? 'indeterminate' : ''\r\n ]\"\r\n [style.width.%]=\"value !== null ? value : 50\"\r\n ></div>\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;position:relative;min-width:240px}.progress-bar-fill{position:absolute;height:100%;border-radius:4px;transition:width .4s ease}.progress-bar-fill.primary{background-color:#007bff}.progress-bar-fill.success{background-color:#28a745}.progress-bar-fill.warning{background-color:#ffc107}.progress-bar-fill.danger{background-color:#dc3545}.progress-bar-fill.animated{transition:width .4s ease-in-out}.progress-bar-fill.indeterminate{width:30%;animation:scrollLoop 1.5s linear infinite}@keyframes scrollLoop{0%{transform:translate(-100%)}to{transform:translate(400%)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
636
|
+
}
|
|
637
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProgressBarComponent, decorators: [{
|
|
638
|
+
type: Component,
|
|
639
|
+
args: [{ selector: 'dso-progress-bar', standalone: true, imports: [CommonModule], template: "<div class=\"progress-bar-container\">\r\n <div\r\n class=\"progress-bar-fill\"\r\n [ngClass]=\"[\r\n color,\r\n animated ? 'animated' : '',\r\n value === null ? 'indeterminate' : ''\r\n ]\"\r\n [style.width.%]=\"value !== null ? value : 50\"\r\n ></div>\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;position:relative;min-width:240px}.progress-bar-fill{position:absolute;height:100%;border-radius:4px;transition:width .4s ease}.progress-bar-fill.primary{background-color:#007bff}.progress-bar-fill.success{background-color:#28a745}.progress-bar-fill.warning{background-color:#ffc107}.progress-bar-fill.danger{background-color:#dc3545}.progress-bar-fill.animated{transition:width .4s ease-in-out}.progress-bar-fill.indeterminate{width:30%;animation:scrollLoop 1.5s linear infinite}@keyframes scrollLoop{0%{transform:translate(-100%)}to{transform:translate(400%)}}\n"] }]
|
|
640
|
+
}], propDecorators: { value: [{
|
|
641
|
+
type: Input
|
|
642
|
+
}], color: [{
|
|
643
|
+
type: Input
|
|
644
|
+
}], animated: [{
|
|
645
|
+
type: Input
|
|
646
|
+
}] } });
|
|
647
|
+
|
|
648
|
+
class FileUploadItemComponent {
|
|
649
|
+
/** The file upload item to display */
|
|
650
|
+
item;
|
|
651
|
+
/** Emitted when the user retries a failed upload */
|
|
652
|
+
retry = new EventEmitter();
|
|
653
|
+
/** Emitted when the user removes the file from the list */
|
|
654
|
+
remove = new EventEmitter();
|
|
655
|
+
/** Emitted when the user cancels an ongoing upload */
|
|
656
|
+
cancel = new EventEmitter();
|
|
657
|
+
/**
|
|
658
|
+
* Format the file size into a human-readable string
|
|
659
|
+
* @param size - size in bytes
|
|
660
|
+
* @returns formatted string like '12.3 KB', '1.5 MB'
|
|
661
|
+
*/
|
|
662
|
+
formatSize(size) {
|
|
663
|
+
if (size < 1024)
|
|
664
|
+
return size + ' B';
|
|
665
|
+
if (size < 1024 * 1024)
|
|
666
|
+
return (size / 1024).toFixed(1) + ' KB';
|
|
667
|
+
return (size / (1024 * 1024)).toFixed(1) + ' MB';
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Get an icon name for the file based on its type or extension
|
|
671
|
+
* @param item - the UploadItem
|
|
672
|
+
* @returns icon name string
|
|
673
|
+
*/
|
|
674
|
+
getFileIcon(item) {
|
|
675
|
+
const file = item.file;
|
|
676
|
+
const type = file.type.toLowerCase();
|
|
677
|
+
const name = file.name.toLowerCase();
|
|
678
|
+
// Image MIME types
|
|
679
|
+
if (type.startsWith('image/')) {
|
|
680
|
+
return 'icon-gallery';
|
|
681
|
+
}
|
|
682
|
+
// Image file extensions fallback
|
|
683
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
|
|
684
|
+
if (imageExtensions.some(ext => name.endsWith(ext))) {
|
|
685
|
+
return 'icon-gallery';
|
|
686
|
+
}
|
|
687
|
+
// Default icon for non-image files
|
|
688
|
+
return 'icon-van';
|
|
689
|
+
}
|
|
690
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
691
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadItemComponent, isStandalone: true, selector: "dso-file-item", inputs: { item: "item" }, outputs: { retry: "retry", remove: "remove", cancel: "cancel" }, ngImport: i0, template: "<div class=\"file-row\">\r\n\r\n <!-- LEFT SECTION: Thumbnail + filename -->\r\n <div class=\"thumbnail-filename-wrapper\">\r\n <div class=\"file-thumb\">\r\n <dso-icon \r\n [iconName]=\"getFileIcon(item)\"\r\n size=\"small\"\r\n class=\"thumb-icon\">\r\n </dso-icon>\r\n </div>\r\n\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ item.file.name }}</div>\r\n <div class=\"file-size\">{{ formatSize(item.file.size) }}</div>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- MIDDLE SECTION: Progress or status -->\r\n <div class=\"filecontent-loader-wrapper\">\r\n \r\n <!-- Progress bar -->\r\n <div class=\"file-progress\" *ngIf=\"!item.hideProgressBar\">\r\n <dso-progress-bar\r\n [value]=\"\r\n item.status === 'uploading' ? item.progress :\r\n item.status === 'completed' ? 100 :\r\n null\r\n \"\r\n [color]=\"item.status === 'completed' ? 'success' : 'primary'\"\r\n ></dso-progress-bar>\r\n </div>\r\n\r\n <!-- Status text -->\r\n <div class=\"file-status\" *ngIf=\"item.hideProgressBar\">\r\n\r\n <!-- Success -->\r\n <div *ngIf=\"item.status === 'completed'\" class=\"status success\">\r\n <dso-icon iconName=\"icon-van\" size=\"small\"></dso-icon>\r\n <span>Uploaded</span>\r\n </div>\r\n\r\n <!-- Failed -->\r\n <div *ngIf=\"item.status === 'failed'\" class=\"status error\">\r\n <dso-icon iconName=\"icon-center-align\" size=\"small\"></dso-icon>\r\n <span>Upload failed</span>\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- RIGHT SECTION: Action buttons -->\r\n <div class=\"file-actions\">\r\n\r\n <!-- Uploading -->\r\n <ng-container *ngIf=\"item.status === 'uploading'\">\r\n <dso-button\r\n btnLabel=\"Cancel\"\r\n btnType=\"text\"\r\n (onClick)=\"cancel.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Failed -->\r\n <ng-container *ngIf=\"item.status === 'failed'\">\r\n <dso-button\r\n btnLabel=\"Retry\"\r\n btnType=\"text\"\r\n (onClick)=\"retry.emit()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Completed -->\r\n <ng-container *ngIf=\"item.status === 'completed'\">\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n</div>\r\n", styles: [".file-row{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:#fafafa;border-radius:4px}.file-info{display:flex;flex-direction:column}.file-info .file-name{font-weight:300}.file-info .file-size{font-size:.85rem;color:#666}.file-progress{padding-right:1rem;flex:1 0 0;display:flex;justify-content:center;align-items:center;min-width:260px;max-width:320px}.file-actions{display:flex;width:160px;gap:.5rem;justify-content:flex-end}.file-status{display:flex;align-items:center;font-size:14px;flex:1 0 0;min-width:260px;max-width:320px}.file-status .status{display:flex;align-items:center;gap:6px;font-weight:500}.file-status .status.success{color:#16a34a}.file-status .status.error{color:#dc2626}.file-status .status .status-icon{width:16px;height:16px}.file-thumb{width:40px;height:40px;flex-shrink:0;border-radius:6px;overflow:hidden;background:#f3f4f6;display:flex;align-items:center;justify-content:center}.file-thumb img{width:100%;height:100%;object-fit:cover}.thumbnail-filename-wrapper{display:flex;gap:24px}.filecontent-loader-wrapper{display:flex;gap:20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "component", type: ProgressBarComponent, selector: "dso-progress-bar", inputs: ["value", "color", "animated"] }] });
|
|
692
|
+
}
|
|
693
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadItemComponent, decorators: [{
|
|
694
|
+
type: Component,
|
|
695
|
+
args: [{ selector: 'dso-file-item', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent, ProgressBarComponent], template: "<div class=\"file-row\">\r\n\r\n <!-- LEFT SECTION: Thumbnail + filename -->\r\n <div class=\"thumbnail-filename-wrapper\">\r\n <div class=\"file-thumb\">\r\n <dso-icon \r\n [iconName]=\"getFileIcon(item)\"\r\n size=\"small\"\r\n class=\"thumb-icon\">\r\n </dso-icon>\r\n </div>\r\n\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ item.file.name }}</div>\r\n <div class=\"file-size\">{{ formatSize(item.file.size) }}</div>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- MIDDLE SECTION: Progress or status -->\r\n <div class=\"filecontent-loader-wrapper\">\r\n \r\n <!-- Progress bar -->\r\n <div class=\"file-progress\" *ngIf=\"!item.hideProgressBar\">\r\n <dso-progress-bar\r\n [value]=\"\r\n item.status === 'uploading' ? item.progress :\r\n item.status === 'completed' ? 100 :\r\n null\r\n \"\r\n [color]=\"item.status === 'completed' ? 'success' : 'primary'\"\r\n ></dso-progress-bar>\r\n </div>\r\n\r\n <!-- Status text -->\r\n <div class=\"file-status\" *ngIf=\"item.hideProgressBar\">\r\n\r\n <!-- Success -->\r\n <div *ngIf=\"item.status === 'completed'\" class=\"status success\">\r\n <dso-icon iconName=\"icon-van\" size=\"small\"></dso-icon>\r\n <span>Uploaded</span>\r\n </div>\r\n\r\n <!-- Failed -->\r\n <div *ngIf=\"item.status === 'failed'\" class=\"status error\">\r\n <dso-icon iconName=\"icon-center-align\" size=\"small\"></dso-icon>\r\n <span>Upload failed</span>\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- RIGHT SECTION: Action buttons -->\r\n <div class=\"file-actions\">\r\n\r\n <!-- Uploading -->\r\n <ng-container *ngIf=\"item.status === 'uploading'\">\r\n <dso-button\r\n btnLabel=\"Cancel\"\r\n btnType=\"text\"\r\n (onClick)=\"cancel.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Failed -->\r\n <ng-container *ngIf=\"item.status === 'failed'\">\r\n <dso-button\r\n btnLabel=\"Retry\"\r\n btnType=\"text\"\r\n (onClick)=\"retry.emit()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Completed -->\r\n <ng-container *ngIf=\"item.status === 'completed'\">\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n</div>\r\n", styles: [".file-row{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:#fafafa;border-radius:4px}.file-info{display:flex;flex-direction:column}.file-info .file-name{font-weight:300}.file-info .file-size{font-size:.85rem;color:#666}.file-progress{padding-right:1rem;flex:1 0 0;display:flex;justify-content:center;align-items:center;min-width:260px;max-width:320px}.file-actions{display:flex;width:160px;gap:.5rem;justify-content:flex-end}.file-status{display:flex;align-items:center;font-size:14px;flex:1 0 0;min-width:260px;max-width:320px}.file-status .status{display:flex;align-items:center;gap:6px;font-weight:500}.file-status .status.success{color:#16a34a}.file-status .status.error{color:#dc2626}.file-status .status .status-icon{width:16px;height:16px}.file-thumb{width:40px;height:40px;flex-shrink:0;border-radius:6px;overflow:hidden;background:#f3f4f6;display:flex;align-items:center;justify-content:center}.file-thumb img{width:100%;height:100%;object-fit:cover}.thumbnail-filename-wrapper{display:flex;gap:24px}.filecontent-loader-wrapper{display:flex;gap:20px}\n"] }]
|
|
696
|
+
}], propDecorators: { item: [{
|
|
697
|
+
type: Input
|
|
698
|
+
}], retry: [{
|
|
699
|
+
type: Output
|
|
700
|
+
}], remove: [{
|
|
701
|
+
type: Output
|
|
702
|
+
}], cancel: [{
|
|
703
|
+
type: Output
|
|
704
|
+
}] } });
|
|
705
|
+
|
|
706
|
+
// upload-simulator.service.ts
|
|
707
|
+
/**
|
|
708
|
+
* UploadSimulatorService
|
|
709
|
+
* -----------------------
|
|
710
|
+
* This service simulates an upload process.
|
|
711
|
+
* It is ONLY for demo and UI development. Later, it can
|
|
712
|
+
* be replaced with a real HTTP upload service without
|
|
713
|
+
* changing the UI components.
|
|
714
|
+
*
|
|
715
|
+
* How it works:
|
|
716
|
+
* - Every 300ms, a timer increases the upload progress.
|
|
717
|
+
* - Progress increases by a random amount (to look realistic).
|
|
718
|
+
* - When it reaches 100%, the upload is marked complete.
|
|
719
|
+
* - There is a 10% chance the upload "fails" to simulate errors.
|
|
720
|
+
* - If an upload is cancelled, the interval stops immediately.
|
|
721
|
+
*
|
|
722
|
+
* This pattern mimics:
|
|
723
|
+
* - progress events (like HttpClient upload events)
|
|
724
|
+
* - cancellation (AbortController / unsubscribe)
|
|
725
|
+
*/
|
|
726
|
+
class UploadSimulatorService {
|
|
727
|
+
/**
|
|
728
|
+
* Simulates uploading a single file.
|
|
729
|
+
*
|
|
730
|
+
* @param item The UploadItem being uploaded
|
|
731
|
+
* @param onProgress Callback fired whenever progress changes
|
|
732
|
+
* @param onComplete Callback fired when upload reaches 100%
|
|
733
|
+
* @param onError Callback fired when upload "fails"
|
|
734
|
+
*/
|
|
735
|
+
simulateUpload(item, onProgress, onComplete, onError) {
|
|
736
|
+
console.log('%c[SIMULATOR] Starting upload:', 'color: green;', item.file.name);
|
|
737
|
+
let progress = 0;
|
|
738
|
+
// This interval simulates a real upload stream
|
|
739
|
+
const interval = setInterval(() => {
|
|
740
|
+
// If cancelled externally, we stop updating
|
|
741
|
+
if (item.status === 'cancelled') {
|
|
742
|
+
console.log('%c[SIMULATOR] Upload CANCELLED:', 'color: orange;', item.file.name);
|
|
743
|
+
clearInterval(interval);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// 🔼 Simulate uploading by adding random progress increments
|
|
747
|
+
const increment = Math.random() * 15 + 5;
|
|
748
|
+
progress += increment;
|
|
749
|
+
console.log(`%c[SIMULATOR] ${item.file.name} +${increment.toFixed(1)}% → ${progress.toFixed(1)}%`, 'color: #1e90ff;');
|
|
750
|
+
// 🟦 If progress reaches 100%, finalize upload
|
|
751
|
+
if (progress >= 100) {
|
|
752
|
+
progress = 100;
|
|
753
|
+
onProgress(progress);
|
|
754
|
+
clearInterval(interval);
|
|
755
|
+
console.log('%c[SIMULATOR] Upload reached 100% for ' + item.file.name, 'color: green; font-weight: bold;');
|
|
756
|
+
// 🔥 50% chance to simulate failure
|
|
757
|
+
const randomFail = Math.random() < 0.5;
|
|
758
|
+
if (randomFail) {
|
|
759
|
+
console.log('%c[SIMULATOR] Simulated FAILURE for ' + item.file.name, 'color: red; font-weight: bold;');
|
|
760
|
+
onError();
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
console.log('%c[SIMULATOR] Upload SUCCESS for ' + item.file.name, 'color: limegreen; font-weight: bold;');
|
|
764
|
+
onComplete();
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Emit progress update
|
|
769
|
+
onProgress(progress);
|
|
770
|
+
}, 300); // runs every 300ms like a real upload tick
|
|
771
|
+
}
|
|
772
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
773
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, providedIn: 'root' });
|
|
774
|
+
}
|
|
775
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, decorators: [{
|
|
776
|
+
type: Injectable,
|
|
777
|
+
args: [{ providedIn: 'root' }]
|
|
778
|
+
}] });
|
|
779
|
+
|
|
780
|
+
// file-upload-multi.component.ts
|
|
781
|
+
/**
|
|
782
|
+
* FileUploadMultiComponent
|
|
783
|
+
* -------------------------
|
|
784
|
+
* This component handles:
|
|
785
|
+
* - Drag & drop file uploads
|
|
786
|
+
* - Selecting multiple files through <input type="file">
|
|
787
|
+
* - Upload progress per file
|
|
788
|
+
* - Cancelling an upload
|
|
789
|
+
* - Retrying failed uploads
|
|
790
|
+
* - Removing items from the queue
|
|
791
|
+
*
|
|
792
|
+
* UploadSimulatorService is used to mimic real upload behavior.
|
|
793
|
+
* Later, you can replace it with a real HTTP upload service.
|
|
794
|
+
*/
|
|
795
|
+
class FileUploadMultiComponent {
|
|
796
|
+
sim;
|
|
797
|
+
// ngOnInit() {
|
|
798
|
+
// // Fake completed file
|
|
799
|
+
// this.uploadQueue.push({
|
|
800
|
+
// file: new File([""], "example-image.jpg", { type: "image/jpeg" }),
|
|
801
|
+
// progress: 100,
|
|
802
|
+
// status: "completed",
|
|
803
|
+
// hideProgressBar: true
|
|
804
|
+
// });
|
|
805
|
+
// // Fake failed file
|
|
806
|
+
// this.uploadQueue.push({
|
|
807
|
+
// file: new File([""], "broken-file.pdf", { type: "application/pdf" }),
|
|
808
|
+
// progress: null,
|
|
809
|
+
// status: "failed",
|
|
810
|
+
// hideProgressBar: true
|
|
811
|
+
// });
|
|
812
|
+
// // Fake uploading file
|
|
813
|
+
// this.uploadQueue.push({
|
|
814
|
+
// file: new File([""], "uploading.png", { type: "image/png" }),
|
|
815
|
+
// progress: 40,
|
|
816
|
+
// status: "uploading",
|
|
817
|
+
// hideProgressBar: false
|
|
818
|
+
// });
|
|
819
|
+
// }
|
|
820
|
+
/** All files being managed (uploaded, uploading, failed, completed). */
|
|
821
|
+
uploadQueue = [];
|
|
822
|
+
/** True while user drags a file over the drop zone. */
|
|
823
|
+
isDragOver = false;
|
|
824
|
+
constructor(sim) {
|
|
825
|
+
this.sim = sim;
|
|
826
|
+
// console.log('%c[COMPONENT] FileUploadMultiComponent initialized', 'color: purple;');
|
|
827
|
+
}
|
|
828
|
+
/* ===========================================================
|
|
829
|
+
* DRAG & DROP EVENTS
|
|
830
|
+
* =========================================================== */
|
|
831
|
+
onDragOver(event) {
|
|
832
|
+
event.preventDefault();
|
|
833
|
+
this.isDragOver = true;
|
|
834
|
+
// console.log('%c[DRAG] Dragging over drop zone...', 'color: #00aaff;');
|
|
835
|
+
}
|
|
836
|
+
onDragLeave(event) {
|
|
837
|
+
this.isDragOver = false;
|
|
838
|
+
// console.log('%c[DRAG] Drag left drop zone', 'color: #888;');
|
|
839
|
+
}
|
|
840
|
+
onDrop(event) {
|
|
841
|
+
event.preventDefault();
|
|
842
|
+
this.isDragOver = false;
|
|
843
|
+
// console.log('%c[DRAG] Files dropped', 'color: #00cc66;');
|
|
844
|
+
if (event.dataTransfer?.files) {
|
|
845
|
+
// console.log('[DRAG] Dropped files:', event.dataTransfer.files);
|
|
846
|
+
this.handleFiles(event.dataTransfer.files);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/* ===========================================================
|
|
850
|
+
* FILE INPUT SELECT
|
|
851
|
+
* =========================================================== */
|
|
852
|
+
onFileSelected(event) {
|
|
853
|
+
const input = event.target;
|
|
854
|
+
if (input.files) {
|
|
855
|
+
// console.log('%c[INPUT] Files selected:', 'color: #0099ff;', input.files);
|
|
856
|
+
this.handleFiles(input.files);
|
|
857
|
+
// Clears <input> so user can re-select same file
|
|
858
|
+
input.value = '';
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/* ===========================================================
|
|
862
|
+
* PROCESSING NEWLY ADDED FILES
|
|
863
|
+
* =========================================================== */
|
|
864
|
+
handleFiles(fileList) {
|
|
865
|
+
// console.log('%c[FILES] Processing new files...', 'color: #ffaa00;');
|
|
866
|
+
Array.from(fileList).forEach(file => {
|
|
867
|
+
// console.log(`%c[FILES] Added: ${file.name} (${file.size} bytes)`, 'color: #ffaa00;');
|
|
868
|
+
const item = {
|
|
869
|
+
file,
|
|
870
|
+
progress: 0,
|
|
871
|
+
status: 'queued'
|
|
872
|
+
};
|
|
873
|
+
this.uploadQueue.push(item);
|
|
874
|
+
this.startUpload(item);
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/* ===========================================================
|
|
878
|
+
* UPLOAD LOGIC
|
|
879
|
+
* =========================================================== */
|
|
880
|
+
startUpload(item) {
|
|
881
|
+
// console.log(`%c[UPLOAD] Starting upload for ${item.file.name}`, 'color: #00cc99;');
|
|
882
|
+
item.status = 'uploading';
|
|
883
|
+
item.progress = 0;
|
|
884
|
+
this.sim.simulateUpload(item,
|
|
885
|
+
// ---- onProgress callback ----
|
|
886
|
+
(progress) => {
|
|
887
|
+
item.progress = progress;
|
|
888
|
+
// console.log(`%c[UPLOAD] ${item.file.name}: ${progress.toFixed(1)}%`, 'color: #0099ff;');
|
|
889
|
+
},
|
|
890
|
+
// ---- onComplete callback ----
|
|
891
|
+
() => {
|
|
892
|
+
item.status = 'completed';
|
|
893
|
+
item.progress = 100;
|
|
894
|
+
// Hide progress bar after a very small delay (100–300ms)
|
|
895
|
+
setTimeout(() => item.hideProgressBar = true, 300);
|
|
896
|
+
// console.log(`%c[UPLOAD] COMPLETED: ${item.file.name}`, 'color: green; font-weight: bold;');
|
|
897
|
+
},
|
|
898
|
+
// ---- onError callback ----
|
|
899
|
+
() => {
|
|
900
|
+
item.status = 'failed';
|
|
901
|
+
item.progress = null;
|
|
902
|
+
// Hide progress bar so status text can show
|
|
903
|
+
setTimeout(() => item.hideProgressBar = true, 300);
|
|
904
|
+
// console.log(`%c[UPLOAD] FAILED: ${item.file.name}`, 'color: red; font-weight: bold;');
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
onDropZoneClick(fileInput) {
|
|
908
|
+
// Prevent click if user is dragging files over the drop-zone
|
|
909
|
+
if (this.isDragOver)
|
|
910
|
+
return;
|
|
911
|
+
console.log('%c[CLICK] Drop zone clicked → opening file dialog', 'color: #0077ff;');
|
|
912
|
+
fileInput.click();
|
|
913
|
+
}
|
|
914
|
+
onBrowseButtonClick(event, fileInput) {
|
|
915
|
+
event.stopPropagation(); // stops click from bubbling to drop-zone
|
|
916
|
+
// console.log('%c[BUTTON] Browse clicked → opening file dialog', 'color: #8844ff;');
|
|
917
|
+
fileInput.click();
|
|
918
|
+
}
|
|
919
|
+
/* ===========================================================
|
|
920
|
+
* BUTTON ACTIONS (CANCEL / RETRY / REMOVE)
|
|
921
|
+
* =========================================================== */
|
|
922
|
+
cancelUpload(item) {
|
|
923
|
+
// console.log(`%c[CANCEL] Cancelling ${item.file.name}`, 'color: orange; font-weight: bold;');
|
|
924
|
+
// item.status = 'cancelled';
|
|
925
|
+
// item.progress = null;
|
|
926
|
+
this.removeItem(item);
|
|
927
|
+
}
|
|
928
|
+
retryUpload(item) {
|
|
929
|
+
// console.log('[RETRY] Restarting upload for', item.file.name);
|
|
930
|
+
// Reset state
|
|
931
|
+
item.status = 'queued';
|
|
932
|
+
item.progress = 0;
|
|
933
|
+
item.hideProgressBar = false; // <-- IMPORTANT
|
|
934
|
+
// Start upload again
|
|
935
|
+
this.startUpload(item);
|
|
936
|
+
}
|
|
937
|
+
removeItem(item) {
|
|
938
|
+
// console.log(`%c[REMOVE] Removing ${item.file.name}`, 'color: gray;');
|
|
939
|
+
this.uploadQueue = this.uploadQueue.filter(i => i !== item);
|
|
940
|
+
}
|
|
941
|
+
/* ===========================================================
|
|
942
|
+
* UTILITY FUNCTIONS
|
|
943
|
+
* =========================================================== */
|
|
944
|
+
/**
|
|
945
|
+
* Converts bytes → human readable sizes.
|
|
946
|
+
*/
|
|
947
|
+
formatSize(size) {
|
|
948
|
+
if (size < 1024)
|
|
949
|
+
return size + ' B';
|
|
950
|
+
if (size < 1024 * 1024)
|
|
951
|
+
return (size / 1024).toFixed(1) + ' KB';
|
|
952
|
+
return (size / (1024 * 1024)).toFixed(1) + ' MB';
|
|
953
|
+
}
|
|
954
|
+
// getThumbnail(item: UploadItem): string | null {
|
|
955
|
+
// const file = item.file;
|
|
956
|
+
// // If actual image → return preview URL
|
|
957
|
+
// if (file.type.startsWith('image/')) {
|
|
958
|
+
// return URL.createObjectURL(file);
|
|
959
|
+
// }
|
|
960
|
+
// // Otherwise return an icon path depending on type
|
|
961
|
+
// if (file.type === 'application/pdf') return 'assets/icons/pdf.svg';
|
|
962
|
+
// if (file.type.includes('word')) return 'assets/icons/doc.svg';
|
|
963
|
+
// if (file.type.includes('spreadsheet')) return 'assets/icons/xlsx.svg';
|
|
964
|
+
// // default icon
|
|
965
|
+
// return 'assets/icons/file.svg';
|
|
966
|
+
// }
|
|
967
|
+
getThumbnail(item) {
|
|
968
|
+
const file = item.file;
|
|
969
|
+
// Image preview for image file types
|
|
970
|
+
if (file.type.startsWith('image/')) {
|
|
971
|
+
return URL.createObjectURL(file);
|
|
972
|
+
}
|
|
973
|
+
// Not an image → no thumbnail
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
getFileIcon(item) {
|
|
977
|
+
const file = item.file;
|
|
978
|
+
const type = file.type.toLowerCase();
|
|
979
|
+
const name = file.name.toLowerCase();
|
|
980
|
+
console.log('%c[DEBUG] File name:', 'color: blue;', file.name);
|
|
981
|
+
console.log('%c[DEBUG] File type:', 'color: green;', file.type);
|
|
982
|
+
// Check MIME type first
|
|
983
|
+
if (type.startsWith('image/')) {
|
|
984
|
+
console.log('%c[DEBUG] Detected as image by MIME type', 'color: purple;');
|
|
985
|
+
return 'icon-gallery';
|
|
986
|
+
}
|
|
987
|
+
// Fallback: check extension for image types including SVG
|
|
988
|
+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
|
|
989
|
+
if (imageExtensions.some(ext => name.endsWith(ext))) {
|
|
990
|
+
console.log('%c[DEBUG] Detected as image by extension', 'color: orange;');
|
|
991
|
+
return 'icon-gallery';
|
|
992
|
+
}
|
|
993
|
+
console.log('%c[DEBUG] Defaulting to document icon', 'color: red;');
|
|
994
|
+
return 'icon-van';
|
|
995
|
+
}
|
|
996
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadMultiComponent, deps: [{ token: UploadSimulatorService }], target: i0.ɵɵFactoryTarget.Component });
|
|
997
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadMultiComponent, isStandalone: true, selector: "dso-file-upload-multi", ngImport: i0, template: "<div class=\"upload-container\">\r\n\r\n <!-- DRAG & DROP AREA -->\r\n <div\r\n class=\"drop-zone\"\r\n [class.drag-over]=\"isDragOver\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (click)=\"onDropZoneClick(fileInput)\"\r\n >\r\n\r\n <dso-button \r\n style=\"padding: 0; \"\r\n btnLabel=\"Select files\"\r\n btnType=\"link\"\r\n (onClick)=\"onBrowseButtonClick($event, fileInput)\">\r\n </dso-button>\r\n or drag & drop files \r\n <input \r\n type=\"file\" \r\n multiple \r\n hidden \r\n #fileInput \r\n (change)=\"onFileSelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- FILE LIST -->\r\n <div class=\"file-list\" *ngIf=\"uploadQueue.length > 0\">\r\n\r\n <dso-file-item\r\n *ngFor=\"let item of uploadQueue\"\r\n [item]=\"item\"\r\n (retry)=\"retryUpload(item)\"\r\n (remove)=\"removeItem(item)\"\r\n (cancel)=\"cancelUpload(item)\"\r\n ></dso-file-item>\r\n\r\n\r\n </div>\r\n</div>\r\n", styles: [".upload-container{display:flex;flex-direction:column;gap:8px}.drop-zone{border:2px dashed #ccc;padding:2rem;text-align:center;border-radius:6px;transition:border-color .2s,background-color .2s;cursor:pointer}.drop-zone.drag-over{border-color:#007bff;background-color:#f0f7ff}.file-list{display:flex;flex-direction:column;gap:.75rem;max-height:360px;overflow-y:auto;padding-right:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: FileUploadItemComponent, selector: "dso-file-item", inputs: ["item"], outputs: ["retry", "remove", "cancel"] }] });
|
|
998
|
+
}
|
|
999
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadMultiComponent, decorators: [{
|
|
1000
|
+
type: Component,
|
|
1001
|
+
args: [{ selector: 'dso-file-upload-multi', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent, FileUploadItemComponent], template: "<div class=\"upload-container\">\r\n\r\n <!-- DRAG & DROP AREA -->\r\n <div\r\n class=\"drop-zone\"\r\n [class.drag-over]=\"isDragOver\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (click)=\"onDropZoneClick(fileInput)\"\r\n >\r\n\r\n <dso-button \r\n style=\"padding: 0; \"\r\n btnLabel=\"Select files\"\r\n btnType=\"link\"\r\n (onClick)=\"onBrowseButtonClick($event, fileInput)\">\r\n </dso-button>\r\n or drag & drop files \r\n <input \r\n type=\"file\" \r\n multiple \r\n hidden \r\n #fileInput \r\n (change)=\"onFileSelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- FILE LIST -->\r\n <div class=\"file-list\" *ngIf=\"uploadQueue.length > 0\">\r\n\r\n <dso-file-item\r\n *ngFor=\"let item of uploadQueue\"\r\n [item]=\"item\"\r\n (retry)=\"retryUpload(item)\"\r\n (remove)=\"removeItem(item)\"\r\n (cancel)=\"cancelUpload(item)\"\r\n ></dso-file-item>\r\n\r\n\r\n </div>\r\n</div>\r\n", styles: [".upload-container{display:flex;flex-direction:column;gap:8px}.drop-zone{border:2px dashed #ccc;padding:2rem;text-align:center;border-radius:6px;transition:border-color .2s,background-color .2s;cursor:pointer}.drop-zone.drag-over{border-color:#007bff;background-color:#f0f7ff}.file-list{display:flex;flex-direction:column;gap:.75rem;max-height:360px;overflow-y:auto;padding-right:4px}\n"] }]
|
|
1002
|
+
}], ctorParameters: () => [{ type: UploadSimulatorService }] });
|
|
1003
|
+
|
|
1004
|
+
class FileUploadSingleComponent {
|
|
1005
|
+
sim;
|
|
1006
|
+
/** Button label text */
|
|
1007
|
+
btnLabel = 'Choose File';
|
|
1008
|
+
/** Allowed file types, e.g. ".png,.jpg,.pdf" */
|
|
1009
|
+
accept = '';
|
|
1010
|
+
/** Emit the selected file to parent component */
|
|
1011
|
+
fileSelected = new EventEmitter();
|
|
1012
|
+
/** The currently uploaded file shown in the UI */
|
|
1013
|
+
item = null;
|
|
1014
|
+
constructor(sim) {
|
|
1015
|
+
this.sim = sim;
|
|
1016
|
+
}
|
|
1017
|
+
/** Trigger the hidden file input click */
|
|
1018
|
+
onButtonClick(input) {
|
|
1019
|
+
input.click();
|
|
1020
|
+
}
|
|
1021
|
+
/** Handle file selection from input */
|
|
1022
|
+
onFileChange(event) {
|
|
1023
|
+
const input = event.target;
|
|
1024
|
+
if (!input.files || input.files.length === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
const file = input.files[0];
|
|
1027
|
+
// Wrap file in UploadItem structure (same as multi-upload)
|
|
1028
|
+
this.item = {
|
|
1029
|
+
file,
|
|
1030
|
+
progress: 0,
|
|
1031
|
+
status: 'queued',
|
|
1032
|
+
hideProgressBar: false
|
|
1033
|
+
};
|
|
1034
|
+
// Emit file to parent
|
|
1035
|
+
this.fileSelected.emit(file);
|
|
1036
|
+
// Start simulated upload
|
|
1037
|
+
this.startUpload();
|
|
1038
|
+
// Reset input so user can pick same file again
|
|
1039
|
+
input.value = '';
|
|
1040
|
+
}
|
|
1041
|
+
/** Start upload simulation */
|
|
1042
|
+
startUpload() {
|
|
1043
|
+
if (!this.item)
|
|
1044
|
+
return;
|
|
1045
|
+
const item = this.item;
|
|
1046
|
+
item.status = 'uploading';
|
|
1047
|
+
item.progress = 0;
|
|
1048
|
+
this.sim.simulateUpload(item,
|
|
1049
|
+
// onProgress callback
|
|
1050
|
+
(progress) => {
|
|
1051
|
+
item.progress = progress;
|
|
1052
|
+
},
|
|
1053
|
+
// onComplete callback
|
|
1054
|
+
() => {
|
|
1055
|
+
item.status = 'completed';
|
|
1056
|
+
item.progress = 100;
|
|
1057
|
+
setTimeout(() => item.hideProgressBar = true, 300);
|
|
1058
|
+
},
|
|
1059
|
+
// onError callback
|
|
1060
|
+
() => {
|
|
1061
|
+
item.status = 'failed';
|
|
1062
|
+
item.progress = null;
|
|
1063
|
+
setTimeout(() => item.hideProgressBar = true, 300);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/** Retry failed upload */
|
|
1067
|
+
retry() {
|
|
1068
|
+
if (!this.item)
|
|
1069
|
+
return;
|
|
1070
|
+
this.item.status = 'queued';
|
|
1071
|
+
this.item.progress = 0;
|
|
1072
|
+
this.item.hideProgressBar = false;
|
|
1073
|
+
this.startUpload();
|
|
1074
|
+
}
|
|
1075
|
+
/** Cancel or remove the file from UI */
|
|
1076
|
+
clearFile() {
|
|
1077
|
+
this.item = null;
|
|
1078
|
+
}
|
|
1079
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadSingleComponent, deps: [{ token: UploadSimulatorService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1080
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadSingleComponent, isStandalone: true, selector: "dso-file-upload-single", inputs: { btnLabel: "btnLabel", accept: "accept" }, outputs: { fileSelected: "fileSelected" }, ngImport: i0, template: "<div class=\"file-upload-single\">\r\n\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n #fileInput\r\n [accept]=\"accept\"\r\n (change)=\"onFileChange($event)\"\r\n hidden\r\n />\r\n\r\n <!-- Main button -->\r\n <dso-button\r\n [btnLabel]=\"btnLabel\"\r\n btnType=\"filled\"\r\n (onClick)=\"onButtonClick(fileInput)\"\r\n ></dso-button>\r\n\r\n <!-- Render file info using shared reusable component -->\r\n <dso-file-item\r\n *ngIf=\"item\"\r\n [item]=\"item\"\r\n (cancel)=\"clearFile()\"\r\n (remove)=\"clearFile()\"\r\n (retry)=\"retry()\"\r\n ></dso-file-item>\r\n\r\n</div>\r\n", styles: [".file-upload-single{display:flex;flex-direction:column;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: FileUploadItemComponent, selector: "dso-file-item", inputs: ["item"], outputs: ["retry", "remove", "cancel"] }] });
|
|
1081
|
+
}
|
|
1082
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadSingleComponent, decorators: [{
|
|
1083
|
+
type: Component,
|
|
1084
|
+
args: [{ selector: 'dso-file-upload-single', standalone: true, imports: [
|
|
1085
|
+
CommonModule,
|
|
1086
|
+
ButtonComponent,
|
|
1087
|
+
FileUploadItemComponent
|
|
1088
|
+
], template: "<div class=\"file-upload-single\">\r\n\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n #fileInput\r\n [accept]=\"accept\"\r\n (change)=\"onFileChange($event)\"\r\n hidden\r\n />\r\n\r\n <!-- Main button -->\r\n <dso-button\r\n [btnLabel]=\"btnLabel\"\r\n btnType=\"filled\"\r\n (onClick)=\"onButtonClick(fileInput)\"\r\n ></dso-button>\r\n\r\n <!-- Render file info using shared reusable component -->\r\n <dso-file-item\r\n *ngIf=\"item\"\r\n [item]=\"item\"\r\n (cancel)=\"clearFile()\"\r\n (remove)=\"clearFile()\"\r\n (retry)=\"retry()\"\r\n ></dso-file-item>\r\n\r\n</div>\r\n", styles: [".file-upload-single{display:flex;flex-direction:column;gap:8px}\n"] }]
|
|
1089
|
+
}], ctorParameters: () => [{ type: UploadSimulatorService }], propDecorators: { btnLabel: [{
|
|
1090
|
+
type: Input
|
|
1091
|
+
}], accept: [{
|
|
1092
|
+
type: Input
|
|
1093
|
+
}], fileSelected: [{
|
|
1094
|
+
type: Output
|
|
1095
|
+
}] } });
|
|
1096
|
+
|
|
1097
|
+
class TextInputComponent {
|
|
1098
|
+
// -------------------------------
|
|
1099
|
+
// INPUT PROPERTIES
|
|
1100
|
+
// -------------------------------
|
|
1101
|
+
inputLabel = ''; // Optional label displayed above input
|
|
1102
|
+
value = ''; // Two-way bound input value
|
|
1103
|
+
placeholder = 'Input value'; // Placeholder text
|
|
1104
|
+
type = ''; // Input type (text, email, etc.)
|
|
1105
|
+
isDisabled = false; // Disable the input
|
|
1106
|
+
helperText = ''; // Optional helper text below input
|
|
1107
|
+
errorText = ''; // Optional error message
|
|
1108
|
+
enableValidation = true; // Flag to enable/disable validation
|
|
1109
|
+
maxLength = 80; // Maximum characters allowed
|
|
1110
|
+
// -------------------------------
|
|
1111
|
+
// OUTPUT EVENTS
|
|
1112
|
+
// -------------------------------
|
|
1113
|
+
valueChange = new EventEmitter(); // Emit input changes to parent
|
|
1114
|
+
// -------------------------------
|
|
1115
|
+
// INTERNAL STATE
|
|
1116
|
+
// -------------------------------
|
|
1117
|
+
isTouched = false; // Tracks if user has interacted with input
|
|
1118
|
+
isInvalid = false; // True if validation fails
|
|
1119
|
+
charCount = 0; // Tracks current character count
|
|
1120
|
+
isExceeded = false; // True if charCount exceeds maxLength
|
|
1121
|
+
// -------------------------------
|
|
1122
|
+
// EVENT HANDLERS
|
|
1123
|
+
// -------------------------------
|
|
1124
|
+
onInputChange(event) {
|
|
1125
|
+
this.value = event.target.value; // Update value
|
|
1126
|
+
const inputValue = event.target.value;
|
|
1127
|
+
const charCount = inputValue.length;
|
|
1128
|
+
// Track if max length exceeded
|
|
1129
|
+
this.isExceeded = charCount > this.maxLength;
|
|
1130
|
+
this.isTouched = true; // Mark input as touched
|
|
1131
|
+
this.valueChange.emit(this.value); // Notify parent
|
|
1132
|
+
this.checkValidity(); // Validate input
|
|
1133
|
+
}
|
|
1134
|
+
onBlur() {
|
|
1135
|
+
this.isTouched = true; // Input lost focus → mark as touched
|
|
1136
|
+
this.checkValidity(); // Validate when blurred
|
|
1137
|
+
}
|
|
1138
|
+
// -------------------------------
|
|
1139
|
+
// HELPER METHODS
|
|
1140
|
+
// -------------------------------
|
|
1141
|
+
getCharCountStyle() {
|
|
1142
|
+
const charCount = this.value.length;
|
|
1143
|
+
if (this.isExceeded) {
|
|
1144
|
+
return 'char-count-error'; // Apply error style if limit exceeded
|
|
1145
|
+
}
|
|
1146
|
+
// Optional: style differently if more than 50% of maxLength used
|
|
1147
|
+
if (charCount > this.maxLength / 2) {
|
|
1148
|
+
return '';
|
|
1149
|
+
}
|
|
1150
|
+
return ''; // Default style
|
|
1151
|
+
}
|
|
1152
|
+
checkValidity() {
|
|
1153
|
+
// If touched and value is empty, mark as invalid
|
|
1154
|
+
this.isInvalid = this.isTouched && !this.value.trim();
|
|
1155
|
+
}
|
|
1156
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1157
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TextInputComponent, isStandalone: true, selector: "dso-input-text", inputs: { inputLabel: "inputLabel", value: "value", placeholder: "placeholder", type: "type", isDisabled: "isDisabled", helperText: "helperText", errorText: "errorText", enableValidation: "enableValidation", maxLength: "maxLength" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"input-container\">\r\n <!-- Optional label -->\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }} \r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div> \r\n <input\r\n [type]=\"type\" \r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\" \r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\" \r\n />\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n\r\n", styles: [":host{display:inline-block}.input-container{width:500px;display:flex;flex-direction:column;gap:8px}.text-wrapper{display:flex;justify-content:space-between}input{width:100%;height:48px;padding:12px 8px;font-size:16px;border:1px solid #ccc;border-radius:4px}input:hover{border:1px solid #071d35}input:focus{border-color:#007bff;outline:none}input.invalid{border-color:red}.error-message{color:red;font-size:16px}input.disabled{cursor:not-allowed;background-color:#f0f0f0;border:1px solid #ddd;color:#000}.helper-text{font-size:16px;color:#6c757d}.error-message{font-size:16px;color:red}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1158
|
+
}
|
|
1159
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextInputComponent, decorators: [{
|
|
1160
|
+
type: Component,
|
|
1161
|
+
args: [{ selector: 'dso-input-text', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"input-container\">\r\n <!-- Optional label -->\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }} \r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div> \r\n <input\r\n [type]=\"type\" \r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\" \r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\" \r\n />\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n\r\n", styles: [":host{display:inline-block}.input-container{width:500px;display:flex;flex-direction:column;gap:8px}.text-wrapper{display:flex;justify-content:space-between}input{width:100%;height:48px;padding:12px 8px;font-size:16px;border:1px solid #ccc;border-radius:4px}input:hover{border:1px solid #071d35}input:focus{border-color:#007bff;outline:none}input.invalid{border-color:red}.error-message{color:red;font-size:16px}input.disabled{cursor:not-allowed;background-color:#f0f0f0;border:1px solid #ddd;color:#000}.helper-text{font-size:16px;color:#6c757d}.error-message{font-size:16px;color:red}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}\n"] }]
|
|
1162
|
+
}], propDecorators: { inputLabel: [{
|
|
1163
|
+
type: Input
|
|
1164
|
+
}], value: [{
|
|
1165
|
+
type: Input
|
|
1166
|
+
}], placeholder: [{
|
|
1167
|
+
type: Input
|
|
1168
|
+
}], type: [{
|
|
1169
|
+
type: Input
|
|
1170
|
+
}], isDisabled: [{
|
|
1171
|
+
type: Input
|
|
1172
|
+
}], helperText: [{
|
|
1173
|
+
type: Input
|
|
1174
|
+
}], errorText: [{
|
|
1175
|
+
type: Input
|
|
1176
|
+
}], enableValidation: [{
|
|
1177
|
+
type: Input
|
|
1178
|
+
}], maxLength: [{
|
|
1179
|
+
type: Input
|
|
1180
|
+
}], valueChange: [{
|
|
1181
|
+
type: Output
|
|
1182
|
+
}] } });
|
|
1183
|
+
|
|
1184
|
+
class PaginationComponent {
|
|
1185
|
+
/* =======================
|
|
1186
|
+
Inputs (controlled state)
|
|
1187
|
+
======================= */
|
|
1188
|
+
page = 1;
|
|
1189
|
+
pageSize = 10;
|
|
1190
|
+
totalItems = 0;
|
|
1191
|
+
pageSizes = [10, 20, 50, 100];
|
|
1192
|
+
/* =======================
|
|
1193
|
+
Outputs (user intent)
|
|
1194
|
+
======================= */
|
|
1195
|
+
pageChange = new EventEmitter();
|
|
1196
|
+
pageSizeChange = new EventEmitter();
|
|
1197
|
+
/* =======================
|
|
1198
|
+
Internal UI state
|
|
1199
|
+
======================= */
|
|
1200
|
+
pageNumbers = [];
|
|
1201
|
+
jumpPageNumber = null;
|
|
1202
|
+
pageSizeOptions = [];
|
|
1203
|
+
/* =======================
|
|
1204
|
+
Lifecycle
|
|
1205
|
+
======================= */
|
|
1206
|
+
ngOnChanges() {
|
|
1207
|
+
this.buildPageSizeOptions();
|
|
1208
|
+
this.generatePageNumbers();
|
|
1209
|
+
}
|
|
1210
|
+
/* =======================
|
|
1211
|
+
Derived values
|
|
1212
|
+
======================= */
|
|
1213
|
+
get totalPages() {
|
|
1214
|
+
return Math.max(Math.ceil(this.totalItems / this.pageSize), 1);
|
|
1215
|
+
}
|
|
1216
|
+
get startItem() {
|
|
1217
|
+
return this.totalItems === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
|
1218
|
+
}
|
|
1219
|
+
get endItem() {
|
|
1220
|
+
return Math.min(this.page * this.pageSize, this.totalItems);
|
|
1221
|
+
}
|
|
1222
|
+
/* =======================
|
|
1223
|
+
UI helpers
|
|
1224
|
+
======================= */
|
|
1225
|
+
buildPageSizeOptions() {
|
|
1226
|
+
this.pageSizeOptions = this.pageSizes.map(size => ({
|
|
1227
|
+
label: `${size} per page`,
|
|
1228
|
+
value: size
|
|
1229
|
+
}));
|
|
1230
|
+
}
|
|
1231
|
+
generatePageNumbers() {
|
|
1232
|
+
const total = this.totalPages;
|
|
1233
|
+
let start = Math.max(this.page - 2, 1);
|
|
1234
|
+
let end = Math.min(start + 4, total);
|
|
1235
|
+
start = Math.max(end - 4, 1);
|
|
1236
|
+
this.pageNumbers = [];
|
|
1237
|
+
for (let i = start; i <= end; i++) {
|
|
1238
|
+
this.pageNumbers.push(i);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/* =======================
|
|
1242
|
+
Navigation (emit only)
|
|
1243
|
+
======================= */
|
|
1244
|
+
goToPage(page) {
|
|
1245
|
+
if (page >= 1 && page <= this.totalPages && page !== this.page) {
|
|
1246
|
+
this.pageChange.emit(page);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
goFirst() {
|
|
1250
|
+
this.goToPage(1);
|
|
1251
|
+
}
|
|
1252
|
+
goPrevious() {
|
|
1253
|
+
this.goToPage(this.page - 1);
|
|
1254
|
+
}
|
|
1255
|
+
goNext() {
|
|
1256
|
+
this.goToPage(this.page + 1);
|
|
1257
|
+
}
|
|
1258
|
+
goLast() {
|
|
1259
|
+
this.goToPage(this.totalPages);
|
|
1260
|
+
}
|
|
1261
|
+
onPageSizeChange(newSize) {
|
|
1262
|
+
this.pageSizeChange.emit(newSize);
|
|
1263
|
+
}
|
|
1264
|
+
jumpToPage() {
|
|
1265
|
+
if (!this.jumpPageNumber)
|
|
1266
|
+
return;
|
|
1267
|
+
const page = Math.max(1, Math.min(this.jumpPageNumber, this.totalPages));
|
|
1268
|
+
this.pageChange.emit(page);
|
|
1269
|
+
this.jumpPageNumber = null;
|
|
1270
|
+
}
|
|
1271
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1272
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: PaginationComponent, isStandalone: true, selector: "dso-pagination", inputs: { page: "page", pageSize: "pageSize", totalItems: "totalItems", pageSizes: "pageSizes" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"pagination-container\">\r\n\r\n <!-- First / Previous -->\r\n <dso-button\r\n btnLabel=\"\u00AB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goFirst()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u2039\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goPrevious()\">\r\n </dso-button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"page-buttons\">\r\n <ng-container *ngFor=\"let p of pageNumbers\">\r\n <dso-button\r\n [btnLabel]=\"p.toString()\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"page === p\"\r\n [isActive]=\"page === p\"\r\n (onClick)=\"goToPage(p)\">\r\n </dso-button>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Next / Last -->\r\n <dso-button\r\n btnLabel=\"\u203A\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goNext()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u00BB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goLast()\">\r\n </dso-button>\r\n\r\n <!-- Page size -->\r\n <dso-dropdown-list\r\n [options]=\"pageSizeOptions\"\r\n [value]=\"pageSize\"\r\n (selectionChange)=\"onPageSizeChange($event)\">\r\n </dso-dropdown-list>\r\n\r\n <!-- Range info -->\r\n <span class=\"range-info\">\r\n Showing {{ startItem }}\u2013{{ endItem }} of {{ totalItems }} results\r\n </span>\r\n\r\n <!-- Jump to page -->\r\n <div class=\"jump-to-page\">\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [(ngModel)]=\"jumpPageNumber\"\r\n (keydown.enter)=\"jumpToPage()\" />\r\n\r\n <dso-button\r\n btnLabel=\"Go\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"!jumpPageNumber\"\r\n (onClick)=\"jumpToPage()\">\r\n </dso-button>\r\n </div>\r\n\r\n</div>\r\n", styles: [".pagination-container{display:flex;align-items:center;gap:8px;font-size:14px}.range-info{margin-right:12px}button{padding:6px 10px;border-radius:4px;border:1px solid #ccc;background:#f0f0f0;cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.page-number.active{background-color:#007bff;color:#fff;font-weight:700}.page-buttons{display:flex;gap:4px}.page-buttons dso-button{flex:0 0 44px;text-align:center}.jump-to-page{display:flex;align-items:center;gap:4px}.jump-to-page input{width:48px;padding:6px 8px;border-radius:4px;border:1px solid #ccc;text-align:center}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: DropdownListComponent, selector: "dso-dropdown-list", inputs: ["options", "placeholder", "value", "disabled"], outputs: ["selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1273
|
+
}
|
|
1274
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationComponent, decorators: [{
|
|
1275
|
+
type: Component,
|
|
1276
|
+
args: [{ selector: 'dso-pagination', standalone: true, imports: [ButtonComponent, CommonModule, DropdownListComponent, FormsModule], template: "<div class=\"pagination-container\">\r\n\r\n <!-- First / Previous -->\r\n <dso-button\r\n btnLabel=\"\u00AB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goFirst()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u2039\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goPrevious()\">\r\n </dso-button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"page-buttons\">\r\n <ng-container *ngFor=\"let p of pageNumbers\">\r\n <dso-button\r\n [btnLabel]=\"p.toString()\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"page === p\"\r\n [isActive]=\"page === p\"\r\n (onClick)=\"goToPage(p)\">\r\n </dso-button>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Next / Last -->\r\n <dso-button\r\n btnLabel=\"\u203A\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goNext()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u00BB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goLast()\">\r\n </dso-button>\r\n\r\n <!-- Page size -->\r\n <dso-dropdown-list\r\n [options]=\"pageSizeOptions\"\r\n [value]=\"pageSize\"\r\n (selectionChange)=\"onPageSizeChange($event)\">\r\n </dso-dropdown-list>\r\n\r\n <!-- Range info -->\r\n <span class=\"range-info\">\r\n Showing {{ startItem }}\u2013{{ endItem }} of {{ totalItems }} results\r\n </span>\r\n\r\n <!-- Jump to page -->\r\n <div class=\"jump-to-page\">\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [(ngModel)]=\"jumpPageNumber\"\r\n (keydown.enter)=\"jumpToPage()\" />\r\n\r\n <dso-button\r\n btnLabel=\"Go\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"!jumpPageNumber\"\r\n (onClick)=\"jumpToPage()\">\r\n </dso-button>\r\n </div>\r\n\r\n</div>\r\n", styles: [".pagination-container{display:flex;align-items:center;gap:8px;font-size:14px}.range-info{margin-right:12px}button{padding:6px 10px;border-radius:4px;border:1px solid #ccc;background:#f0f0f0;cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.page-number.active{background-color:#007bff;color:#fff;font-weight:700}.page-buttons{display:flex;gap:4px}.page-buttons dso-button{flex:0 0 44px;text-align:center}.jump-to-page{display:flex;align-items:center;gap:4px}.jump-to-page input{width:48px;padding:6px 8px;border-radius:4px;border:1px solid #ccc;text-align:center}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n"] }]
|
|
1277
|
+
}], propDecorators: { page: [{
|
|
1278
|
+
type: Input
|
|
1279
|
+
}], pageSize: [{
|
|
1280
|
+
type: Input
|
|
1281
|
+
}], totalItems: [{
|
|
1282
|
+
type: Input
|
|
1283
|
+
}], pageSizes: [{
|
|
1284
|
+
type: Input
|
|
1285
|
+
}], pageChange: [{
|
|
1286
|
+
type: Output
|
|
1287
|
+
}], pageSizeChange: [{
|
|
1288
|
+
type: Output
|
|
1289
|
+
}] } });
|
|
1290
|
+
|
|
1291
|
+
class RadioComponent {
|
|
1292
|
+
label = 'Radio label';
|
|
1293
|
+
value;
|
|
1294
|
+
name; // for grouping radios
|
|
1295
|
+
disabled = false;
|
|
1296
|
+
error = false;
|
|
1297
|
+
isChecked = false;
|
|
1298
|
+
change = new EventEmitter();
|
|
1299
|
+
// Method to handle change in radio state
|
|
1300
|
+
toggleRadio(event) {
|
|
1301
|
+
// console.log(this.value);
|
|
1302
|
+
event.stopPropagation();
|
|
1303
|
+
this.change.emit(this.value); // Emit the updated value to the parent
|
|
1304
|
+
}
|
|
1305
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1306
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: RadioComponent, isStandalone: true, selector: "dso-radio", inputs: { label: "label", value: "value", name: "name", disabled: "disabled", error: "error", isChecked: "isChecked" }, outputs: { change: "change" }, ngImport: i0, template: "<label class=\"radio-wrapper\" \r\n[class.disabled]=\"disabled\" \r\n[class.error]=\"error\">\r\n <input\r\n class=\"radio-input\"\r\n type=\"radio\"\r\n [attr.name]=\"name\"\r\n [value]=\"value\"\r\n [checked]=\"isChecked\"\r\n [disabled]=\"disabled\"\r\n (change)=\"toggleRadio($event)\"\r\n />\r\n <span class=\"custom-radio\"></span>\r\n <span *ngIf=\"label\" class=\"radio-label\">{{ label }}</span>\r\n</label>\r\n", styles: [".radio-wrapper{display:flex;align-items:center;gap:8px;cursor:pointer}.radio-wrapper.disabled{cursor:not-allowed}.radio-wrapper.disabled .custom-radio{background-color:#f0f0f0;border-color:#d0d0d0}.radio-wrapper.disabled .radio-input:checked~.custom-radio{border:1px solid rgba(0,0,255,.5)}.radio-wrapper.disabled .custom-radio:after{opacity:.5}.radio-input{position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-radio{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #666666;border-radius:50%}.radio-input:checked~.custom-radio{border:1px solid blue}input[type=radio]:checked+.custom-radio:after{display:block;content:\"\";width:100%;height:100%;background:#00f;border:5px solid blue;background:transparent;box-sizing:border-box;border-radius:50%}.radio-label{font-size:14px}.error .custom-radio{border-color:red}.radio-wrapper:not(.disabled):hover .custom-radio{background-color:#eef5f9;border-color:#0000ffc0;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1307
|
+
}
|
|
1308
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RadioComponent, decorators: [{
|
|
1309
|
+
type: Component,
|
|
1310
|
+
args: [{ selector: 'dso-radio', standalone: true, imports: [FormsModule, CommonModule], template: "<label class=\"radio-wrapper\" \r\n[class.disabled]=\"disabled\" \r\n[class.error]=\"error\">\r\n <input\r\n class=\"radio-input\"\r\n type=\"radio\"\r\n [attr.name]=\"name\"\r\n [value]=\"value\"\r\n [checked]=\"isChecked\"\r\n [disabled]=\"disabled\"\r\n (change)=\"toggleRadio($event)\"\r\n />\r\n <span class=\"custom-radio\"></span>\r\n <span *ngIf=\"label\" class=\"radio-label\">{{ label }}</span>\r\n</label>\r\n", styles: [".radio-wrapper{display:flex;align-items:center;gap:8px;cursor:pointer}.radio-wrapper.disabled{cursor:not-allowed}.radio-wrapper.disabled .custom-radio{background-color:#f0f0f0;border-color:#d0d0d0}.radio-wrapper.disabled .radio-input:checked~.custom-radio{border:1px solid rgba(0,0,255,.5)}.radio-wrapper.disabled .custom-radio:after{opacity:.5}.radio-input{position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-radio{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #666666;border-radius:50%}.radio-input:checked~.custom-radio{border:1px solid blue}input[type=radio]:checked+.custom-radio:after{display:block;content:\"\";width:100%;height:100%;background:#00f;border:5px solid blue;background:transparent;box-sizing:border-box;border-radius:50%}.radio-label{font-size:14px}.error .custom-radio{border-color:red}.radio-wrapper:not(.disabled):hover .custom-radio{background-color:#eef5f9;border-color:#0000ffc0;cursor:pointer}\n"] }]
|
|
1311
|
+
}], propDecorators: { label: [{
|
|
1312
|
+
type: Input
|
|
1313
|
+
}], value: [{
|
|
1314
|
+
type: Input
|
|
1315
|
+
}], name: [{
|
|
1316
|
+
type: Input
|
|
1317
|
+
}], disabled: [{
|
|
1318
|
+
type: Input
|
|
1319
|
+
}], error: [{
|
|
1320
|
+
type: Input
|
|
1321
|
+
}], isChecked: [{
|
|
1322
|
+
type: Input
|
|
1323
|
+
}], change: [{
|
|
1324
|
+
type: Output
|
|
1325
|
+
}] } });
|
|
1326
|
+
|
|
1327
|
+
class SingleSelectComponent {
|
|
1328
|
+
elementRef;
|
|
1329
|
+
// Reference to the dropdown <ul> for scroll management
|
|
1330
|
+
dropdownListRef;
|
|
1331
|
+
// Inputs for configuration & data binding
|
|
1332
|
+
options = [];
|
|
1333
|
+
dropdownPosition = 'bottom';
|
|
1334
|
+
placeholder = 'Select an option';
|
|
1335
|
+
inputTextLabel = 'Dropdown Title'; // Optional input label
|
|
1336
|
+
isDisabled = false; // Controls disabled state
|
|
1337
|
+
enableValidation = true; // Enables validation checks
|
|
1338
|
+
helperText; // Optional helper text
|
|
1339
|
+
selectIconName = null; // Icon name/class for clear button
|
|
1340
|
+
errorText = 'This field is required'; // Validation error text
|
|
1341
|
+
// Output event emitter to notify selection changes
|
|
1342
|
+
selectionChange = new EventEmitter();
|
|
1343
|
+
// Internal state properties
|
|
1344
|
+
selectedOption = null; // Currently selected option object
|
|
1345
|
+
searchQuery = ''; // Search input value
|
|
1346
|
+
filteredOptions = []; // Options filtered based on search
|
|
1347
|
+
isOpen = false; // Dropdown open/close state
|
|
1348
|
+
isInvalid = false; // Validation invalid flag
|
|
1349
|
+
isExceeded = false; // (Optional) exceeded limit flag, currently unused
|
|
1350
|
+
hasOpened = false;
|
|
1351
|
+
hasInteracted = false; // (Optional) touched flag, currently unused
|
|
1352
|
+
lastScrollTop = 0; // Stores last scroll position of dropdown list
|
|
1353
|
+
constructor(elementRef) {
|
|
1354
|
+
this.elementRef = elementRef;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* //Handles dropdown scroll events to persist scroll position
|
|
1358
|
+
*/
|
|
1359
|
+
onDropdownScroll() {
|
|
1360
|
+
if (this.dropdownListRef) {
|
|
1361
|
+
this.lastScrollTop = this.dropdownListRef.nativeElement.scrollTop;
|
|
1362
|
+
console.log(`Dropdown scrollTop updated to: ${this.lastScrollTop}`);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
console.warn('dropdownListRef is undefined on scroll');
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Detects clicks outside of the component to close the dropdown if open
|
|
1370
|
+
*/
|
|
1371
|
+
onOutsideClick(event) {
|
|
1372
|
+
const clickedInside = this.elementRef.nativeElement.contains(event.target);
|
|
1373
|
+
if (!clickedInside) {
|
|
1374
|
+
if (this.isOpen) {
|
|
1375
|
+
this.isOpen = false; // Close dropdown if clicked outside
|
|
1376
|
+
console.log('Dropdown closed via outside click');
|
|
1377
|
+
this.hasInteracted = true;
|
|
1378
|
+
// Validate only if nothing is selected
|
|
1379
|
+
if (this.enableValidation && !this.selectedOption) {
|
|
1380
|
+
this.isInvalid = true;
|
|
1381
|
+
console.log('Validation failed: no option selected after outside click');
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
this.isInvalid = false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Initialize component data on creation
|
|
1391
|
+
*/
|
|
1392
|
+
ngOnInit() {
|
|
1393
|
+
// Sample hardcoded options (can be overridden via @Input)
|
|
1394
|
+
this.options = [
|
|
1395
|
+
{ value: '1', label: 'Apple' },
|
|
1396
|
+
{ value: '2', label: 'Banana' },
|
|
1397
|
+
{ value: '3', label: 'Cherry' },
|
|
1398
|
+
{ value: '4', label: 'Date' },
|
|
1399
|
+
{ value: '5', label: 'Grapes' },
|
|
1400
|
+
{ value: '6', label: 'AAAA' },
|
|
1401
|
+
{ value: '7', label: 'BBBB' },
|
|
1402
|
+
{ value: '8', label: 'CCCC' },
|
|
1403
|
+
{ value: '9', label: 'DDDD' },
|
|
1404
|
+
];
|
|
1405
|
+
// Show all options initially
|
|
1406
|
+
this.filteredOptions = this.options;
|
|
1407
|
+
}
|
|
1408
|
+
ngOnDestroy() {
|
|
1409
|
+
// Placeholder for any cleanup logic if needed
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Handles click on the main select box
|
|
1413
|
+
* Prevents opening if disabled
|
|
1414
|
+
*/
|
|
1415
|
+
onSelectClick(event) {
|
|
1416
|
+
event.stopPropagation();
|
|
1417
|
+
if (this.isDisabled) {
|
|
1418
|
+
event.stopPropagation();
|
|
1419
|
+
event.preventDefault();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
this.toggleDropdown(event);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Toggles dropdown open/close state and restores scroll position if opening
|
|
1426
|
+
*/
|
|
1427
|
+
toggleDropdown(event) {
|
|
1428
|
+
if (this.isDisabled) {
|
|
1429
|
+
event.stopPropagation();
|
|
1430
|
+
// console.warn('Dropdown is disabled, toggle prevented.');
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.isOpen = !this.isOpen;
|
|
1434
|
+
this.hasInteracted = true;
|
|
1435
|
+
// console.log(`Dropdown toggled. isOpen: ${this.isOpen}`);
|
|
1436
|
+
if (this.isOpen) {
|
|
1437
|
+
this.hasOpened = true;
|
|
1438
|
+
setTimeout(() => {
|
|
1439
|
+
if (this.dropdownListRef) {
|
|
1440
|
+
// console.log(`Restoring scrollTop to: ${this.lastScrollTop}`);
|
|
1441
|
+
this.dropdownListRef.nativeElement.scrollTop = this.lastScrollTop;
|
|
1442
|
+
console.log('Scroll Restored');
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
// Validate on close
|
|
1448
|
+
this.hasInteracted = true;
|
|
1449
|
+
console.log('User has interacted and closed the dropdown');
|
|
1450
|
+
if (this.enableValidation && !this.selectedOption) {
|
|
1451
|
+
this.isInvalid = true;
|
|
1452
|
+
console.log('No option selected — validation failed');
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
this.isInvalid = false;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Filters dropdown options based on current search query
|
|
1461
|
+
*/
|
|
1462
|
+
filterOptions() {
|
|
1463
|
+
const query = this.searchQuery.toLowerCase();
|
|
1464
|
+
this.filteredOptions = this.options.filter((option) => option.label.toLowerCase().includes(query));
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Handles option selection:
|
|
1468
|
+
* - Updates selected option
|
|
1469
|
+
* - Validates
|
|
1470
|
+
* - Emits selection change event
|
|
1471
|
+
* - Closes dropdown
|
|
1472
|
+
*/
|
|
1473
|
+
selectOption(event, option) {
|
|
1474
|
+
event.stopPropagation();
|
|
1475
|
+
this.selectedOption = option;
|
|
1476
|
+
this.isInvalid = false;
|
|
1477
|
+
this.selectionChange.emit(this.selectedOption);
|
|
1478
|
+
this.isOpen = false;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Clears the selected option and resets validation and scroll
|
|
1482
|
+
*/
|
|
1483
|
+
clearSelection(event) {
|
|
1484
|
+
// console.log('Clear selection triggered');
|
|
1485
|
+
// this.hasInteracted = false;
|
|
1486
|
+
this.selectedOption = null;
|
|
1487
|
+
this.searchQuery = ''; // Clear the search input field as well
|
|
1488
|
+
this.filteredOptions = this.options; // Show all options again
|
|
1489
|
+
this.isInvalid = false;
|
|
1490
|
+
this.lastScrollTop = 0;
|
|
1491
|
+
this.selectionChange.emit(this.selectedOption);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Checks validity based on current selection if validation is enabled
|
|
1495
|
+
*/
|
|
1496
|
+
checkValidity() {
|
|
1497
|
+
if (this.enableValidation) {
|
|
1498
|
+
this.isInvalid = !this.selectedOption;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Returns the icon path if icon name is provided
|
|
1503
|
+
*/
|
|
1504
|
+
getIconPath() {
|
|
1505
|
+
if (this.selectIconName) {
|
|
1506
|
+
return `./assets/icons/${this.selectIconName}.svg`;
|
|
1507
|
+
}
|
|
1508
|
+
return '';
|
|
1509
|
+
}
|
|
1510
|
+
showError() {
|
|
1511
|
+
return this.enableValidation && this.hasInteracted && !this.selectedOption;
|
|
1512
|
+
}
|
|
1513
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SingleSelectComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1514
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SingleSelectComponent, isStandalone: true, selector: "dso-single-select", inputs: { options: "options", dropdownPosition: "dropdownPosition", placeholder: "placeholder", inputTextLabel: "inputTextLabel", isDisabled: "isDisabled", enableValidation: "enableValidation", helperText: "helperText", selectIconName: "selectIconName", errorText: "errorText" }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "document:click": "onOutsideClick($event)" } }, viewQueries: [{ propertyName: "dropdownListRef", first: true, predicate: ["dropdownList"], descendants: true }], ngImport: i0, template: "<div class=\"select-wrapper\">\r\n<!-- Label (optional) -->\r\n<div *ngIf=\"inputTextLabel\" class=\"select-label\">{{ inputTextLabel }}</div>\r\n \r\n<!-- <div class=\"select-container\" (click)=\"toggleDropdown($event)\" [class.disabled]=\"isDisabled\"> -->\r\n<div class=\"select-container\" [class.disabled]=\"isDisabled\" [class.error]=\"enableValidation && hasInteracted && isInvalid\" [class.focused]=\"isOpen\"\r\n > \r\n <div class=\"selected-option\"\r\n (click)=\"onSelectClick($event)\" \r\n [ngClass]=\"{'placeholder': !selectedOption, \r\n 'selected': selectedOption }\">\r\n <span>{{ selectedOption?.label || placeholder }}</span>\r\n <div class=\"clearbtn-wrapper\"> \r\n <dso-button \r\n btnLabel=\"Clear\" \r\n btnType=\"ghost\" \r\n btnSize='smBtn'\r\n *ngIf=\"selectedOption && !isDisabled\" \r\n (click)=\"clearSelection($event)\"></dso-button>\r\n <dso-icon *ngIf=\"selectIconName\" [iconName]=\"selectIconName\" [size]=\"'small'\"></dso-icon>\r\n </div>\r\n </div>\r\n\r\n <!-- Dropdown -->\r\n <div class=\"dropdown\" *ngIf=\"isOpen\" \r\n (click)=\"$event.stopPropagation()\" \r\n [ngClass]=\"{'dropdown-bottom': dropdownPosition === 'bottom', 'dropdown-top': dropdownPosition === 'top'}\" >\r\n <input type=\"text\" \r\n [(ngModel)]=\"searchQuery\" \r\n (input)=\"filterOptions()\" \r\n placeholder=\"Search...\" />\r\n <ul #dropdownList \r\n (scroll)=\"onDropdownScroll()\" \r\n class=\"dropdown-list\">\r\n <li *ngFor=\"let option of filteredOptions\" (click)=\"selectOption($event, option)\" [ngClass]=\"{'active': option.value === selectedOption?.value}\">\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n\r\n<!-- Helper Text (when validation is disabled) -->\r\n<div *ngIf=\"enableValidation && helperText && !(hasInteracted && isInvalid)\" class=\"helper-text\">\r\n {{ helperText }}\r\n</div>\r\n\r\n<!-- Error Message (when validation is enabled) -->\r\n<div *ngIf=\"enableValidation && hasInteracted && isInvalid\" class=\"error-message\">\r\n {{ errorText }}\r\n</div>\r\n</div>", styles: [":host{display:inline-block;font-family:inherit}.select-label{font-size:16px;color:#333;display:block}.select-wrapper{display:flex;flex-direction:column;gap:8px}.select-container{position:relative;width:300px;display:flex;flex-direction:column;border:1px solid #ccc;border-radius:4px;background-color:#fff;box-sizing:border-box}.select-container:hover{border:1px solid #071d35}.select-container.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.select-container.disabled .selected-option{pointer-events:none;color:#aaa}.select-container.error{border-color:red}.select-container.focused{border-color:#007bff}.dropdown-list{max-height:150px;overflow-y:auto}.selected-option{padding:12px 8px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}.selected-option.placeholder{color:#aaa}.selected-option.selected{color:#000}img.button-icon{width:20px;height:20px}.clearbtn-wrapper{display:flex;gap:4px;justify-content:space-between;align-items:center}.dropdown{width:300px;position:absolute;background-color:#fff;border:1px solid #ccc;max-height:200px;overflow-y:auto;box-shadow:0 4px 6px #0000000d}.dropdown input{width:100%;padding:10px;box-sizing:border-box;border:none;border-bottom:1px solid #eee;outline:none}.dropdown ul{list-style:none;padding:0;margin:0}.dropdown ul li{padding:8px 12px;cursor:pointer;transition:background-color .2s}.dropdown ul li:hover{background-color:#f0f0f0}.dropdown ul li.active{background-color:#3784d6;color:#fff;font-weight:700}.dropdown-bottom{top:100%}.dropdown-top{bottom:100%}.helper-text,.error-message{font-size:16px}.helper-text{color:#888}.error-message{color:red}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
|
|
1515
|
+
}
|
|
1516
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SingleSelectComponent, decorators: [{
|
|
1517
|
+
type: Component,
|
|
1518
|
+
args: [{ selector: 'dso-single-select', standalone: true, imports: [FormsModule, CommonModule, ButtonComponent, IconComponent], template: "<div class=\"select-wrapper\">\r\n<!-- Label (optional) -->\r\n<div *ngIf=\"inputTextLabel\" class=\"select-label\">{{ inputTextLabel }}</div>\r\n \r\n<!-- <div class=\"select-container\" (click)=\"toggleDropdown($event)\" [class.disabled]=\"isDisabled\"> -->\r\n<div class=\"select-container\" [class.disabled]=\"isDisabled\" [class.error]=\"enableValidation && hasInteracted && isInvalid\" [class.focused]=\"isOpen\"\r\n > \r\n <div class=\"selected-option\"\r\n (click)=\"onSelectClick($event)\" \r\n [ngClass]=\"{'placeholder': !selectedOption, \r\n 'selected': selectedOption }\">\r\n <span>{{ selectedOption?.label || placeholder }}</span>\r\n <div class=\"clearbtn-wrapper\"> \r\n <dso-button \r\n btnLabel=\"Clear\" \r\n btnType=\"ghost\" \r\n btnSize='smBtn'\r\n *ngIf=\"selectedOption && !isDisabled\" \r\n (click)=\"clearSelection($event)\"></dso-button>\r\n <dso-icon *ngIf=\"selectIconName\" [iconName]=\"selectIconName\" [size]=\"'small'\"></dso-icon>\r\n </div>\r\n </div>\r\n\r\n <!-- Dropdown -->\r\n <div class=\"dropdown\" *ngIf=\"isOpen\" \r\n (click)=\"$event.stopPropagation()\" \r\n [ngClass]=\"{'dropdown-bottom': dropdownPosition === 'bottom', 'dropdown-top': dropdownPosition === 'top'}\" >\r\n <input type=\"text\" \r\n [(ngModel)]=\"searchQuery\" \r\n (input)=\"filterOptions()\" \r\n placeholder=\"Search...\" />\r\n <ul #dropdownList \r\n (scroll)=\"onDropdownScroll()\" \r\n class=\"dropdown-list\">\r\n <li *ngFor=\"let option of filteredOptions\" (click)=\"selectOption($event, option)\" [ngClass]=\"{'active': option.value === selectedOption?.value}\">\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n\r\n<!-- Helper Text (when validation is disabled) -->\r\n<div *ngIf=\"enableValidation && helperText && !(hasInteracted && isInvalid)\" class=\"helper-text\">\r\n {{ helperText }}\r\n</div>\r\n\r\n<!-- Error Message (when validation is enabled) -->\r\n<div *ngIf=\"enableValidation && hasInteracted && isInvalid\" class=\"error-message\">\r\n {{ errorText }}\r\n</div>\r\n</div>", styles: [":host{display:inline-block;font-family:inherit}.select-label{font-size:16px;color:#333;display:block}.select-wrapper{display:flex;flex-direction:column;gap:8px}.select-container{position:relative;width:300px;display:flex;flex-direction:column;border:1px solid #ccc;border-radius:4px;background-color:#fff;box-sizing:border-box}.select-container:hover{border:1px solid #071d35}.select-container.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.select-container.disabled .selected-option{pointer-events:none;color:#aaa}.select-container.error{border-color:red}.select-container.focused{border-color:#007bff}.dropdown-list{max-height:150px;overflow-y:auto}.selected-option{padding:12px 8px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}.selected-option.placeholder{color:#aaa}.selected-option.selected{color:#000}img.button-icon{width:20px;height:20px}.clearbtn-wrapper{display:flex;gap:4px;justify-content:space-between;align-items:center}.dropdown{width:300px;position:absolute;background-color:#fff;border:1px solid #ccc;max-height:200px;overflow-y:auto;box-shadow:0 4px 6px #0000000d}.dropdown input{width:100%;padding:10px;box-sizing:border-box;border:none;border-bottom:1px solid #eee;outline:none}.dropdown ul{list-style:none;padding:0;margin:0}.dropdown ul li{padding:8px 12px;cursor:pointer;transition:background-color .2s}.dropdown ul li:hover{background-color:#f0f0f0}.dropdown ul li.active{background-color:#3784d6;color:#fff;font-weight:700}.dropdown-bottom{top:100%}.dropdown-top{bottom:100%}.helper-text,.error-message{font-size:16px}.helper-text{color:#888}.error-message{color:red}\n"] }]
|
|
1519
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { dropdownListRef: [{
|
|
1520
|
+
type: ViewChild,
|
|
1521
|
+
args: ['dropdownList']
|
|
1522
|
+
}], options: [{
|
|
1523
|
+
type: Input
|
|
1524
|
+
}], dropdownPosition: [{
|
|
1525
|
+
type: Input
|
|
1526
|
+
}], placeholder: [{
|
|
1527
|
+
type: Input
|
|
1528
|
+
}], inputTextLabel: [{
|
|
1529
|
+
type: Input
|
|
1530
|
+
}], isDisabled: [{
|
|
1531
|
+
type: Input
|
|
1532
|
+
}], enableValidation: [{
|
|
1533
|
+
type: Input
|
|
1534
|
+
}], helperText: [{
|
|
1535
|
+
type: Input
|
|
1536
|
+
}], selectIconName: [{
|
|
1537
|
+
type: Input
|
|
1538
|
+
}], errorText: [{
|
|
1539
|
+
type: Input
|
|
1540
|
+
}], selectionChange: [{
|
|
1541
|
+
type: Output
|
|
1542
|
+
}], onOutsideClick: [{
|
|
1543
|
+
type: HostListener,
|
|
1544
|
+
args: ['document:click', ['$event']]
|
|
1545
|
+
}] } });
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* DSO Side Navigation Component
|
|
1549
|
+
*
|
|
1550
|
+
* Features:
|
|
1551
|
+
* - Configurable title/logo area.
|
|
1552
|
+
* - Dynamic menu items with icons and routing.
|
|
1553
|
+
* - Supports default active item by route or index.
|
|
1554
|
+
* - Click-outside detection to close the sidebar.
|
|
1555
|
+
* - Optional overlay for dimming the background.
|
|
1556
|
+
*/
|
|
1557
|
+
class SideNavComponent {
|
|
1558
|
+
host;
|
|
1559
|
+
/** Title displayed next to the logo */
|
|
1560
|
+
title = '';
|
|
1561
|
+
/** List of navigation items */
|
|
1562
|
+
items = [];
|
|
1563
|
+
/** Controls whether the sidebar is visible */
|
|
1564
|
+
open = true;
|
|
1565
|
+
/** Enable/disable background overlay when sidebar is open */
|
|
1566
|
+
overlay = true;
|
|
1567
|
+
/** Optional: default active menu item by route */
|
|
1568
|
+
defaultActiveRoute;
|
|
1569
|
+
/** Optional: default active menu item by index */
|
|
1570
|
+
defaultActiveIndex;
|
|
1571
|
+
/** Event emitted when sidebar is closed */
|
|
1572
|
+
closed = new EventEmitter();
|
|
1573
|
+
/**
|
|
1574
|
+
* ElementRef to the component host element.
|
|
1575
|
+
* Used for detecting click-outside events.
|
|
1576
|
+
*/
|
|
1577
|
+
constructor(host) {
|
|
1578
|
+
this.host = host;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Lifecycle hook: initialize default active menu item.
|
|
1582
|
+
*/
|
|
1583
|
+
ngOnInit() {
|
|
1584
|
+
// Reset all items to inactive first
|
|
1585
|
+
this.items.forEach(item => item.isActive = false);
|
|
1586
|
+
// Set default active item
|
|
1587
|
+
const defaultItem = this.defaultActiveRoute
|
|
1588
|
+
? this.items.find(item => item.route === this.defaultActiveRoute)
|
|
1589
|
+
: this.items[this.defaultActiveIndex ?? 0]; // fallback to first item if no index
|
|
1590
|
+
if (defaultItem) {
|
|
1591
|
+
defaultItem.isActive = true;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Close the sidebar and emit the `closed` event.
|
|
1596
|
+
* Can be called manually or via click-outside detection.
|
|
1597
|
+
*/
|
|
1598
|
+
closeSidebar() {
|
|
1599
|
+
this.open = false;
|
|
1600
|
+
this.closed.emit();
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* HostListener to detect clicks anywhere in the document.
|
|
1604
|
+
* If the click happens outside the sidebar while it's open, it triggers closeSidebar().
|
|
1605
|
+
*
|
|
1606
|
+
* @param event MouseEvent triggered on document click
|
|
1607
|
+
*/
|
|
1608
|
+
onDocumentClick(event) {
|
|
1609
|
+
if (!this.open)
|
|
1610
|
+
return;
|
|
1611
|
+
// Check if click was inside the sidebar
|
|
1612
|
+
const clickedInside = this.host.nativeElement.contains(event.target);
|
|
1613
|
+
if (!clickedInside) {
|
|
1614
|
+
this.closeSidebar();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Called when a sidebar menu item is clicked.
|
|
1619
|
+
* - Marks the clicked item as active.
|
|
1620
|
+
* - Optionally, navigation can be handled via `[routerLink]`.
|
|
1621
|
+
*
|
|
1622
|
+
* @param item The menu item that was clicked
|
|
1623
|
+
*/
|
|
1624
|
+
onItemClick(item) {
|
|
1625
|
+
this.items.forEach(i => i.isActive = false);
|
|
1626
|
+
item.isActive = true;
|
|
1627
|
+
}
|
|
1628
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SideNavComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1629
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SideNavComponent, isStandalone: true, selector: "dso-side-nav", inputs: { title: "title", items: "items", open: "open", overlay: "overlay", defaultActiveRoute: "defaultActiveRoute", defaultActiveIndex: "defaultActiveIndex" }, outputs: { closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<!-- Optional overlay -->\r\n<div \r\n class=\"sidebar-overlay\" \r\n *ngIf=\"open && overlay\"\r\n (click)=\"closeSidebar()\">\r\n</div>\r\n\r\n<nav class=\"dso-side-nav\" [class.open]=\"open\">\r\n\r\n <!-- Logo and Title -->\r\n <div class=\"side-nav-header\">\r\n <ng-content select=\"[dso-side-logo]\"></ng-content>\r\n <span class=\"side-nav-title\">{{ title }}</span>\r\n </div>\r\n\r\n <!-- Navigation list -->\r\n <ul class=\"side-nav-list\">\r\n <li *ngFor=\"let item of items\" [class.active]=\"item.isActive\">\r\n <a\r\n class=\"nav-item\"\r\n [routerLink]=\"item.route\"\r\n routerLinkActive=\"active\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <!-- <i class=\"nav-icon\" [class]=\"item.icon\"></i> -->\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n\r\n <span class=\"nav-label\">{{ item.label }}</span>\r\n </a>\r\n </li>\r\n </ul>\r\n\r\n <!-- Footer slot (optional) -->\r\n <div class=\"side-nav-footer\">\r\n <ng-content select=\"[dso-side-footer]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: [".dso-side-nav{position:fixed;left:0;top:0;width:240px;height:100vh;background:#fff;border-right:1px solid #e5e5e5;display:flex;flex-direction:column;transform:translate(-100%);transition:transform .25s ease-in-out;z-index:1000}.dso-side-nav.open{transform:translate(0)}.dso-side-nav .side-nav-header{display:flex;align-items:center;gap:.75rem;padding:1rem;white-space:nowrap}.dso-side-nav .side-nav-title{font-size:1rem;font-weight:600}.dso-side-nav .side-nav-list{flex:1;list-style:none;padding:0 4px;margin:.5rem 0}.dso-side-nav .side-nav-list li{margin:.25rem 0}.dso-side-nav .side-nav-list li a.nav-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;border-radius:4px;text-decoration:none;color:#333}.dso-side-nav .side-nav-list li a.nav-item:hover{background:#f4f4f4}.dso-side-nav .side-nav-list li.active a.nav-item,.dso-side-nav .side-nav-list li a.nav-item.active{background:#e6f0ff;border-left:3px solid #007bff}.dso-side-nav .side-nav-list li.active a.nav-item .nav-label,.dso-side-nav .side-nav-list li a.nav-item.active .nav-label{font-weight:600}.dso-side-nav .side-nav-footer{padding:1rem;border-top:1px solid #e5e5e5}.sidebar-overlay{position:fixed;inset:0;background:#00000059;z-index:900}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$1.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
|
|
1630
|
+
}
|
|
1631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SideNavComponent, decorators: [{
|
|
1632
|
+
type: Component,
|
|
1633
|
+
args: [{ selector: 'dso-side-nav', standalone: true, imports: [CommonModule, RouterModule, IconComponent], template: "<!-- Optional overlay -->\r\n<div \r\n class=\"sidebar-overlay\" \r\n *ngIf=\"open && overlay\"\r\n (click)=\"closeSidebar()\">\r\n</div>\r\n\r\n<nav class=\"dso-side-nav\" [class.open]=\"open\">\r\n\r\n <!-- Logo and Title -->\r\n <div class=\"side-nav-header\">\r\n <ng-content select=\"[dso-side-logo]\"></ng-content>\r\n <span class=\"side-nav-title\">{{ title }}</span>\r\n </div>\r\n\r\n <!-- Navigation list -->\r\n <ul class=\"side-nav-list\">\r\n <li *ngFor=\"let item of items\" [class.active]=\"item.isActive\">\r\n <a\r\n class=\"nav-item\"\r\n [routerLink]=\"item.route\"\r\n routerLinkActive=\"active\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <!-- <i class=\"nav-icon\" [class]=\"item.icon\"></i> -->\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n\r\n <span class=\"nav-label\">{{ item.label }}</span>\r\n </a>\r\n </li>\r\n </ul>\r\n\r\n <!-- Footer slot (optional) -->\r\n <div class=\"side-nav-footer\">\r\n <ng-content select=\"[dso-side-footer]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: [".dso-side-nav{position:fixed;left:0;top:0;width:240px;height:100vh;background:#fff;border-right:1px solid #e5e5e5;display:flex;flex-direction:column;transform:translate(-100%);transition:transform .25s ease-in-out;z-index:1000}.dso-side-nav.open{transform:translate(0)}.dso-side-nav .side-nav-header{display:flex;align-items:center;gap:.75rem;padding:1rem;white-space:nowrap}.dso-side-nav .side-nav-title{font-size:1rem;font-weight:600}.dso-side-nav .side-nav-list{flex:1;list-style:none;padding:0 4px;margin:.5rem 0}.dso-side-nav .side-nav-list li{margin:.25rem 0}.dso-side-nav .side-nav-list li a.nav-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;border-radius:4px;text-decoration:none;color:#333}.dso-side-nav .side-nav-list li a.nav-item:hover{background:#f4f4f4}.dso-side-nav .side-nav-list li.active a.nav-item,.dso-side-nav .side-nav-list li a.nav-item.active{background:#e6f0ff;border-left:3px solid #007bff}.dso-side-nav .side-nav-list li.active a.nav-item .nav-label,.dso-side-nav .side-nav-list li a.nav-item.active .nav-label{font-weight:600}.dso-side-nav .side-nav-footer{padding:1rem;border-top:1px solid #e5e5e5}.sidebar-overlay{position:fixed;inset:0;background:#00000059;z-index:900}\n"] }]
|
|
1634
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { title: [{
|
|
1635
|
+
type: Input
|
|
1636
|
+
}], items: [{
|
|
1637
|
+
type: Input
|
|
1638
|
+
}], open: [{
|
|
1639
|
+
type: Input
|
|
1640
|
+
}], overlay: [{
|
|
1641
|
+
type: Input
|
|
1642
|
+
}], defaultActiveRoute: [{
|
|
1643
|
+
type: Input
|
|
1644
|
+
}], defaultActiveIndex: [{
|
|
1645
|
+
type: Input
|
|
1646
|
+
}], closed: [{
|
|
1647
|
+
type: Output
|
|
1648
|
+
}], onDocumentClick: [{
|
|
1649
|
+
type: HostListener,
|
|
1650
|
+
args: ['document:click', ['$event']]
|
|
1651
|
+
}] } });
|
|
1652
|
+
|
|
1653
|
+
class SpinnerComponent {
|
|
1654
|
+
el;
|
|
1655
|
+
/** Size presets */
|
|
1656
|
+
size = 'medium';
|
|
1657
|
+
/** Color variants */
|
|
1658
|
+
color = 'primary';
|
|
1659
|
+
/** Overlay background toggle */
|
|
1660
|
+
overlay = false;
|
|
1661
|
+
/** Custom overlay background color */
|
|
1662
|
+
overlayColor = 'rgba(0,0,0,0.4)';
|
|
1663
|
+
/** Optional illustration (inside spinner) */
|
|
1664
|
+
illustrationSrc;
|
|
1665
|
+
/** Optional label text (below spinner) */
|
|
1666
|
+
label;
|
|
1667
|
+
// NEW: optional Lottie animation
|
|
1668
|
+
animationSrc;
|
|
1669
|
+
constructor(el) {
|
|
1670
|
+
this.el = el;
|
|
1671
|
+
}
|
|
1672
|
+
ngAfterViewInit() {
|
|
1673
|
+
if (this.animationSrc) {
|
|
1674
|
+
lottie.loadAnimation({
|
|
1675
|
+
container: this.el.nativeElement.querySelector('.spinner-lottie'),
|
|
1676
|
+
renderer: 'svg',
|
|
1677
|
+
loop: true,
|
|
1678
|
+
autoplay: true,
|
|
1679
|
+
path: this.animationSrc, // path to your JSON animation
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
get isIllustrated() {
|
|
1684
|
+
return !!this.illustrationSrc;
|
|
1685
|
+
}
|
|
1686
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpinnerComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1687
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SpinnerComponent, isStandalone: true, selector: "dso-spinner", inputs: { size: "size", color: "color", overlay: "overlay", overlayColor: "overlayColor", illustrationSrc: "illustrationSrc", label: "label", animationSrc: "animationSrc" }, ngImport: i0, template: "<div *ngIf=\"overlay\" class=\"spinner-overlay\" [ngStyle]=\"{'background-color': overlayColor}\">\r\n <div class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n </div>\r\n</div>\r\n\r\n<div *ngIf=\"!overlay\" class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n</div>\r\n", styles: [".spinner-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;z-index:9999}.spinner-wrapper{display:flex;flex-direction:column;align-items:center;gap:8px}.spinner{position:relative;display:flex;justify-content:center;align-items:center}.spinner:before{content:\"\";position:absolute;inset:0;border-radius:50%;border:3px solid rgba(0,0,0,.1);border-top-color:#007bff;animation:spin .8s linear infinite}.spinner.small{width:32px;height:32px}.spinner.small:before{border-width:2px}.spinner.medium{width:48px;height:48px}.spinner.medium:before{border-width:3px}.spinner.large{width:72px;height:72px}.spinner.large:before{border-width:4px}.spinner.illustrated{width:240px;height:240px}.spinner.illustrated:before{border-width:6px}.spinner.illustrated .spinner-illustration{width:180px;height:180px}.spinner.primary:before{border-top-color:#007bff}.spinner.success:before{border-top-color:#28a745}.spinner.warning:before{border-top-color:#ffc107}.spinner.danger:before{border-top-color:#dc3545}.spinner-illustration{object-fit:contain;z-index:1}.spinner-label{font-size:14px;color:#fff;text-align:center}@keyframes spin{to{transform:rotate(360deg)}}.spinner-lottie{width:80px;height:80px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
|
|
1688
|
+
}
|
|
1689
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpinnerComponent, decorators: [{
|
|
1690
|
+
type: Component,
|
|
1691
|
+
args: [{ selector: 'dso-spinner', standalone: true, imports: [CommonModule], template: "<div *ngIf=\"overlay\" class=\"spinner-overlay\" [ngStyle]=\"{'background-color': overlayColor}\">\r\n <div class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n </div>\r\n</div>\r\n\r\n<div *ngIf=\"!overlay\" class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n</div>\r\n", styles: [".spinner-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;z-index:9999}.spinner-wrapper{display:flex;flex-direction:column;align-items:center;gap:8px}.spinner{position:relative;display:flex;justify-content:center;align-items:center}.spinner:before{content:\"\";position:absolute;inset:0;border-radius:50%;border:3px solid rgba(0,0,0,.1);border-top-color:#007bff;animation:spin .8s linear infinite}.spinner.small{width:32px;height:32px}.spinner.small:before{border-width:2px}.spinner.medium{width:48px;height:48px}.spinner.medium:before{border-width:3px}.spinner.large{width:72px;height:72px}.spinner.large:before{border-width:4px}.spinner.illustrated{width:240px;height:240px}.spinner.illustrated:before{border-width:6px}.spinner.illustrated .spinner-illustration{width:180px;height:180px}.spinner.primary:before{border-top-color:#007bff}.spinner.success:before{border-top-color:#28a745}.spinner.warning:before{border-top-color:#ffc107}.spinner.danger:before{border-top-color:#dc3545}.spinner-illustration{object-fit:contain;z-index:1}.spinner-label{font-size:14px;color:#fff;text-align:center}@keyframes spin{to{transform:rotate(360deg)}}.spinner-lottie{width:80px;height:80px}\n"] }]
|
|
1692
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { size: [{
|
|
1693
|
+
type: Input
|
|
1694
|
+
}], color: [{
|
|
1695
|
+
type: Input
|
|
1696
|
+
}], overlay: [{
|
|
1697
|
+
type: Input
|
|
1698
|
+
}], overlayColor: [{
|
|
1699
|
+
type: Input
|
|
1700
|
+
}], illustrationSrc: [{
|
|
1701
|
+
type: Input
|
|
1702
|
+
}], label: [{
|
|
1703
|
+
type: Input
|
|
1704
|
+
}], animationSrc: [{
|
|
1705
|
+
type: Input
|
|
1706
|
+
}] } });
|
|
1707
|
+
|
|
1708
|
+
class TooltipDirective {
|
|
1709
|
+
el;
|
|
1710
|
+
renderer;
|
|
1711
|
+
tooltipElement = null;
|
|
1712
|
+
// Make input reactive so it updates dynamically
|
|
1713
|
+
_tooltipText = '';
|
|
1714
|
+
set dsoDirectiveTooltip(value) {
|
|
1715
|
+
this._tooltipText = value;
|
|
1716
|
+
if (this.tooltipElement) {
|
|
1717
|
+
this.tooltipElement.textContent = value;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
get dsoDirectiveTooltip() {
|
|
1721
|
+
return this._tooltipText;
|
|
1722
|
+
}
|
|
1723
|
+
position = 'top';
|
|
1724
|
+
tooltipWidth = 'auto';
|
|
1725
|
+
gap = 4; // distance from the element
|
|
1726
|
+
constructor(el, renderer) {
|
|
1727
|
+
this.el = el;
|
|
1728
|
+
this.renderer = renderer;
|
|
1729
|
+
}
|
|
1730
|
+
onMouseEnter() {
|
|
1731
|
+
if (!this.dsoDirectiveTooltip)
|
|
1732
|
+
return;
|
|
1733
|
+
// Create tooltip element if not exists
|
|
1734
|
+
if (!this.tooltipElement) {
|
|
1735
|
+
this.tooltipElement = this.renderer.createElement('div');
|
|
1736
|
+
this.renderer.addClass(this.tooltipElement, 'tooltip-directive');
|
|
1737
|
+
// Style
|
|
1738
|
+
this.renderer.setStyle(this.tooltipElement, 'position', 'fixed'); // fixed so it won't be clipped
|
|
1739
|
+
this.renderer.setStyle(this.tooltipElement, 'background', '#333');
|
|
1740
|
+
this.renderer.setStyle(this.tooltipElement, 'color', '#fff');
|
|
1741
|
+
this.renderer.setStyle(this.tooltipElement, 'padding', '4px 8px');
|
|
1742
|
+
this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
|
|
1743
|
+
this.renderer.setStyle(this.tooltipElement, 'font-size', '12px');
|
|
1744
|
+
this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
|
|
1745
|
+
this.renderer.setStyle(this.tooltipElement, 'max-width', this.tooltipWidth);
|
|
1746
|
+
this.renderer.setStyle(this.tooltipElement, 'white-space', 'normal'); // allow wrapping
|
|
1747
|
+
this.renderer.setStyle(this.tooltipElement, 'word-break', 'break-word'); // break long words
|
|
1748
|
+
this.renderer.appendChild(document.body, this.tooltipElement);
|
|
1749
|
+
}
|
|
1750
|
+
// Step 3: set tooltip text
|
|
1751
|
+
if (this.tooltipElement) {
|
|
1752
|
+
this.tooltipElement.textContent = this.dsoDirectiveTooltip;
|
|
1753
|
+
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
1754
|
+
const tooltipHeight = this.tooltipElement.offsetHeight;
|
|
1755
|
+
const tooltipWidth = this.tooltipElement.offsetWidth;
|
|
1756
|
+
// Step 4: position tooltip based on input
|
|
1757
|
+
switch (this.position) {
|
|
1758
|
+
case 'top':
|
|
1759
|
+
this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top - tooltipHeight - this.gap}px`);
|
|
1760
|
+
this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left + (rect.width - tooltipWidth) / 2}px`);
|
|
1761
|
+
break;
|
|
1762
|
+
case 'bottom':
|
|
1763
|
+
this.renderer.setStyle(this.tooltipElement, 'top', `${rect.bottom + this.gap}px`);
|
|
1764
|
+
this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left + (rect.width - tooltipWidth) / 2}px`);
|
|
1765
|
+
break;
|
|
1766
|
+
case 'left':
|
|
1767
|
+
this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top + (rect.height - tooltipHeight) / 2}px`);
|
|
1768
|
+
this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left - tooltipWidth - this.gap}px`);
|
|
1769
|
+
break;
|
|
1770
|
+
case 'right':
|
|
1771
|
+
this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top + (rect.height - tooltipHeight) / 2}px`);
|
|
1772
|
+
this.renderer.setStyle(this.tooltipElement, 'left', `${rect.right + this.gap}px`);
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
this.renderer.setStyle(this.tooltipElement, 'visibility', 'visible');
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
onMouseLeave() {
|
|
1779
|
+
if (this.tooltipElement) {
|
|
1780
|
+
this.renderer.setStyle(this.tooltipElement, 'visibility', 'hidden');
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
ngOnDestroy() {
|
|
1784
|
+
if (this.tooltipElement) {
|
|
1785
|
+
this.renderer.removeChild(document.body, this.tooltipElement);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
|
1789
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TooltipDirective, isStandalone: true, selector: "[dsoDirectiveTooltip]", inputs: { dsoDirectiveTooltip: "dsoDirectiveTooltip", position: "position", tooltipWidth: "tooltipWidth" }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0 });
|
|
1790
|
+
}
|
|
1791
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipDirective, decorators: [{
|
|
1792
|
+
type: Directive,
|
|
1793
|
+
args: [{
|
|
1794
|
+
selector: '[dsoDirectiveTooltip]', // This directive will be used with the attribute dsoTooltip
|
|
1795
|
+
standalone: true
|
|
1796
|
+
}]
|
|
1797
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { dsoDirectiveTooltip: [{
|
|
1798
|
+
type: Input
|
|
1799
|
+
}], position: [{
|
|
1800
|
+
type: Input
|
|
1801
|
+
}], tooltipWidth: [{
|
|
1802
|
+
type: Input
|
|
1803
|
+
}], onMouseEnter: [{
|
|
1804
|
+
type: HostListener,
|
|
1805
|
+
args: ['mouseenter']
|
|
1806
|
+
}], onMouseLeave: [{
|
|
1807
|
+
type: HostListener,
|
|
1808
|
+
args: ['mouseleave']
|
|
1809
|
+
}] } });
|
|
1810
|
+
|
|
1811
|
+
class TableComponent {
|
|
1812
|
+
/* =====================
|
|
1813
|
+
Inputs
|
|
1814
|
+
===================== */
|
|
1815
|
+
columns = [];
|
|
1816
|
+
rows = [];
|
|
1817
|
+
defaultSortColumn;
|
|
1818
|
+
defaultSortDirection = 'asc';
|
|
1819
|
+
stickyHeader = false;
|
|
1820
|
+
maxTableHeight;
|
|
1821
|
+
/* =====================
|
|
1822
|
+
Outputs
|
|
1823
|
+
===================== */
|
|
1824
|
+
sortChange = new EventEmitter();
|
|
1825
|
+
/* =====================
|
|
1826
|
+
UI state only
|
|
1827
|
+
===================== */
|
|
1828
|
+
sortColumn = null;
|
|
1829
|
+
sortDirection = 'asc';
|
|
1830
|
+
/* =====================
|
|
1831
|
+
Lifecycle
|
|
1832
|
+
===================== */
|
|
1833
|
+
ngOnChanges() {
|
|
1834
|
+
// Initialize visual sort state ONLY
|
|
1835
|
+
if (this.defaultSortColumn && !this.sortColumn) {
|
|
1836
|
+
this.sortColumn = this.defaultSortColumn;
|
|
1837
|
+
this.sortDirection = this.defaultSortDirection;
|
|
1838
|
+
// Emit once so parent applies default sort
|
|
1839
|
+
this.sortChange.emit({
|
|
1840
|
+
column: this.sortColumn,
|
|
1841
|
+
direction: this.sortDirection
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/* =====================
|
|
1846
|
+
Sorting (emit only)
|
|
1847
|
+
===================== */
|
|
1848
|
+
sort(colKey) {
|
|
1849
|
+
let direction = 'asc';
|
|
1850
|
+
if (this.sortColumn === colKey) {
|
|
1851
|
+
direction = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
1852
|
+
}
|
|
1853
|
+
this.sortColumn = colKey;
|
|
1854
|
+
this.sortDirection = direction;
|
|
1855
|
+
this.sortChange.emit({ column: colKey, direction });
|
|
1856
|
+
}
|
|
1857
|
+
/* =====================
|
|
1858
|
+
Column sizing
|
|
1859
|
+
===================== */
|
|
1860
|
+
getColumnStyle(col) {
|
|
1861
|
+
return {
|
|
1862
|
+
'min-width': col.minWidth || null,
|
|
1863
|
+
'max-width': col.maxWidth || null,
|
|
1864
|
+
'width': !col.minWidth && !col.maxWidth ? 'auto' : null,
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
/* =====================
|
|
1868
|
+
Truncation
|
|
1869
|
+
===================== */
|
|
1870
|
+
shouldTruncateNative(cell) {
|
|
1871
|
+
if (!cell)
|
|
1872
|
+
return false;
|
|
1873
|
+
const content = cell.querySelector('.cell-text');
|
|
1874
|
+
if (!content)
|
|
1875
|
+
return false;
|
|
1876
|
+
return content.scrollWidth > content.clientWidth;
|
|
1877
|
+
}
|
|
1878
|
+
/* =====================
|
|
1879
|
+
Selection (unchanged)
|
|
1880
|
+
===================== */
|
|
1881
|
+
selectedRows = new Set();
|
|
1882
|
+
toggleRow(row) {
|
|
1883
|
+
if (this.selectedRows.has(row)) {
|
|
1884
|
+
this.selectedRows.delete(row);
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
this.selectedRows.add(row);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
isRowSelected(row) {
|
|
1891
|
+
return this.selectedRows.has(row);
|
|
1892
|
+
}
|
|
1893
|
+
get allSelected() {
|
|
1894
|
+
return this.rows.length > 0 &&
|
|
1895
|
+
this.selectedRows.size === this.rows.length;
|
|
1896
|
+
}
|
|
1897
|
+
toggleSelectAll() {
|
|
1898
|
+
if (this.allSelected) {
|
|
1899
|
+
this.selectedRows.clear();
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
this.rows.forEach(row => this.selectedRows.add(row));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
onEditRow(row) {
|
|
1906
|
+
console.log('Edit clicked for row:', row);
|
|
1907
|
+
}
|
|
1908
|
+
onDeleteRow(row) {
|
|
1909
|
+
console.log('Delete clicked for row:', row);
|
|
1910
|
+
}
|
|
1911
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1912
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TableComponent, isStandalone: true, selector: "dso-table", inputs: { columns: "columns", rows: "rows", defaultSortColumn: "defaultSortColumn", defaultSortDirection: "defaultSortDirection", stickyHeader: "stickyHeader", maxTableHeight: "maxTableHeight" }, outputs: { sortChange: "sortChange" }, usesOnChanges: true, ngImport: i0, template: "<div #tableContainer class=\"dso-table-container\" [class.sticky-header]=\"stickyHeader\" [style.max-height]=\"maxTableHeight\">\r\n <table class=\"dso-table\">\r\n<thead>\r\n <tr>\r\n <th *ngFor=\"let col of columns\"\r\n [class.sticky]=\"stickyHeader || col.sticky\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [class.active]=\"sortColumn === col.key\"\r\n class=\"sortable\"\r\n (click)=\"col.sortable !== false && col.type !== 'checkbox' && sort(col.key)\">\r\n\r\n <!-- If this column is a checkbox column -->\r\n <ng-container *ngIf=\"col.type === 'checkbox'; else normalHeader\">\r\n <dso-checkbox\r\n size=\"small\"\r\n [isChecked]=\"allSelected\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleSelectAll()\">\r\n </dso-checkbox>\r\n </ng-container>\r\n\r\n <!-- Your original header stays EXACTLY as-is -->\r\n <ng-template #normalHeader>\r\n <div class=\"header-content\">\r\n {{ col.label }}\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn === col.key\"\r\n [iconName]=\"sortDirection === 'asc'\r\n ? 'icon-van'\r\n : 'icon-chevron_down'\"\r\n size=\"small\"\r\n class=\"sort-icon\">\r\n </dso-icon>\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn !== col.key\"\r\n iconName=\"icon-chevron_up\"\r\n size=\"small\"\r\n class=\"hover-indicator\">\r\n </dso-icon>\r\n </div>\r\n </ng-template>\r\n\r\n </th>\r\n </tr>\r\n</thead>\r\n\r\n\r\n<tbody>\r\n <tr *ngFor=\"let row of rows\">\r\n <td\r\n #cell\r\n *ngFor=\"let col of columns\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [dsoDirectiveTooltip]=\"shouldTruncateNative(cell) ? row[col.key] : ''\">\r\n <div class=\"cell-wrapper\" style=\"display:flex; align-items:center;\">\r\n <dso-checkbox\r\n *ngIf=\"col.type==='checkbox'\"\r\n size=\"small\"\r\n [isChecked]=\"isRowSelected(row)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleRow(row)\">\r\n </dso-checkbox>\r\n\r\n <!-- Action button cell -->\r\n <ng-container *ngIf=\"col.type==='action'\">\r\n <dso-button\r\n btnType=\"filled\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Edit\"\r\n (click)=\"onEditRow(row)\">\r\n </dso-button>\r\n <dso-button\r\n btnType=\"outlined\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Delete\"\r\n (click)=\"onDeleteRow(row)\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Normal text cell -->\r\n <span *ngIf=\"!col.type || (col.type !== 'checkbox' && col.type !== 'action')\" class=\"cell-text\">\r\n {{ row[col.key] }}\r\n </span>\r\n </div>\r\n </td>\r\n </tr>\r\n</tbody>\r\n\r\n </table>\r\n</div>\r\n", styles: [".dso-table-container{overflow:auto;border:1px solid #ddd;position:relative}.dso-table{width:100%;border-collapse:collapse}.dso-table th,.dso-table td{padding:8px 12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dso-table thead th{background:#2c2f36;color:#fff;font-weight:600}.dso-table-container.sticky-header .dso-table th.sticky{position:sticky;top:0}.dso-table th.sticky{top:0;z-index:20;box-shadow:0 2px 2px -1px #00000040}.dso-table thead th.sortable:hover{background:#3a3e46;cursor:pointer}.dso-table thead th.active{background:#464b54!important}.dso-table thead th:last-child{border-right:none}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td .cell-text{display:inline-block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cell-wrapper{display:inline-flex;align-items:center;margin:0;padding:0;gap:8px}.sticky-left{position:sticky;left:0;z-index:15;background:#fff}.sticky-right{position:sticky;right:0;z-index:15;background:#fff}.dso-table thead th.sticky-left,.dso-table thead th.sticky-right{z-index:25}.sticky-left.scroll-shadow{box-shadow:4px 0 6px -3px #00000040}.sticky-right.scroll-shadow{box-shadow:-4px 0 6px -3px #00000040}.dso-table tbody tr:hover{background:#f5f7fa}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: CheckboxComponent, selector: "dso-checkbox", inputs: ["label", "isChecked", "disabled", "size", "error", "required", "errorMessage", "iconName"], outputs: ["change"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "directive", type: TooltipDirective, selector: "[dsoDirectiveTooltip]", inputs: ["dsoDirectiveTooltip", "position", "tooltipWidth"] }] });
|
|
1913
|
+
}
|
|
1914
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableComponent, decorators: [{
|
|
1915
|
+
type: Component,
|
|
1916
|
+
args: [{ selector: 'dso-table', standalone: true, imports: [
|
|
1917
|
+
ButtonComponent,
|
|
1918
|
+
CheckboxComponent,
|
|
1919
|
+
CommonModule,
|
|
1920
|
+
IconComponent,
|
|
1921
|
+
TooltipDirective
|
|
1922
|
+
], template: "<div #tableContainer class=\"dso-table-container\" [class.sticky-header]=\"stickyHeader\" [style.max-height]=\"maxTableHeight\">\r\n <table class=\"dso-table\">\r\n<thead>\r\n <tr>\r\n <th *ngFor=\"let col of columns\"\r\n [class.sticky]=\"stickyHeader || col.sticky\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [class.active]=\"sortColumn === col.key\"\r\n class=\"sortable\"\r\n (click)=\"col.sortable !== false && col.type !== 'checkbox' && sort(col.key)\">\r\n\r\n <!-- If this column is a checkbox column -->\r\n <ng-container *ngIf=\"col.type === 'checkbox'; else normalHeader\">\r\n <dso-checkbox\r\n size=\"small\"\r\n [isChecked]=\"allSelected\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleSelectAll()\">\r\n </dso-checkbox>\r\n </ng-container>\r\n\r\n <!-- Your original header stays EXACTLY as-is -->\r\n <ng-template #normalHeader>\r\n <div class=\"header-content\">\r\n {{ col.label }}\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn === col.key\"\r\n [iconName]=\"sortDirection === 'asc'\r\n ? 'icon-van'\r\n : 'icon-chevron_down'\"\r\n size=\"small\"\r\n class=\"sort-icon\">\r\n </dso-icon>\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn !== col.key\"\r\n iconName=\"icon-chevron_up\"\r\n size=\"small\"\r\n class=\"hover-indicator\">\r\n </dso-icon>\r\n </div>\r\n </ng-template>\r\n\r\n </th>\r\n </tr>\r\n</thead>\r\n\r\n\r\n<tbody>\r\n <tr *ngFor=\"let row of rows\">\r\n <td\r\n #cell\r\n *ngFor=\"let col of columns\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [dsoDirectiveTooltip]=\"shouldTruncateNative(cell) ? row[col.key] : ''\">\r\n <div class=\"cell-wrapper\" style=\"display:flex; align-items:center;\">\r\n <dso-checkbox\r\n *ngIf=\"col.type==='checkbox'\"\r\n size=\"small\"\r\n [isChecked]=\"isRowSelected(row)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleRow(row)\">\r\n </dso-checkbox>\r\n\r\n <!-- Action button cell -->\r\n <ng-container *ngIf=\"col.type==='action'\">\r\n <dso-button\r\n btnType=\"filled\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Edit\"\r\n (click)=\"onEditRow(row)\">\r\n </dso-button>\r\n <dso-button\r\n btnType=\"outlined\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Delete\"\r\n (click)=\"onDeleteRow(row)\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Normal text cell -->\r\n <span *ngIf=\"!col.type || (col.type !== 'checkbox' && col.type !== 'action')\" class=\"cell-text\">\r\n {{ row[col.key] }}\r\n </span>\r\n </div>\r\n </td>\r\n </tr>\r\n</tbody>\r\n\r\n </table>\r\n</div>\r\n", styles: [".dso-table-container{overflow:auto;border:1px solid #ddd;position:relative}.dso-table{width:100%;border-collapse:collapse}.dso-table th,.dso-table td{padding:8px 12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dso-table thead th{background:#2c2f36;color:#fff;font-weight:600}.dso-table-container.sticky-header .dso-table th.sticky{position:sticky;top:0}.dso-table th.sticky{top:0;z-index:20;box-shadow:0 2px 2px -1px #00000040}.dso-table thead th.sortable:hover{background:#3a3e46;cursor:pointer}.dso-table thead th.active{background:#464b54!important}.dso-table thead th:last-child{border-right:none}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td .cell-text{display:inline-block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cell-wrapper{display:inline-flex;align-items:center;margin:0;padding:0;gap:8px}.sticky-left{position:sticky;left:0;z-index:15;background:#fff}.sticky-right{position:sticky;right:0;z-index:15;background:#fff}.dso-table thead th.sticky-left,.dso-table thead th.sticky-right{z-index:25}.sticky-left.scroll-shadow{box-shadow:4px 0 6px -3px #00000040}.sticky-right.scroll-shadow{box-shadow:-4px 0 6px -3px #00000040}.dso-table tbody tr:hover{background:#f5f7fa}\n"] }]
|
|
1923
|
+
}], propDecorators: { columns: [{
|
|
1924
|
+
type: Input
|
|
1925
|
+
}], rows: [{
|
|
1926
|
+
type: Input
|
|
1927
|
+
}], defaultSortColumn: [{
|
|
1928
|
+
type: Input
|
|
1929
|
+
}], defaultSortDirection: [{
|
|
1930
|
+
type: Input
|
|
1931
|
+
}], stickyHeader: [{
|
|
1932
|
+
type: Input
|
|
1933
|
+
}], maxTableHeight: [{
|
|
1934
|
+
type: Input
|
|
1935
|
+
}], sortChange: [{
|
|
1936
|
+
type: Output
|
|
1937
|
+
}] } });
|
|
1938
|
+
|
|
1939
|
+
// tab.component.ts
|
|
1940
|
+
class TabComponent {
|
|
1941
|
+
/** Label for the tab shown in the tabs header */
|
|
1942
|
+
label = '';
|
|
1943
|
+
/** Is this tab currently active? Controlled by TabsComponent */
|
|
1944
|
+
active = false;
|
|
1945
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1946
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TabComponent, isStandalone: true, selector: "dso-tab", inputs: { label: "label" }, ngImport: i0, template: "<!-- tab.component.html -->\r\n<!-- Only display the tab content if it is active -->\r\n<div *ngIf=\"active\" class=\"tab-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1947
|
+
}
|
|
1948
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabComponent, decorators: [{
|
|
1949
|
+
type: Component,
|
|
1950
|
+
args: [{ selector: 'dso-tab', imports: [CommonModule], standalone: true, template: "<!-- tab.component.html -->\r\n<!-- Only display the tab content if it is active -->\r\n<div *ngIf=\"active\" class=\"tab-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n" }]
|
|
1951
|
+
}], propDecorators: { label: [{
|
|
1952
|
+
type: Input
|
|
1953
|
+
}] } });
|
|
1954
|
+
|
|
1955
|
+
// tabs.component.ts
|
|
1956
|
+
class TabsComponent {
|
|
1957
|
+
/** Query all child tabs inside <dso-tabs> */
|
|
1958
|
+
tabs;
|
|
1959
|
+
/** Optional: two-way binding for active tab index */
|
|
1960
|
+
tabIndex = 0;
|
|
1961
|
+
tabIndexChange = new EventEmitter();
|
|
1962
|
+
ngAfterContentInit() {
|
|
1963
|
+
// Activate the default tab
|
|
1964
|
+
this.selectTab(this.tabIndex);
|
|
1965
|
+
}
|
|
1966
|
+
/** When a tab header is clicked */
|
|
1967
|
+
selectTab(index) {
|
|
1968
|
+
if (!this.tabs)
|
|
1969
|
+
return;
|
|
1970
|
+
this.tabs.forEach((tab, i) => tab.active = i === index);
|
|
1971
|
+
// Update internal tabIndex and emit event for two-way binding
|
|
1972
|
+
this.tabIndex = index;
|
|
1973
|
+
this.tabIndexChange.emit(this.tabIndex);
|
|
1974
|
+
}
|
|
1975
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1976
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TabsComponent, isStandalone: true, selector: "dso-tabs", inputs: { tabIndex: "tabIndex" }, outputs: { tabIndexChange: "tabIndexChange" }, queries: [{ propertyName: "tabs", predicate: TabComponent }], ngImport: i0, template: "<!-- tabs.component.html -->\r\n<div class=\"tabs-container\">\r\n <!-- Tab headers -->\r\n <div class=\"tab-headers\">\r\n <button\r\n *ngFor=\"let tab of tabs; let i = index\"\r\n [class.active]=\"tab.active\"\r\n (click)=\"selectTab(i)\">\r\n {{ tab.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Tab content will render via ng-content inside TabComponent -->\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".tabs-container{border:1px solid #ccc;border-radius:4px}.tab-headers{display:flex;border-bottom:1px solid #ccc}.tab-headers button{flex:1;padding:8px 12px;background:none;border:none;cursor:pointer;font-weight:500}.tab-headers button.active{border-bottom:2px solid #007bff;font-weight:700}.tab-headers button:hover{background-color:#f5f5f5}.tab-content{padding:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
1977
|
+
}
|
|
1978
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabsComponent, decorators: [{
|
|
1979
|
+
type: Component,
|
|
1980
|
+
args: [{ selector: 'dso-tabs', standalone: true, imports: [CommonModule, TabComponent], template: "<!-- tabs.component.html -->\r\n<div class=\"tabs-container\">\r\n <!-- Tab headers -->\r\n <div class=\"tab-headers\">\r\n <button\r\n *ngFor=\"let tab of tabs; let i = index\"\r\n [class.active]=\"tab.active\"\r\n (click)=\"selectTab(i)\">\r\n {{ tab.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Tab content will render via ng-content inside TabComponent -->\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".tabs-container{border:1px solid #ccc;border-radius:4px}.tab-headers{display:flex;border-bottom:1px solid #ccc}.tab-headers button{flex:1;padding:8px 12px;background:none;border:none;cursor:pointer;font-weight:500}.tab-headers button.active{border-bottom:2px solid #007bff;font-weight:700}.tab-headers button:hover{background-color:#f5f5f5}.tab-content{padding:12px}\n"] }]
|
|
1981
|
+
}], propDecorators: { tabs: [{
|
|
1982
|
+
type: ContentChildren,
|
|
1983
|
+
args: [TabComponent]
|
|
1984
|
+
}], tabIndex: [{
|
|
1985
|
+
type: Input
|
|
1986
|
+
}], tabIndexChange: [{
|
|
1987
|
+
type: Output
|
|
1988
|
+
}] } });
|
|
1989
|
+
|
|
1990
|
+
class TagComponent {
|
|
1991
|
+
variant = 'neutral';
|
|
1992
|
+
removable = false;
|
|
1993
|
+
label = '';
|
|
1994
|
+
removed = false;
|
|
1995
|
+
removeTag() {
|
|
1996
|
+
this.removed = true;
|
|
1997
|
+
}
|
|
1998
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1999
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TagComponent, isStandalone: true, selector: "dso-tag", inputs: { variant: "variant", removable: "removable", label: "label" }, ngImport: i0, template: "<div\r\n *ngIf=\"!removed\"\r\n class=\"tag\"\r\n [ngClass]=\"'tag-' + variant\"\r\n>\r\n <span class=\"tag-label\">{{ label }}</span>\r\n <!-- <dso-button></dso-button> -->\r\n <button \r\n *ngIf=\"removable\" \r\n class=\"tag-remove-btn\" \r\n (click)=\"removeTag()\"\r\n aria-label=\"Remove tag\"\r\n >\r\n ×\r\n </button>\r\n</div>\r\n\r\n", styles: [".tag{display:inline-flex;align-items:center;padding:4px 12px;font-size:.875rem;border-radius:16px;font-weight:500;white-space:nowrap;background-color:#e0e0e0;color:#333;gap:2px}.tag-success{background-color:#d4edda;color:#155724}.tag-info{background-color:#d1ecf1;color:#0c5460}.tag-warning{background-color:#fff3cd;color:#856404}.tag-danger{background-color:#f8d7da;color:#721c24}.tag-neutral{background-color:#e2e3e5;color:#383d41}.tag-remove-btn{background:transparent;border:none;font-size:1rem;line-height:1;cursor:pointer;color:inherit}.tag-remove-btn:hover{opacity:.6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
2000
|
+
}
|
|
2001
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TagComponent, decorators: [{
|
|
2002
|
+
type: Component,
|
|
2003
|
+
args: [{ selector: 'dso-tag', standalone: true, imports: [CommonModule, ButtonComponent], template: "<div\r\n *ngIf=\"!removed\"\r\n class=\"tag\"\r\n [ngClass]=\"'tag-' + variant\"\r\n>\r\n <span class=\"tag-label\">{{ label }}</span>\r\n <!-- <dso-button></dso-button> -->\r\n <button \r\n *ngIf=\"removable\" \r\n class=\"tag-remove-btn\" \r\n (click)=\"removeTag()\"\r\n aria-label=\"Remove tag\"\r\n >\r\n ×\r\n </button>\r\n</div>\r\n\r\n", styles: [".tag{display:inline-flex;align-items:center;padding:4px 12px;font-size:.875rem;border-radius:16px;font-weight:500;white-space:nowrap;background-color:#e0e0e0;color:#333;gap:2px}.tag-success{background-color:#d4edda;color:#155724}.tag-info{background-color:#d1ecf1;color:#0c5460}.tag-warning{background-color:#fff3cd;color:#856404}.tag-danger{background-color:#f8d7da;color:#721c24}.tag-neutral{background-color:#e2e3e5;color:#383d41}.tag-remove-btn{background:transparent;border:none;font-size:1rem;line-height:1;cursor:pointer;color:inherit}.tag-remove-btn:hover{opacity:.6}\n"] }]
|
|
2004
|
+
}], propDecorators: { variant: [{
|
|
2005
|
+
type: Input
|
|
2006
|
+
}], removable: [{
|
|
2007
|
+
type: Input
|
|
2008
|
+
}], label: [{
|
|
2009
|
+
type: Input
|
|
2010
|
+
}] } });
|
|
2011
|
+
|
|
2012
|
+
class TextAreaComponent {
|
|
2013
|
+
inputLabel = ''; // Optional label for the textarea
|
|
2014
|
+
placeholder = '';
|
|
2015
|
+
value = ''; // Two-way binding for value
|
|
2016
|
+
helperText = 'helpering '; // Optional helper text
|
|
2017
|
+
errorText = 'tes test'; // Optional error text
|
|
2018
|
+
isDisabled = false; // Optional disabled state
|
|
2019
|
+
enableValidation = true; // Toggle validation
|
|
2020
|
+
maxLength = 300; // Maximum character length (for example)
|
|
2021
|
+
isTouched = false;
|
|
2022
|
+
isInvalid = false; // Error state
|
|
2023
|
+
charCount = 0; // To track character count
|
|
2024
|
+
isExceeded = false; // To track if the limit is exceeded
|
|
2025
|
+
onInputChange(event) {
|
|
2026
|
+
const inputValue = event.target.value;
|
|
2027
|
+
console.log(inputValue);
|
|
2028
|
+
const charCount = inputValue.length;
|
|
2029
|
+
// Check if the limit has been exceeded
|
|
2030
|
+
this.isExceeded = charCount > this.maxLength;
|
|
2031
|
+
this.isTouched = true;
|
|
2032
|
+
this.checkValidity();
|
|
2033
|
+
}
|
|
2034
|
+
// Method to determine the style of the character count
|
|
2035
|
+
getCharCountStyle() {
|
|
2036
|
+
const charCount = this.value.length;
|
|
2037
|
+
if (this.isExceeded) {
|
|
2038
|
+
// this.isExceeded = true;
|
|
2039
|
+
return 'char-count-error';
|
|
2040
|
+
}
|
|
2041
|
+
// If character count exceeds 50% of maxLength, change color to black
|
|
2042
|
+
if (charCount > this.maxLength / 2) {
|
|
2043
|
+
return '';
|
|
2044
|
+
}
|
|
2045
|
+
// Otherwise, keep it grey
|
|
2046
|
+
return '';
|
|
2047
|
+
}
|
|
2048
|
+
onBlur() {
|
|
2049
|
+
this.isTouched = true;
|
|
2050
|
+
this.checkValidity();
|
|
2051
|
+
}
|
|
2052
|
+
checkValidity() {
|
|
2053
|
+
// Set to invalid if it's required and empty
|
|
2054
|
+
this.isInvalid = this.isTouched && !this.value.trim();
|
|
2055
|
+
}
|
|
2056
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextAreaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2057
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TextAreaComponent, isStandalone: true, selector: "dso-text-area", inputs: { inputLabel: "inputLabel", placeholder: "placeholder", value: "value", helperText: "helperText", errorText: "errorText", isDisabled: "isDisabled", enableValidation: "enableValidation", maxLength: "maxLength" }, ngImport: i0, template: "<div class=\"text-area-container\">\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }}\r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div>\r\n\r\n <textarea\r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\"\r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\"\r\n [class.disabled]=\"isDisabled\"\r\n >\r\n </textarea>\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n", styles: [".text-area-container{display:flex;flex-direction:column;width:500px;gap:8px}.text-wrapper{display:flex;justify-content:space-between}textarea{width:100%;height:160px;padding:12px 8px;font-size:1rem;resize:vertical;border:1px solid #ccc;border-radius:4px}textarea:hover{border:1px solid #071d35}textarea:focus{border-color:#007bff;outline:none}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}.helper-text{font-size:16px;color:#555}.error-message{font-size:16px;color:red}span .error-message{color:red;font-size:12px}textarea.invalid{border-color:red}textarea.disabled{background-color:#f0f0f0;cursor:not-allowed;border-color:#ccc}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
2058
|
+
}
|
|
2059
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextAreaComponent, decorators: [{
|
|
2060
|
+
type: Component,
|
|
2061
|
+
args: [{ selector: 'dso-text-area', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"text-area-container\">\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }}\r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div>\r\n\r\n <textarea\r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\"\r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\"\r\n [class.disabled]=\"isDisabled\"\r\n >\r\n </textarea>\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n", styles: [".text-area-container{display:flex;flex-direction:column;width:500px;gap:8px}.text-wrapper{display:flex;justify-content:space-between}textarea{width:100%;height:160px;padding:12px 8px;font-size:1rem;resize:vertical;border:1px solid #ccc;border-radius:4px}textarea:hover{border:1px solid #071d35}textarea:focus{border-color:#007bff;outline:none}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}.helper-text{font-size:16px;color:#555}.error-message{font-size:16px;color:red}span .error-message{color:red;font-size:12px}textarea.invalid{border-color:red}textarea.disabled{background-color:#f0f0f0;cursor:not-allowed;border-color:#ccc}\n"] }]
|
|
2062
|
+
}], propDecorators: { inputLabel: [{
|
|
2063
|
+
type: Input
|
|
2064
|
+
}], placeholder: [{
|
|
2065
|
+
type: Input
|
|
2066
|
+
}], value: [{
|
|
2067
|
+
type: Input
|
|
2068
|
+
}], helperText: [{
|
|
2069
|
+
type: Input
|
|
2070
|
+
}], errorText: [{
|
|
2071
|
+
type: Input
|
|
2072
|
+
}], isDisabled: [{
|
|
2073
|
+
type: Input
|
|
2074
|
+
}], enableValidation: [{
|
|
2075
|
+
type: Input
|
|
2076
|
+
}], maxLength: [{
|
|
2077
|
+
type: Input
|
|
2078
|
+
}] } });
|
|
2079
|
+
|
|
2080
|
+
class ToastService {
|
|
2081
|
+
// private toastSubject = new Subject<ToastMessage>();
|
|
2082
|
+
toastSubject = new ReplaySubject; // keep last message
|
|
2083
|
+
toast$ = this.toastSubject.asObservable();
|
|
2084
|
+
show(header, message, variant = 'neutral', duration) {
|
|
2085
|
+
this.toastSubject.next({ header, message, variant, duration });
|
|
2086
|
+
console.log('Toast Service Broadcasting:', { header, message, variant, duration }); // <-- debug
|
|
2087
|
+
}
|
|
2088
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2089
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, providedIn: 'root' });
|
|
2090
|
+
}
|
|
2091
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, decorators: [{
|
|
2092
|
+
type: Injectable,
|
|
2093
|
+
args: [{ providedIn: 'root' }]
|
|
2094
|
+
}] });
|
|
2095
|
+
|
|
2096
|
+
class ToastComponent {
|
|
2097
|
+
toastService;
|
|
2098
|
+
toasts = [];
|
|
2099
|
+
position = 'top-right';
|
|
2100
|
+
toastData; // Stores the current toast being displayed
|
|
2101
|
+
constructor(toastService) {
|
|
2102
|
+
this.toastService = toastService;
|
|
2103
|
+
} // Inject toast service
|
|
2104
|
+
ngOnInit() {
|
|
2105
|
+
// Subscribe to toast messages from the service
|
|
2106
|
+
this.toastService.toast$.subscribe((toast) => {
|
|
2107
|
+
console.log('Toast Child Listening:', toast); // Add this debug log!
|
|
2108
|
+
this.toasts.push(toast); // Add the new toast to the array for display
|
|
2109
|
+
// Auto-hide after specified duration (default 3000ms)
|
|
2110
|
+
setTimeout(() => {
|
|
2111
|
+
// this.visible = false; // Hide toast
|
|
2112
|
+
this.toasts.shift(); // Remove first toast from the array
|
|
2113
|
+
}, toast.duration || 3000);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastComponent, deps: [{ token: ToastService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2117
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ToastComponent, isStandalone: true, selector: "dso-toast", inputs: { position: "position" }, ngImport: i0, template: "<div class=\"toast-wrapper\" \r\n [ngClass]=\"['toast-' + (position || 'top-right')]\">\r\n <div\r\n *ngFor=\"let toast of toasts; let i = index\"\r\n class=\"toast-container\"\r\n [ngClass]=\"['toast-' + (toast.variant || 'neutral')]\"\r\n >\r\n \r\n <div class=\"toast-header\" *ngIf=\"toast.header\">{{ toast.header }}</div>\r\n <div class=\"toast-body\">\r\n <div class=\"toast-message\">{{ toast.message }}</div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- \r\n[ngStyle]=\"{\r\n animation: 'fadeInOut ' + (toast.duration || 3000) + 'ms ease forwards'}\"> -->", styles: [".toast-wrapper{position:fixed;display:flex;flex-direction:column;z-index:1000;gap:16px}.toast-container{padding:12px 16px;border-radius:8px;box-shadow:0 4px 12px #0003;color:#fff;width:280px;font-family:sans-serif;animation:fadeIn .3s ease forwards}.toast-top-right{top:20px;right:20px;align-items:flex-end}.toast-top-left{top:20px;left:20px;align-items:flex-start}.toast-bottom-right{bottom:20px;right:20px;align-items:flex-end}.toast-bottom-left{bottom:20px;left:20px;align-items:flex-start}.toast-top-center{top:20px;left:50%;transform:translate(-50%)}.toast-bottom-center{bottom:20px;left:50%;transform:translate(-50%)}.toast-success{background-color:#28a745}.toast-info{background-color:#17a2b8}.toast-warning{background-color:#ffc107;color:#333}.toast-danger{background-color:#dc3545}.toast-neutral{background-color:#6c757d}.toast-header{font-weight:600;margin-bottom:4px}.toast-subtext{font-size:.85rem;opacity:.8}@keyframes fadeIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
2118
|
+
}
|
|
2119
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastComponent, decorators: [{
|
|
2120
|
+
type: Component,
|
|
2121
|
+
args: [{ selector: 'dso-toast', standalone: true, imports: [CommonModule] // Import CommonModule for ngIf, ngFor etc.
|
|
2122
|
+
, template: "<div class=\"toast-wrapper\" \r\n [ngClass]=\"['toast-' + (position || 'top-right')]\">\r\n <div\r\n *ngFor=\"let toast of toasts; let i = index\"\r\n class=\"toast-container\"\r\n [ngClass]=\"['toast-' + (toast.variant || 'neutral')]\"\r\n >\r\n \r\n <div class=\"toast-header\" *ngIf=\"toast.header\">{{ toast.header }}</div>\r\n <div class=\"toast-body\">\r\n <div class=\"toast-message\">{{ toast.message }}</div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- \r\n[ngStyle]=\"{\r\n animation: 'fadeInOut ' + (toast.duration || 3000) + 'ms ease forwards'}\"> -->", styles: [".toast-wrapper{position:fixed;display:flex;flex-direction:column;z-index:1000;gap:16px}.toast-container{padding:12px 16px;border-radius:8px;box-shadow:0 4px 12px #0003;color:#fff;width:280px;font-family:sans-serif;animation:fadeIn .3s ease forwards}.toast-top-right{top:20px;right:20px;align-items:flex-end}.toast-top-left{top:20px;left:20px;align-items:flex-start}.toast-bottom-right{bottom:20px;right:20px;align-items:flex-end}.toast-bottom-left{bottom:20px;left:20px;align-items:flex-start}.toast-top-center{top:20px;left:50%;transform:translate(-50%)}.toast-bottom-center{bottom:20px;left:50%;transform:translate(-50%)}.toast-success{background-color:#28a745}.toast-info{background-color:#17a2b8}.toast-warning{background-color:#ffc107;color:#333}.toast-danger{background-color:#dc3545}.toast-neutral{background-color:#6c757d}.toast-header{font-weight:600;margin-bottom:4px}.toast-subtext{font-size:.85rem;opacity:.8}@keyframes fadeIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}\n"] }]
|
|
2123
|
+
}], ctorParameters: () => [{ type: ToastService }], propDecorators: { position: [{
|
|
2124
|
+
type: Input
|
|
2125
|
+
}] } });
|
|
2126
|
+
|
|
2127
|
+
class TooltipComponent {
|
|
2128
|
+
el;
|
|
2129
|
+
text; // Tooltip text to display
|
|
2130
|
+
position = 'top'; // Position of the tooltip (default is 'top')
|
|
2131
|
+
tooltipWidth = '300px'; // Default width, can be overridden
|
|
2132
|
+
constructor(el) {
|
|
2133
|
+
this.el = el;
|
|
2134
|
+
}
|
|
2135
|
+
showTooltip = false; // This flag controls the visibility of the tooltip
|
|
2136
|
+
isHovered = false; // This flag checks if the mouse is over the tooltip
|
|
2137
|
+
// Triggered when the mouse enters the wrapper area (button + tooltip)
|
|
2138
|
+
onMouseEnter() {
|
|
2139
|
+
// console.log('Mouse entered');
|
|
2140
|
+
this.showTooltip = true; // Show tooltip
|
|
2141
|
+
}
|
|
2142
|
+
// Triggered when the mouse leaves the wrapper area (button + tooltip)
|
|
2143
|
+
onMouseLeave() {
|
|
2144
|
+
// console.log('Mouse left');
|
|
2145
|
+
this.showTooltip = false; // Hide tooltip
|
|
2146
|
+
}
|
|
2147
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2148
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TooltipComponent, isStandalone: true, selector: "dso-tooltip", inputs: { text: "text", position: "position", tooltipWidth: "tooltipWidth" }, ngImport: i0, template: "<!-- Tooltip wrapper: listens for mouse enter/leave -->\r\n<div class=\"tooltip-wrapper\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\r\n <ng-content></ng-content> <!-- This will allow the button or other content to be passed inside -->\r\n\r\n <!-- Tooltip box -->\r\n <div \r\n class=\"tooltip\" \r\n *ngIf=\"showTooltip\" \r\n [ngClass]=\"position\"\r\n [ngStyle]=\"{ 'width': tooltipWidth }\"\r\n >\r\n {{ text }}\r\n </div>\r\n</div>", styles: [".tooltip-wrapper{position:relative}.tooltip{position:absolute;background-color:#000;color:#fff;display:block;white-space:normal;word-break:break-word;border-radius:4px;pointer-events:auto;padding:12px;font-size:12px;z-index:100}.tooltip.top{bottom:calc(100% + 4px);transform:translate(-25%)}.tooltip.bottom{top:calc(100% + 4px);transform:translate(-25%)}.tooltip.left{top:0%;right:calc(100% + 4px);transform:translateY(-25%)}.tooltip.right{bottom:0%;left:calc(100% + 4px);transform:translateY(25%)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
|
|
2149
|
+
}
|
|
2150
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipComponent, decorators: [{
|
|
2151
|
+
type: Component,
|
|
2152
|
+
args: [{ selector: 'dso-tooltip', standalone: true, imports: [CommonModule], template: "<!-- Tooltip wrapper: listens for mouse enter/leave -->\r\n<div class=\"tooltip-wrapper\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\r\n <ng-content></ng-content> <!-- This will allow the button or other content to be passed inside -->\r\n\r\n <!-- Tooltip box -->\r\n <div \r\n class=\"tooltip\" \r\n *ngIf=\"showTooltip\" \r\n [ngClass]=\"position\"\r\n [ngStyle]=\"{ 'width': tooltipWidth }\"\r\n >\r\n {{ text }}\r\n </div>\r\n</div>", styles: [".tooltip-wrapper{position:relative}.tooltip{position:absolute;background-color:#000;color:#fff;display:block;white-space:normal;word-break:break-word;border-radius:4px;pointer-events:auto;padding:12px;font-size:12px;z-index:100}.tooltip.top{bottom:calc(100% + 4px);transform:translate(-25%)}.tooltip.bottom{top:calc(100% + 4px);transform:translate(-25%)}.tooltip.left{top:0%;right:calc(100% + 4px);transform:translateY(-25%)}.tooltip.right{bottom:0%;left:calc(100% + 4px);transform:translateY(25%)}\n"] }]
|
|
2153
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { text: [{
|
|
2154
|
+
type: Input
|
|
2155
|
+
}], position: [{
|
|
2156
|
+
type: Input
|
|
2157
|
+
}], tooltipWidth: [{
|
|
2158
|
+
type: Input
|
|
2159
|
+
}] } });
|
|
2160
|
+
|
|
2161
|
+
class TopNavComponent {
|
|
2162
|
+
/** App name displayed next to logo */
|
|
2163
|
+
appName = '';
|
|
2164
|
+
/** Dynamic menu items */
|
|
2165
|
+
menuItems = [];
|
|
2166
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TopNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2167
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TopNavComponent, isStandalone: true, selector: "dso-top-nav", inputs: { appName: "appName", menuItems: "menuItems" }, ngImport: i0, template: "<nav class=\"dso-top-nav\">\r\n\r\n <!-- Left section: menu+logo -->\r\n <div class=\"nav-left\">\r\n <div class=\"nav-left-group menu-logo\">\r\n <ng-content select=\"[dso-menu-logo]\"></ng-content>\r\n <span class=\"top-nav-app-name\" *ngIf=\"appName\">{{ appName }}</span>\r\n </div>\r\n\r\n <div class=\"nav-left-group menu-buttons\">\r\n <ng-container *ngFor=\"let item of menuItems\">\r\n <!-- Router link item -->\r\n <a \r\n *ngIf=\"item.route; else clickItem\" \r\n [routerLink]=\"item.route\" \r\n class=\"top-nav-item\"\r\n >\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </a>\r\n\r\n <!-- Click handler item -->\r\n <ng-template #clickItem>\r\n <button class=\"top-nav-item\" (click)=\"item.click?.()\">\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </button>\r\n </ng-template>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Right section -->\r\n <div class=\"nav-right\">\r\n <!-- \u2705 Project each element individually with dso-nav-right -->\r\n <ng-content select=\"[dso-nav-right]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: ["@charset \"UTF-8\";.dso-top-nav{display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 1rem;background-color:#fff;border:1px solid #e5e5e5}.dso-top-nav .nav-left{display:flex;align-items:center;gap:2rem}.dso-top-nav .nav-left .nav-left-group{display:flex;align-items:center}.dso-top-nav .nav-left .menu-logo{gap:.5rem}.dso-top-nav .nav-left .menu-buttons{gap:1rem}.dso-top-nav .nav-right{display:flex;align-items:center;gap:1rem;flex-direction:row;justify-content:flex-end}.dso-top-nav .nav-right dso-button{margin:0}.dso-top-nav .nav-right p{margin:0;font-weight:500}.top-nav-item{display:flex;align-items:center;gap:.5rem;padding:.25rem .5rem;background:none;border:none;cursor:pointer;font:inherit;color:inherit;text-decoration:none}.top-nav-item:hover{background-color:#f2f2f2;border-radius:4px}.top-nav-app-name{font-weight:700;font-size:1rem;margin-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
|
|
2168
|
+
}
|
|
2169
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TopNavComponent, decorators: [{
|
|
2170
|
+
type: Component,
|
|
2171
|
+
args: [{ selector: 'dso-top-nav', standalone: true, imports: [CommonModule, RouterModule, IconComponent], template: "<nav class=\"dso-top-nav\">\r\n\r\n <!-- Left section: menu+logo -->\r\n <div class=\"nav-left\">\r\n <div class=\"nav-left-group menu-logo\">\r\n <ng-content select=\"[dso-menu-logo]\"></ng-content>\r\n <span class=\"top-nav-app-name\" *ngIf=\"appName\">{{ appName }}</span>\r\n </div>\r\n\r\n <div class=\"nav-left-group menu-buttons\">\r\n <ng-container *ngFor=\"let item of menuItems\">\r\n <!-- Router link item -->\r\n <a \r\n *ngIf=\"item.route; else clickItem\" \r\n [routerLink]=\"item.route\" \r\n class=\"top-nav-item\"\r\n >\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </a>\r\n\r\n <!-- Click handler item -->\r\n <ng-template #clickItem>\r\n <button class=\"top-nav-item\" (click)=\"item.click?.()\">\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </button>\r\n </ng-template>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Right section -->\r\n <div class=\"nav-right\">\r\n <!-- \u2705 Project each element individually with dso-nav-right -->\r\n <ng-content select=\"[dso-nav-right]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: ["@charset \"UTF-8\";.dso-top-nav{display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 1rem;background-color:#fff;border:1px solid #e5e5e5}.dso-top-nav .nav-left{display:flex;align-items:center;gap:2rem}.dso-top-nav .nav-left .nav-left-group{display:flex;align-items:center}.dso-top-nav .nav-left .menu-logo{gap:.5rem}.dso-top-nav .nav-left .menu-buttons{gap:1rem}.dso-top-nav .nav-right{display:flex;align-items:center;gap:1rem;flex-direction:row;justify-content:flex-end}.dso-top-nav .nav-right dso-button{margin:0}.dso-top-nav .nav-right p{margin:0;font-weight:500}.top-nav-item{display:flex;align-items:center;gap:.5rem;padding:.25rem .5rem;background:none;border:none;cursor:pointer;font:inherit;color:inherit;text-decoration:none}.top-nav-item:hover{background-color:#f2f2f2;border-radius:4px}.top-nav-app-name{font-weight:700;font-size:1rem;margin-left:.5rem}\n"] }]
|
|
2172
|
+
}], propDecorators: { appName: [{
|
|
2173
|
+
type: Input
|
|
2174
|
+
}], menuItems: [{
|
|
2175
|
+
type: Input
|
|
2176
|
+
}] } });
|
|
2177
|
+
|
|
125
2178
|
/**
|
|
126
2179
|
* Generated bundle index. Do not edit.
|
|
127
2180
|
*/
|
|
128
2181
|
|
|
129
|
-
export { ButtonComponent, IconComponent };
|
|
2182
|
+
export { AlertComponent, BadgeComponent, BreadcrumbComponent, ButtonComponent, CheckboxComponent, DatepickerComponent, DialogComponent, DropdownListComponent, DsoTruncateDirective, FileUploadItemComponent, FileUploadMultiComponent, FileUploadSingleComponent, IconComponent, PaginationComponent, ProgressBarComponent, RadioComponent, SideNavComponent, SingleSelectComponent, SpinnerComponent, TableComponent, TabsComponent, TagComponent, TextAreaComponent, TextInputComponent, ToastComponent, TooltipComponent, TopNavComponent };
|
|
130
2183
|
//# sourceMappingURL=dso-design-system-ui.mjs.map
|