@shadng/sng-ui 1.0.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/LICENSE +21 -0
- package/README.md +75 -0
- package/cli/sng-ui.js +331 -0
- package/ng-package.json +29 -0
- package/package.json +64 -0
- package/registry.json +72 -0
- package/src/lib/accordion/cn.ts +6 -0
- package/src/lib/accordion/index.ts +18 -0
- package/src/lib/accordion/sng-accordion-content.ts +131 -0
- package/src/lib/accordion/sng-accordion-item.ts +299 -0
- package/src/lib/accordion/sng-accordion-trigger.ts +137 -0
- package/src/lib/accordion/sng-accordion.ts +118 -0
- package/src/lib/accordion/sng-accordion.types.ts +82 -0
- package/src/lib/alert/cn.ts +6 -0
- package/src/lib/alert/index.ts +3 -0
- package/src/lib/alert/sng-alert-description.ts +49 -0
- package/src/lib/alert/sng-alert-title.ts +46 -0
- package/src/lib/alert/sng-alert.ts +48 -0
- package/src/lib/avatar/cn.ts +6 -0
- package/src/lib/avatar/index.ts +3 -0
- package/src/lib/avatar/sng-avatar-fallback.ts +50 -0
- package/src/lib/avatar/sng-avatar-image.ts +73 -0
- package/src/lib/avatar/sng-avatar.ts +60 -0
- package/src/lib/badge/cn.ts +6 -0
- package/src/lib/badge/index.ts +1 -0
- package/src/lib/badge/sng-badge.ts +36 -0
- package/src/lib/breadcrumb/cn.ts +6 -0
- package/src/lib/breadcrumb/index.ts +7 -0
- package/src/lib/breadcrumb/sng-breadcrumb-ellipsis.ts +61 -0
- package/src/lib/breadcrumb/sng-breadcrumb-item.ts +47 -0
- package/src/lib/breadcrumb/sng-breadcrumb-link.ts +43 -0
- package/src/lib/breadcrumb/sng-breadcrumb-list.ts +42 -0
- package/src/lib/breadcrumb/sng-breadcrumb-page.ts +44 -0
- package/src/lib/breadcrumb/sng-breadcrumb-separator.ts +60 -0
- package/src/lib/breadcrumb/sng-breadcrumb.ts +52 -0
- package/src/lib/button/cn.ts +6 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button/sng-button.ts +264 -0
- package/src/lib/calendar/cn.ts +6 -0
- package/src/lib/calendar/index.ts +2 -0
- package/src/lib/calendar/sng-calendar.ts +753 -0
- package/src/lib/card/cn.ts +6 -0
- package/src/lib/card/index.ts +6 -0
- package/src/lib/card/sng-card-content.ts +36 -0
- package/src/lib/card/sng-card-description.ts +38 -0
- package/src/lib/card/sng-card-footer.ts +34 -0
- package/src/lib/card/sng-card-header.ts +34 -0
- package/src/lib/card/sng-card-title.ts +48 -0
- package/src/lib/card/sng-card.ts +43 -0
- package/src/lib/carousel/cn.ts +6 -0
- package/src/lib/carousel/index.ts +18 -0
- package/src/lib/carousel/sng-carousel.ts +526 -0
- package/src/lib/checkbox/cn.ts +6 -0
- package/src/lib/checkbox/index.ts +1 -0
- package/src/lib/checkbox/sng-checkbox.ts +154 -0
- package/src/lib/code-block/cn.ts +6 -0
- package/src/lib/code-block/index.ts +1 -0
- package/src/lib/code-block/sng-code-block.ts +296 -0
- package/src/lib/dialog/cn.ts +6 -0
- package/src/lib/dialog/index.ts +37 -0
- package/src/lib/dialog/sng-dialog-close.ts +76 -0
- package/src/lib/dialog/sng-dialog-content.ts +132 -0
- package/src/lib/dialog/sng-dialog-description.ts +36 -0
- package/src/lib/dialog/sng-dialog-footer.ts +39 -0
- package/src/lib/dialog/sng-dialog-header.ts +39 -0
- package/src/lib/dialog/sng-dialog-title.ts +52 -0
- package/src/lib/dialog/sng-dialog.service.ts +222 -0
- package/src/lib/dialog/sng-dialog.ts +224 -0
- package/src/lib/drawer/cn.ts +6 -0
- package/src/lib/drawer/index.ts +36 -0
- package/src/lib/drawer/sng-drawer-close.ts +28 -0
- package/src/lib/drawer/sng-drawer-content.ts +135 -0
- package/src/lib/drawer/sng-drawer-description.ts +29 -0
- package/src/lib/drawer/sng-drawer-footer.ts +34 -0
- package/src/lib/drawer/sng-drawer-handle.ts +30 -0
- package/src/lib/drawer/sng-drawer-header.ts +30 -0
- package/src/lib/drawer/sng-drawer-title.ts +27 -0
- package/src/lib/drawer/sng-drawer-trigger.ts +21 -0
- package/src/lib/drawer/sng-drawer-wrapper.ts +27 -0
- package/src/lib/drawer/sng-drawer.ts +166 -0
- package/src/lib/file-input/cn.ts +6 -0
- package/src/lib/file-input/index.ts +1 -0
- package/src/lib/file-input/sng-file-input.ts +288 -0
- package/src/lib/hover-card/cn.ts +6 -0
- package/src/lib/hover-card/index.ts +3 -0
- package/src/lib/hover-card/sng-hover-card-content.ts +100 -0
- package/src/lib/hover-card/sng-hover-card-trigger.ts +43 -0
- package/src/lib/hover-card/sng-hover-card.ts +246 -0
- package/src/lib/input/cn.ts +6 -0
- package/src/lib/input/index.ts +1 -0
- package/src/lib/input/sng-input.ts +160 -0
- package/src/lib/layout/cn.ts +6 -0
- package/src/lib/layout/index.ts +98 -0
- package/src/lib/layout/sng-layout-footer.ts +37 -0
- package/src/lib/layout/sng-layout-header.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-content.ts +149 -0
- package/src/lib/layout/sng-layout-sidebar-footer.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-group-action.ts +67 -0
- package/src/lib/layout/sng-layout-sidebar-group-content.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-group-label.ts +53 -0
- package/src/lib/layout/sng-layout-sidebar-group.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-header.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-input.ts +112 -0
- package/src/lib/layout/sng-layout-sidebar-inset.ts +45 -0
- package/src/lib/layout/sng-layout-sidebar-menu-action.ts +84 -0
- package/src/lib/layout/sng-layout-sidebar-menu-badge.ts +47 -0
- package/src/lib/layout/sng-layout-sidebar-menu-button.ts +160 -0
- package/src/lib/layout/sng-layout-sidebar-menu-item.ts +40 -0
- package/src/lib/layout/sng-layout-sidebar-menu-skeleton.ts +71 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-button.ts +142 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-item.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub.ts +48 -0
- package/src/lib/layout/sng-layout-sidebar-menu.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-provider.ts +189 -0
- package/src/lib/layout/sng-layout-sidebar-rail.ts +60 -0
- package/src/lib/layout/sng-layout-sidebar-separator.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-trigger.ts +97 -0
- package/src/lib/layout/sng-layout-sidebar.ts +254 -0
- package/src/lib/menu/cn.ts +6 -0
- package/src/lib/menu/index.ts +21 -0
- package/src/lib/menu/sng-context-trigger.ts +128 -0
- package/src/lib/menu/sng-menu-checkbox-item.ts +91 -0
- package/src/lib/menu/sng-menu-item.ts +80 -0
- package/src/lib/menu/sng-menu-label.ts +47 -0
- package/src/lib/menu/sng-menu-radio-group.ts +38 -0
- package/src/lib/menu/sng-menu-radio-item.ts +94 -0
- package/src/lib/menu/sng-menu-separator.ts +27 -0
- package/src/lib/menu/sng-menu-shortcut.ts +25 -0
- package/src/lib/menu/sng-menu-sub-content.ts +267 -0
- package/src/lib/menu/sng-menu-sub-trigger.ts +68 -0
- package/src/lib/menu/sng-menu-sub.ts +124 -0
- package/src/lib/menu/sng-menu-tokens.ts +52 -0
- package/src/lib/menu/sng-menu-trigger.ts +266 -0
- package/src/lib/menu/sng-menu.ts +100 -0
- package/src/lib/nav-menu/cn.ts +6 -0
- package/src/lib/nav-menu/index.ts +6 -0
- package/src/lib/nav-menu/sng-nav-menu-content.ts +72 -0
- package/src/lib/nav-menu/sng-nav-menu-item.ts +109 -0
- package/src/lib/nav-menu/sng-nav-menu-link.ts +54 -0
- package/src/lib/nav-menu/sng-nav-menu-list.ts +43 -0
- package/src/lib/nav-menu/sng-nav-menu-trigger.ts +98 -0
- package/src/lib/nav-menu/sng-nav-menu.ts +99 -0
- package/src/lib/otp-input/cn.ts +6 -0
- package/src/lib/otp-input/index.ts +14 -0
- package/src/lib/otp-input/sng-otp-input-group.ts +38 -0
- package/src/lib/otp-input/sng-otp-input-separator.ts +43 -0
- package/src/lib/otp-input/sng-otp-input-slot.ts +128 -0
- package/src/lib/otp-input/sng-otp-input-tokens.ts +20 -0
- package/src/lib/otp-input/sng-otp-input.ts +301 -0
- package/src/lib/popover/cn.ts +6 -0
- package/src/lib/popover/index.ts +3 -0
- package/src/lib/popover/sng-popover-content.ts +66 -0
- package/src/lib/popover/sng-popover-trigger.ts +44 -0
- package/src/lib/popover/sng-popover.ts +218 -0
- package/src/lib/preview-box/cn.ts +6 -0
- package/src/lib/preview-box/index.ts +5 -0
- package/src/lib/preview-box/sng-code-block.ts +80 -0
- package/src/lib/preview-box/sng-html-block.ts +79 -0
- package/src/lib/preview-box/sng-preview-block.ts +47 -0
- package/src/lib/preview-box/sng-preview-box.ts +369 -0
- package/src/lib/preview-box/sng-style-block.ts +80 -0
- package/src/lib/progress/cn.ts +6 -0
- package/src/lib/progress/index.ts +1 -0
- package/src/lib/progress/sng-progress.ts +65 -0
- package/src/lib/radio/cn.ts +6 -0
- package/src/lib/radio/index.ts +5 -0
- package/src/lib/radio/sng-radio-item.ts +100 -0
- package/src/lib/radio/sng-radio.ts +54 -0
- package/src/lib/resizable/cn.ts +6 -0
- package/src/lib/resizable/index.ts +3 -0
- package/src/lib/resizable/sng-resizable-group.ts +188 -0
- package/src/lib/resizable/sng-resizable-handle.ts +236 -0
- package/src/lib/resizable/sng-resizable-panel.ts +71 -0
- package/src/lib/search-input/cn.ts +6 -0
- package/src/lib/search-input/index.ts +16 -0
- package/src/lib/search-input/sng-search-input-context.ts +24 -0
- package/src/lib/search-input/sng-search-input-empty.ts +42 -0
- package/src/lib/search-input/sng-search-input-group.ts +69 -0
- package/src/lib/search-input/sng-search-input-item.ts +164 -0
- package/src/lib/search-input/sng-search-input-list.ts +34 -0
- package/src/lib/search-input/sng-search-input-separator.ts +32 -0
- package/src/lib/search-input/sng-search-input-shortcut.ts +29 -0
- package/src/lib/search-input/sng-search-input.ts +368 -0
- package/src/lib/select/cn.ts +6 -0
- package/src/lib/select/index.ts +7 -0
- package/src/lib/select/sng-select-content.ts +27 -0
- package/src/lib/select/sng-select-empty.ts +48 -0
- package/src/lib/select/sng-select-group.ts +29 -0
- package/src/lib/select/sng-select-item.ts +140 -0
- package/src/lib/select/sng-select-label.ts +29 -0
- package/src/lib/select/sng-select-separator.ts +29 -0
- package/src/lib/select/sng-select.ts +326 -0
- package/src/lib/separator/cn.ts +6 -0
- package/src/lib/separator/index.ts +1 -0
- package/src/lib/separator/sng-separator.ts +40 -0
- package/src/lib/skeleton/cn.ts +6 -0
- package/src/lib/skeleton/index.ts +1 -0
- package/src/lib/skeleton/sng-skeleton.ts +49 -0
- package/src/lib/slider/cn.ts +6 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/sng-slider.ts +137 -0
- package/src/lib/sng-table/cn.ts +6 -0
- package/src/lib/sng-table/flex-render.ts +222 -0
- package/src/lib/sng-table/index.ts +85 -0
- package/src/lib/sng-table/sng-table-body.ts +59 -0
- package/src/lib/sng-table/sng-table-caption.ts +49 -0
- package/src/lib/sng-table/sng-table-cell.ts +62 -0
- package/src/lib/sng-table/sng-table-footer.ts +60 -0
- package/src/lib/sng-table/sng-table-head.ts +66 -0
- package/src/lib/sng-table/sng-table-header.ts +48 -0
- package/src/lib/sng-table/sng-table-pagination.ts +265 -0
- package/src/lib/sng-table/sng-table-row.ts +65 -0
- package/src/lib/sng-table/sng-table.ts +67 -0
- package/src/lib/sng-table-core/core/create-cell.ts +117 -0
- package/src/lib/sng-table-core/core/create-column.ts +266 -0
- package/src/lib/sng-table-core/core/create-header.ts +271 -0
- package/src/lib/sng-table-core/core/create-row.ts +293 -0
- package/src/lib/sng-table-core/core/create-table.ts +534 -0
- package/src/lib/sng-table-core/core/types.ts +1197 -0
- package/src/lib/sng-table-core/core/utils.ts +307 -0
- package/src/lib/sng-table-core/features/column-filtering.ts +376 -0
- package/src/lib/sng-table-core/features/column-ordering.ts +159 -0
- package/src/lib/sng-table-core/features/column-pinning.ts +219 -0
- package/src/lib/sng-table-core/features/column-sizing.ts +268 -0
- package/src/lib/sng-table-core/features/column-visibility.ts +128 -0
- package/src/lib/sng-table-core/features/faceting.ts +279 -0
- package/src/lib/sng-table-core/features/fuzzy-filtering.ts +188 -0
- package/src/lib/sng-table-core/features/global-filtering.ts +128 -0
- package/src/lib/sng-table-core/features/pagination.ts +179 -0
- package/src/lib/sng-table-core/features/row-expanding.ts +181 -0
- package/src/lib/sng-table-core/features/row-grouping.ts +235 -0
- package/src/lib/sng-table-core/features/row-pinning.ts +196 -0
- package/src/lib/sng-table-core/features/row-selection.ts +298 -0
- package/src/lib/sng-table-core/features/sorting.ts +425 -0
- package/src/lib/sng-table-core/features/virtualization.ts +298 -0
- package/src/lib/sng-table-core/index.ts +235 -0
- package/src/lib/sng-table-core/row-models/core-row-model.ts +256 -0
- package/src/lib/sng-table-core/row-models/expanded-row-model.ts +175 -0
- package/src/lib/sng-table-core/row-models/filtered-row-model.ts +307 -0
- package/src/lib/sng-table-core/row-models/grouped-row-model.ts +290 -0
- package/src/lib/sng-table-core/row-models/paginated-row-model.ts +135 -0
- package/src/lib/sng-table-core/row-models/sorted-row-model.ts +197 -0
- package/src/lib/styles/sng-themes.css +164 -0
- package/src/lib/switch/cn.ts +6 -0
- package/src/lib/switch/index.ts +1 -0
- package/src/lib/switch/sng-switch.ts +137 -0
- package/src/lib/tabs/cn.ts +6 -0
- package/src/lib/tabs/index.ts +4 -0
- package/src/lib/tabs/sng-tabs-content.ts +66 -0
- package/src/lib/tabs/sng-tabs-list.ts +55 -0
- package/src/lib/tabs/sng-tabs-trigger.ts +86 -0
- package/src/lib/tabs/sng-tabs.ts +83 -0
- package/src/lib/toast/cn.ts +6 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/sng-toast.service.ts +258 -0
- package/src/lib/toast/sng-toast.ts +101 -0
- package/src/lib/toast/sng-toaster.ts +67 -0
- package/src/lib/toggle/cn.ts +6 -0
- package/src/lib/toggle/index.ts +6 -0
- package/src/lib/toggle/sng-toggle-group-item.ts +89 -0
- package/src/lib/toggle/sng-toggle-group.ts +85 -0
- package/src/lib/toggle/sng-toggle.ts +78 -0
- package/src/lib/toggle-group/index.ts +6 -0
- package/src/lib/tooltip/cn.ts +6 -0
- package/src/lib/tooltip/index.ts +5 -0
- package/src/lib/tooltip/sng-tooltip-content.ts +64 -0
- package/src/lib/tooltip/sng-tooltip.ts +216 -0
- package/src/public-api.ts +207 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +11 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
input,
|
|
6
|
+
model,
|
|
7
|
+
signal,
|
|
8
|
+
computed,
|
|
9
|
+
viewChild,
|
|
10
|
+
ElementRef,
|
|
11
|
+
booleanAttribute,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { cn } from './cn';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* File input component with button and dropzone modes.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```html
|
|
20
|
+
* <sng-file-input [(files)]="selectedFiles" />
|
|
21
|
+
* <sng-file-input dropzone multiple accept="image/*" />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'sng-file-input',
|
|
26
|
+
standalone: true,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
encapsulation: ViewEncapsulation.None,
|
|
29
|
+
host: {
|
|
30
|
+
'[class]': 'hostClasses()',
|
|
31
|
+
},
|
|
32
|
+
template: `
|
|
33
|
+
<input
|
|
34
|
+
#fileInputRef
|
|
35
|
+
type="file"
|
|
36
|
+
class="sr-only"
|
|
37
|
+
[accept]="accept()"
|
|
38
|
+
[multiple]="multiple()"
|
|
39
|
+
[disabled]="disabled()"
|
|
40
|
+
(change)="onFileChange($event)"
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
@if (dropzone()) {
|
|
44
|
+
<!-- Dropzone mode -->
|
|
45
|
+
<div
|
|
46
|
+
[class]="dropzoneClasses()"
|
|
47
|
+
tabindex="0"
|
|
48
|
+
role="button"
|
|
49
|
+
[attr.aria-label]="'Upload files' + (accept() ? ', accepts: ' + accept() : '')"
|
|
50
|
+
(click)="openFilePicker()"
|
|
51
|
+
(dragover)="onDragOver($event)"
|
|
52
|
+
(dragleave)="onDragLeave($event)"
|
|
53
|
+
(drop)="onDrop($event)"
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
class="h-10 w-10 text-muted-foreground"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
stroke-width="1.5"
|
|
61
|
+
stroke-linecap="round"
|
|
62
|
+
stroke-linejoin="round"
|
|
63
|
+
>
|
|
64
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
65
|
+
<polyline points="17 8 12 3 7 8" />
|
|
66
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
67
|
+
</svg>
|
|
68
|
+
<div class="text-center">
|
|
69
|
+
<p class="text-sm font-medium text-foreground">
|
|
70
|
+
Drag & drop files here
|
|
71
|
+
</p>
|
|
72
|
+
<p class="text-xs text-muted-foreground">
|
|
73
|
+
or click to browse
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
@if (accept()) {
|
|
77
|
+
<p class="text-xs text-muted-foreground">
|
|
78
|
+
Accepted: {{ accept() }}
|
|
79
|
+
</p>
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
} @else {
|
|
83
|
+
<!-- Button mode -->
|
|
84
|
+
<div [class]="buttonModeClasses()">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class="h-full rounded-l-md border-r border-border bg-muted px-3 text-sm font-medium text-foreground transition-colors hover:bg-muted/80"
|
|
88
|
+
[disabled]="disabled()"
|
|
89
|
+
(click)="openFilePicker()"
|
|
90
|
+
>
|
|
91
|
+
Choose File
|
|
92
|
+
</button>
|
|
93
|
+
<span class="flex-1 truncate px-3 text-muted-foreground">
|
|
94
|
+
{{ fileNameDisplay() }}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
<!-- Selected files list -->
|
|
100
|
+
@if (files().length > 0 && showFileList()) {
|
|
101
|
+
<div class="mt-2 space-y-1">
|
|
102
|
+
@for (file of files(); track file.name) {
|
|
103
|
+
<div
|
|
104
|
+
class="flex items-center justify-between rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm"
|
|
105
|
+
>
|
|
106
|
+
<span class="truncate text-foreground">{{ file.name }}</span>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="ml-2 text-muted-foreground transition-colors hover:text-foreground"
|
|
110
|
+
[disabled]="disabled()"
|
|
111
|
+
(click)="removeFile(file)"
|
|
112
|
+
>
|
|
113
|
+
<svg
|
|
114
|
+
class="h-4 w-4"
|
|
115
|
+
viewBox="0 0 24 24"
|
|
116
|
+
fill="none"
|
|
117
|
+
stroke="currentColor"
|
|
118
|
+
stroke-width="2"
|
|
119
|
+
stroke-linecap="round"
|
|
120
|
+
stroke-linejoin="round"
|
|
121
|
+
>
|
|
122
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
123
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
}
|
|
128
|
+
</div>
|
|
129
|
+
}
|
|
130
|
+
`,
|
|
131
|
+
})
|
|
132
|
+
export class SngFileInput {
|
|
133
|
+
private fileInputRef = viewChild<ElementRef<HTMLInputElement>>('fileInputRef');
|
|
134
|
+
|
|
135
|
+
/** Whether the input is disabled. */
|
|
136
|
+
disabled = input(false, { transform: booleanAttribute });
|
|
137
|
+
|
|
138
|
+
/** Whether to show dropzone mode. */
|
|
139
|
+
dropzone = input(false, { transform: booleanAttribute });
|
|
140
|
+
|
|
141
|
+
/** Whether multiple files can be selected. */
|
|
142
|
+
multiple = input(false, { transform: booleanAttribute });
|
|
143
|
+
|
|
144
|
+
/** Accepted file types (e.g., '.jpg,.png' or 'image/*'). */
|
|
145
|
+
accept = input<string>('');
|
|
146
|
+
|
|
147
|
+
/** Whether to show the file list below the input. */
|
|
148
|
+
showFileList = input(true, { transform: booleanAttribute });
|
|
149
|
+
|
|
150
|
+
/** Custom CSS classes. */
|
|
151
|
+
class = input<string>('');
|
|
152
|
+
|
|
153
|
+
/** Selected files. Supports two-way binding via [(files)]. */
|
|
154
|
+
files = model<File[]>([]);
|
|
155
|
+
|
|
156
|
+
/** @internal */
|
|
157
|
+
_isDragging = signal(false);
|
|
158
|
+
|
|
159
|
+
/** @internal */
|
|
160
|
+
hostClasses = computed(() => cn('block w-full', this.class()));
|
|
161
|
+
|
|
162
|
+
/** @internal */
|
|
163
|
+
dropzoneClasses = computed(() =>
|
|
164
|
+
cn(
|
|
165
|
+
'flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 transition-colors',
|
|
166
|
+
this._isDragging() ? 'border-primary bg-muted/50' : 'border-muted-foreground/25',
|
|
167
|
+
this.disabled() ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
/** @internal */
|
|
172
|
+
buttonModeClasses = computed(() =>
|
|
173
|
+
cn(
|
|
174
|
+
'flex h-9 items-center rounded-md border border-border bg-background text-sm shadow-sm transition-colors',
|
|
175
|
+
this.disabled() ? 'cursor-not-allowed opacity-50' : ''
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
/** @internal */
|
|
180
|
+
fileNameDisplay = computed(() => {
|
|
181
|
+
const fileList = this.files();
|
|
182
|
+
if (fileList.length === 0) return 'No file chosen';
|
|
183
|
+
if (fileList.length === 1) return fileList[0].name;
|
|
184
|
+
return `${fileList.length} files selected`;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/** Open the native file picker dialog. */
|
|
188
|
+
openFilePicker(): void {
|
|
189
|
+
if (this.disabled()) return;
|
|
190
|
+
this.fileInputRef()?.nativeElement.click();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @internal */
|
|
194
|
+
onFileChange(event: Event): void {
|
|
195
|
+
const input = event.target as HTMLInputElement;
|
|
196
|
+
if (input.files) {
|
|
197
|
+
const newFiles = Array.from(input.files);
|
|
198
|
+
this.files.set(newFiles);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @internal */
|
|
203
|
+
onDragOver(event: DragEvent): void {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
event.stopPropagation();
|
|
206
|
+
if (!this.disabled()) {
|
|
207
|
+
this._isDragging.set(true);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** @internal */
|
|
212
|
+
onDragLeave(event: DragEvent): void {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
event.stopPropagation();
|
|
215
|
+
this._isDragging.set(false);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @internal */
|
|
219
|
+
onDrop(event: DragEvent): void {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
event.stopPropagation();
|
|
222
|
+
this._isDragging.set(false);
|
|
223
|
+
|
|
224
|
+
if (this.disabled()) return;
|
|
225
|
+
|
|
226
|
+
const droppedFiles = event.dataTransfer?.files;
|
|
227
|
+
if (droppedFiles && droppedFiles.length > 0) {
|
|
228
|
+
let newFiles = Array.from(droppedFiles);
|
|
229
|
+
|
|
230
|
+
// Filter by accept if specified
|
|
231
|
+
const acceptAttr = this.accept();
|
|
232
|
+
if (acceptAttr) {
|
|
233
|
+
newFiles = this.filterFilesByAccept(newFiles, acceptAttr);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle multiple vs single
|
|
237
|
+
if (!this.multiple() && newFiles.length > 1) {
|
|
238
|
+
newFiles = [newFiles[0]];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.files.set(newFiles);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Remove a specific file from the selection. */
|
|
246
|
+
removeFile(fileToRemove: File): void {
|
|
247
|
+
if (this.disabled()) return;
|
|
248
|
+
const updatedFiles = this.files().filter(f => f !== fileToRemove);
|
|
249
|
+
this.files.set(updatedFiles);
|
|
250
|
+
|
|
251
|
+
// Clear the native input
|
|
252
|
+
const inputEl = this.fileInputRef()?.nativeElement;
|
|
253
|
+
if (inputEl) {
|
|
254
|
+
inputEl.value = '';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Clear all selected files. */
|
|
259
|
+
clearFiles(): void {
|
|
260
|
+
this.files.set([]);
|
|
261
|
+
const inputEl = this.fileInputRef()?.nativeElement;
|
|
262
|
+
if (inputEl) {
|
|
263
|
+
inputEl.value = '';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private filterFilesByAccept(files: File[], accept: string): File[] {
|
|
268
|
+
const acceptTypes = accept.split(',').map(t => t.trim().toLowerCase());
|
|
269
|
+
return files.filter(file => {
|
|
270
|
+
const fileName = file.name.toLowerCase();
|
|
271
|
+
const fileType = file.type.toLowerCase();
|
|
272
|
+
|
|
273
|
+
return acceptTypes.some(acceptType => {
|
|
274
|
+
if (acceptType.startsWith('.')) {
|
|
275
|
+
// Extension match (e.g., .jpg, .png)
|
|
276
|
+
return fileName.endsWith(acceptType);
|
|
277
|
+
} else if (acceptType.endsWith('/*')) {
|
|
278
|
+
// MIME type wildcard (e.g., image/*)
|
|
279
|
+
const category = acceptType.slice(0, -2);
|
|
280
|
+
return fileType.startsWith(category);
|
|
281
|
+
} else {
|
|
282
|
+
// Exact MIME type match
|
|
283
|
+
return fileType === acceptType;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
TemplateRef,
|
|
6
|
+
ViewContainerRef,
|
|
7
|
+
inject,
|
|
8
|
+
viewChild,
|
|
9
|
+
input,
|
|
10
|
+
computed,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import type { SngHoverCard } from './sng-hover-card';
|
|
13
|
+
import { cn } from './cn';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component that renders the hover card content panel.
|
|
17
|
+
* Must be used inside a SngHoverCard container.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <sng-hover-card>
|
|
22
|
+
* <sng-hover-card-trigger href="#">@user</sng-hover-card-trigger>
|
|
23
|
+
* <sng-hover-card-content class="w-80">
|
|
24
|
+
* <div class="flex gap-4">
|
|
25
|
+
* <img src="avatar.jpg" alt="Avatar" />
|
|
26
|
+
* <div>
|
|
27
|
+
* <h4>User Name</h4>
|
|
28
|
+
* <p>User bio here</p>
|
|
29
|
+
* </div>
|
|
30
|
+
* </div>
|
|
31
|
+
* </sng-hover-card-content>
|
|
32
|
+
* </sng-hover-card>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
@Component({
|
|
36
|
+
selector: 'sng-hover-card-content',
|
|
37
|
+
standalone: true,
|
|
38
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
39
|
+
encapsulation: ViewEncapsulation.None,
|
|
40
|
+
styles: [`
|
|
41
|
+
.sng-hover-card-content[data-state=open][data-side=bottom] { animation: sng-hover-card-enter-bottom 150ms ease both; }
|
|
42
|
+
.sng-hover-card-content[data-state=open][data-side=top] { animation: sng-hover-card-enter-top 150ms ease both; }
|
|
43
|
+
.sng-hover-card-content[data-state=open][data-side=left] { animation: sng-hover-card-enter-left 150ms ease both; }
|
|
44
|
+
.sng-hover-card-content[data-state=open][data-side=right] { animation: sng-hover-card-enter-right 150ms ease both; }
|
|
45
|
+
.sng-hover-card-content[data-state=closed] { animation: sng-hover-card-exit 150ms ease both; }
|
|
46
|
+
@keyframes sng-hover-card-enter-bottom { from { opacity: 0; transform: scale(0.95) translateY(-0.5rem); } }
|
|
47
|
+
@keyframes sng-hover-card-enter-top { from { opacity: 0; transform: scale(0.95) translateY(0.5rem); } }
|
|
48
|
+
@keyframes sng-hover-card-enter-left { from { opacity: 0; transform: scale(0.95) translateX(0.5rem); } }
|
|
49
|
+
@keyframes sng-hover-card-enter-right { from { opacity: 0; transform: scale(0.95) translateX(-0.5rem); } }
|
|
50
|
+
@keyframes sng-hover-card-exit { to { opacity: 0; transform: scale(0.95); } }
|
|
51
|
+
`],
|
|
52
|
+
host: {},
|
|
53
|
+
template: `
|
|
54
|
+
<ng-template #content>
|
|
55
|
+
<div
|
|
56
|
+
class="sng-hover-card-content"
|
|
57
|
+
[class]="hostClasses()"
|
|
58
|
+
data-state="open"
|
|
59
|
+
[attr.data-side]="dataSide()"
|
|
60
|
+
(mouseenter)="onMouseEnter()"
|
|
61
|
+
(mouseleave)="onMouseLeave()"
|
|
62
|
+
>
|
|
63
|
+
<ng-content />
|
|
64
|
+
</div>
|
|
65
|
+
</ng-template>
|
|
66
|
+
`,
|
|
67
|
+
})
|
|
68
|
+
export class SngHoverCardContent {
|
|
69
|
+
/**
|
|
70
|
+
* Custom CSS classes for the hover card content panel.
|
|
71
|
+
*/
|
|
72
|
+
class = input<string>('');
|
|
73
|
+
|
|
74
|
+
// Assigned by parent
|
|
75
|
+
hoverCard: SngHoverCard | null = null;
|
|
76
|
+
|
|
77
|
+
private contentTemplate = viewChild.required<TemplateRef<unknown>>('content');
|
|
78
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
79
|
+
|
|
80
|
+
dataSide = computed(() => this.hoverCard?.side() ?? 'bottom');
|
|
81
|
+
|
|
82
|
+
hostClasses = computed(() =>
|
|
83
|
+
cn(
|
|
84
|
+
'z-50 w-64 rounded-md border border-border bg-popover text-popover-foreground p-4 shadow-md outline-none',
|
|
85
|
+
this.class()
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
get templateRef(): TemplateRef<unknown> {
|
|
90
|
+
return this.contentTemplate();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMouseEnter() {
|
|
94
|
+
this.hoverCard?.onContentEnter();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onMouseLeave() {
|
|
98
|
+
this.hoverCard?.onContentLeave();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
inject,
|
|
5
|
+
} from '@angular/core';
|
|
6
|
+
import type { SngHoverCard } from './sng-hover-card';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Directive that marks an element as the trigger for a hover card.
|
|
10
|
+
* Opens the hover card on mouse enter and closes on mouse leave.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```html
|
|
14
|
+
* <sng-hover-card>
|
|
15
|
+
* <sng-hover-card-trigger href="#">Hover over me</sng-hover-card-trigger>
|
|
16
|
+
* <sng-hover-card-content>Content</sng-hover-card-content>
|
|
17
|
+
* </sng-hover-card>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
@Directive({
|
|
21
|
+
selector: 'sng-hover-card-trigger',
|
|
22
|
+
standalone: true,
|
|
23
|
+
host: {
|
|
24
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
25
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
export class SngHoverCardTrigger {
|
|
29
|
+
private elementRef = inject(ElementRef);
|
|
30
|
+
private hoverCard: SngHoverCard | null = null;
|
|
31
|
+
|
|
32
|
+
registerHoverCard(hoverCard: SngHoverCard) {
|
|
33
|
+
this.hoverCard = hoverCard;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onMouseEnter() {
|
|
37
|
+
this.hoverCard?.onTriggerEnter(this.elementRef);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMouseLeave() {
|
|
41
|
+
this.hoverCard?.onTriggerLeave();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
input,
|
|
6
|
+
computed,
|
|
7
|
+
contentChild,
|
|
8
|
+
ElementRef,
|
|
9
|
+
inject,
|
|
10
|
+
OnDestroy,
|
|
11
|
+
AfterContentInit,
|
|
12
|
+
signal,
|
|
13
|
+
WritableSignal,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { Overlay, OverlayPositionBuilder, OverlayRef, ConnectedPosition } from '@angular/cdk/overlay';
|
|
16
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
17
|
+
import { Subscription, timer } from 'rxjs';
|
|
18
|
+
import { SngHoverCardTrigger } from './sng-hover-card-trigger';
|
|
19
|
+
import { SngHoverCardContent } from './sng-hover-card-content';
|
|
20
|
+
import { cn } from './cn';
|
|
21
|
+
|
|
22
|
+
type OverlaySide = 'top' | 'bottom' | 'left' | 'right';
|
|
23
|
+
|
|
24
|
+
function getOverlayPosition(side: OverlaySide, offset = 4): ConnectedPosition {
|
|
25
|
+
const positions: Record<OverlaySide, ConnectedPosition> = {
|
|
26
|
+
top: { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -offset },
|
|
27
|
+
bottom: { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: offset },
|
|
28
|
+
left: { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -offset },
|
|
29
|
+
right: { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: offset },
|
|
30
|
+
};
|
|
31
|
+
return positions[side];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class OverlayLifecycle {
|
|
35
|
+
private _overlayRef: OverlayRef | null = null;
|
|
36
|
+
private _subscriptions = new Subscription();
|
|
37
|
+
private _closing = false;
|
|
38
|
+
readonly isOpen: WritableSignal<boolean> = signal(false);
|
|
39
|
+
|
|
40
|
+
attach(overlayRef: OverlayRef): void {
|
|
41
|
+
this._overlayRef = overlayRef;
|
|
42
|
+
this.isOpen.set(true);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addSubscription(subscription: Subscription): void { this._subscriptions.add(subscription); }
|
|
46
|
+
hasOverlay(): boolean { return this._overlayRef !== null; }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Closes the overlay with a CSS exit animation, then disposes.
|
|
50
|
+
* Sets data-state="closed" on overlay elements and waits for all CSS
|
|
51
|
+
* animations to finish (via Web Animations API) before removing the overlay.
|
|
52
|
+
*/
|
|
53
|
+
close(): void {
|
|
54
|
+
if (!this._overlayRef || this._closing) return;
|
|
55
|
+
this._closing = true;
|
|
56
|
+
this.isOpen.set(false);
|
|
57
|
+
|
|
58
|
+
const panel = this._overlayRef.overlayElement;
|
|
59
|
+
panel.querySelectorAll('[data-state]').forEach(el =>
|
|
60
|
+
el.setAttribute('data-state', 'closed')
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const animations = panel.getAnimations({ subtree: true });
|
|
64
|
+
if (animations.length > 0) {
|
|
65
|
+
Promise.allSettled(animations.map(animation => animation.finished)).finally(() => this.dispose());
|
|
66
|
+
} else {
|
|
67
|
+
this.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
dispose(): void {
|
|
72
|
+
this._closing = false;
|
|
73
|
+
this._subscriptions.unsubscribe();
|
|
74
|
+
this._subscriptions = new Subscription();
|
|
75
|
+
if (this._overlayRef) { this._overlayRef.detach(); this._overlayRef.dispose(); this._overlayRef = null; }
|
|
76
|
+
this.isOpen.set(false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
destroy(): void { this.dispose(); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type HoverCardSide = OverlaySide;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Container component for hover card functionality.
|
|
86
|
+
* Displays a content panel on hover with configurable delays.
|
|
87
|
+
* Uses CDK Overlay for positioning.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```html
|
|
91
|
+
* <sng-hover-card [openDelay]="300" [closeDelay]="200">
|
|
92
|
+
* <sng-hover-card-trigger href="#">@username</sng-hover-card-trigger>
|
|
93
|
+
* <sng-hover-card-content>
|
|
94
|
+
* <p>User profile preview</p>
|
|
95
|
+
* </sng-hover-card-content>
|
|
96
|
+
* </sng-hover-card>
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
@Component({
|
|
100
|
+
selector: 'sng-hover-card',
|
|
101
|
+
standalone: true,
|
|
102
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
103
|
+
encapsulation: ViewEncapsulation.None,
|
|
104
|
+
host: {
|
|
105
|
+
'[class]': 'hostClasses()',
|
|
106
|
+
},
|
|
107
|
+
template: `<ng-content />`,
|
|
108
|
+
})
|
|
109
|
+
export class SngHoverCard implements AfterContentInit, OnDestroy {
|
|
110
|
+
/**
|
|
111
|
+
* Custom CSS classes for the hover card container.
|
|
112
|
+
*/
|
|
113
|
+
class = input<string>('');
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Position of the hover card relative to the trigger element.
|
|
117
|
+
*/
|
|
118
|
+
side = input<HoverCardSide>('bottom');
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Delay in milliseconds before opening the hover card.
|
|
122
|
+
*/
|
|
123
|
+
openDelay = input<number>(200);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Delay in milliseconds before closing the hover card.
|
|
127
|
+
*/
|
|
128
|
+
closeDelay = input<number>(300);
|
|
129
|
+
|
|
130
|
+
private overlay = inject(Overlay);
|
|
131
|
+
private overlayPositionBuilder = inject(OverlayPositionBuilder);
|
|
132
|
+
private lifecycle = new OverlayLifecycle();
|
|
133
|
+
private openTimerSubscription: Subscription | null = null;
|
|
134
|
+
private closeTimerSubscription: Subscription | null = null;
|
|
135
|
+
|
|
136
|
+
trigger = contentChild(SngHoverCardTrigger);
|
|
137
|
+
content = contentChild(SngHoverCardContent);
|
|
138
|
+
|
|
139
|
+
isOpen = this.lifecycle.isOpen;
|
|
140
|
+
|
|
141
|
+
hostClasses = computed(() => cn('inline-block', this.class()));
|
|
142
|
+
|
|
143
|
+
ngAfterContentInit() {
|
|
144
|
+
const triggerDirective = this.trigger();
|
|
145
|
+
if (triggerDirective) {
|
|
146
|
+
triggerDirective.registerHoverCard(this);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onTriggerEnter(triggerElement: ElementRef) {
|
|
151
|
+
this.clearCloseTimeout();
|
|
152
|
+
if (this.isOpen()) return;
|
|
153
|
+
const delay = Math.max(0, this.openDelay());
|
|
154
|
+
if (delay === 0) {
|
|
155
|
+
this.show(triggerElement);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.openTimerSubscription = timer(delay).subscribe(() => {
|
|
159
|
+
this.openTimerSubscription = null;
|
|
160
|
+
this.show(triggerElement);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onTriggerLeave() {
|
|
165
|
+
this.clearOpenTimeout();
|
|
166
|
+
this.scheduleClose();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
onContentEnter() {
|
|
170
|
+
this.clearCloseTimeout();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
onContentLeave() {
|
|
174
|
+
this.scheduleClose();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private scheduleClose() {
|
|
178
|
+
const delay = Math.max(0, this.closeDelay());
|
|
179
|
+
if (delay === 0) {
|
|
180
|
+
this.hide();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.closeTimerSubscription = timer(delay).subscribe(() => {
|
|
184
|
+
this.closeTimerSubscription = null;
|
|
185
|
+
this.hide();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private clearOpenTimeout() {
|
|
190
|
+
this.openTimerSubscription?.unsubscribe();
|
|
191
|
+
this.openTimerSubscription = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private clearCloseTimeout() {
|
|
195
|
+
this.closeTimerSubscription?.unsubscribe();
|
|
196
|
+
this.closeTimerSubscription = null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private show(triggerElement: ElementRef) {
|
|
200
|
+
if (this.lifecycle.hasOverlay()) return;
|
|
201
|
+
|
|
202
|
+
const primary = this.side();
|
|
203
|
+
const fallbackMap: Record<OverlaySide, OverlaySide> = {
|
|
204
|
+
bottom: 'top', top: 'bottom', left: 'right', right: 'left',
|
|
205
|
+
};
|
|
206
|
+
const positionStrategy = this.overlayPositionBuilder
|
|
207
|
+
.flexibleConnectedTo(triggerElement)
|
|
208
|
+
.withPositions([
|
|
209
|
+
getOverlayPosition(primary),
|
|
210
|
+
getOverlayPosition(fallbackMap[primary]),
|
|
211
|
+
])
|
|
212
|
+
.withPush(true)
|
|
213
|
+
.withViewportMargin(8);
|
|
214
|
+
|
|
215
|
+
const overlayRef = this.overlay.create({
|
|
216
|
+
positionStrategy,
|
|
217
|
+
scrollStrategy: this.overlay.scrollStrategies.close(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const contentComponent = this.content();
|
|
221
|
+
if (contentComponent) {
|
|
222
|
+
contentComponent.hoverCard = this;
|
|
223
|
+
const portal = new TemplatePortal(
|
|
224
|
+
contentComponent.templateRef,
|
|
225
|
+
contentComponent.viewContainerRef
|
|
226
|
+
);
|
|
227
|
+
overlayRef.attach(portal);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.lifecycle.attach(overlayRef);
|
|
231
|
+
this.lifecycle.addSubscription(
|
|
232
|
+
overlayRef.detachments().subscribe(() => this.lifecycle.dispose())
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
hide() {
|
|
237
|
+
if (!this.lifecycle.hasOverlay()) return;
|
|
238
|
+
this.lifecycle.close();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ngOnDestroy() {
|
|
242
|
+
this.clearOpenTimeout();
|
|
243
|
+
this.clearCloseTimeout();
|
|
244
|
+
this.lifecycle.destroy();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SngInput, type SngInputType } from './sng-input';
|