@haloduck/ui 2.0.24 → 2.0.25
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/fesm2022/haloduck-ui.mjs +45 -34
- package/fesm2022/haloduck-ui.mjs.map +1 -1
- package/index.d.ts +4 -2
- package/package.json +1 -1
package/fesm2022/haloduck-ui.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Input, Component, inject, ChangeDetectorRef, forwardRef, ViewChild, signal, EventEmitter, Output, ViewContainerRef, NgZone, isDevMode, DestroyRef, ElementRef, ChangeDetectionStrategy, HostBinding, Directive } from '@angular/core';
|
|
2
|
+
import { Injectable, Input, Component, inject, ChangeDetectorRef, forwardRef, ViewChild, signal, EventEmitter, Output, ViewContainerRef, NgZone, isDevMode, DestroyRef, ElementRef, ViewChildren, ChangeDetectionStrategy, HostBinding, Directive } from '@angular/core';
|
|
3
3
|
import { signIn, confirmSignIn, resetPassword, confirmResetPassword } from 'aws-amplify/auth';
|
|
4
4
|
import * as i1$1 from '@angular/forms';
|
|
5
5
|
import { NG_VALUE_ACCESSOR, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
6
|
-
import { BehaviorSubject, zip, Subject, takeUntil, tap, combineLatest, switchMap, of, Observable, distinctUntilChanged, map, take } from 'rxjs';
|
|
6
|
+
import { BehaviorSubject, zip, Subject, takeUntil, tap, combineLatest, switchMap, of, Observable, distinctUntilChanged, map, take, firstValueFrom } from 'rxjs';
|
|
7
7
|
import { ulid } from 'ulid';
|
|
8
8
|
import * as i1 from '@angular/common';
|
|
9
9
|
import { CommonModule, DecimalPipe, DatePipe } from '@angular/common';
|
|
@@ -2908,9 +2908,11 @@ class TableComponent {
|
|
|
2908
2908
|
onRowClick = new EventEmitter();
|
|
2909
2909
|
onRowDblClick = new EventEmitter();
|
|
2910
2910
|
loadMoreRow;
|
|
2911
|
-
|
|
2911
|
+
dataRows;
|
|
2912
2912
|
scrollContainer;
|
|
2913
2913
|
intersectionObserver;
|
|
2914
|
+
scrollListenerTarget;
|
|
2915
|
+
didScrollSinceLastLoad = false;
|
|
2914
2916
|
getColumnValue(row, column) {
|
|
2915
2917
|
const value = this.getColumnValueRaw(row, column);
|
|
2916
2918
|
if (column.customRenderFn) {
|
|
@@ -3004,11 +3006,19 @@ class TableComponent {
|
|
|
3004
3006
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
3005
3007
|
.subscribe(() => {
|
|
3006
3008
|
this.setupIntersectionObserver();
|
|
3007
|
-
this.checkAndAutoLoadIfNoScroll();
|
|
3008
3009
|
});
|
|
3010
|
+
const container = this.scrollContainer?.nativeElement;
|
|
3011
|
+
this.scrollListenerTarget = container || (typeof window !== 'undefined' ? window : undefined);
|
|
3012
|
+
if (this.scrollListenerTarget) {
|
|
3013
|
+
this.scrollListenerTarget.addEventListener('scroll', this.onScroll, { passive: true });
|
|
3014
|
+
}
|
|
3009
3015
|
}
|
|
3010
3016
|
ngOnDestroy() {
|
|
3011
3017
|
this.cleanupIntersectionObserver();
|
|
3018
|
+
if (this.scrollListenerTarget) {
|
|
3019
|
+
this.scrollListenerTarget.removeEventListener('scroll', this.onScroll);
|
|
3020
|
+
this.scrollListenerTarget = undefined;
|
|
3021
|
+
}
|
|
3012
3022
|
}
|
|
3013
3023
|
setupIntersectionObserver() {
|
|
3014
3024
|
if (!this.useLoadMore)
|
|
@@ -3016,9 +3026,14 @@ class TableComponent {
|
|
|
3016
3026
|
this.cleanupIntersectionObserver();
|
|
3017
3027
|
const rootElement = this.scrollContainer?.nativeElement || null;
|
|
3018
3028
|
// 우선순위: 마지막 데이터 행 -> 로드모어 행
|
|
3019
|
-
const
|
|
3029
|
+
const lastRowEl = this.dataRows?.last?.nativeElement;
|
|
3030
|
+
const target = lastRowEl || this.loadMoreRow?.nativeElement;
|
|
3020
3031
|
if (!target)
|
|
3021
3032
|
return;
|
|
3033
|
+
// 새로운 옵저버 생성 시 초기 교차 무시 플래그 초기화
|
|
3034
|
+
this.hasObservedOnce = false;
|
|
3035
|
+
// 스크롤 발생 전에는 자동 로드를 막음
|
|
3036
|
+
this.didScrollSinceLastLoad = false;
|
|
3022
3037
|
this.ngZone.runOutsideAngular(() => {
|
|
3023
3038
|
this.intersectionObserver = new IntersectionObserver(this.onIntersection, {
|
|
3024
3039
|
root: rootElement || undefined,
|
|
@@ -3037,39 +3052,35 @@ class TableComponent {
|
|
|
3037
3052
|
hasObservedOnce = false;
|
|
3038
3053
|
onIntersection = (entries) => {
|
|
3039
3054
|
for (const entry of entries) {
|
|
3040
|
-
if (entry.isIntersecting)
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3055
|
+
if (!entry.isIntersecting)
|
|
3056
|
+
continue;
|
|
3057
|
+
if (!this.hasObservedOnce) {
|
|
3058
|
+
// 첫 교차는 무시: 새 옵저버가 생성될 때 현재 화면에 이미 보이는 경우를 방지
|
|
3059
|
+
this.hasObservedOnce = true;
|
|
3060
|
+
continue;
|
|
3061
|
+
}
|
|
3062
|
+
// 마지막 키가 존재, 로딩 아님, 그리고 실제 스크롤이 있었을 때만 로드
|
|
3063
|
+
Promise.all([
|
|
3064
|
+
firstValueFrom(this.lastEvaluatedKey.pipe(take(1))),
|
|
3065
|
+
firstValueFrom(this.isLoading.pipe(take(1))),
|
|
3066
|
+
]).then(([lastKey, isLoading]) => {
|
|
3067
|
+
if (!(lastKey && !isLoading && this.didScrollSinceLastLoad)) {
|
|
3068
|
+
return;
|
|
3045
3069
|
}
|
|
3070
|
+
// 중복 호출 방지: 즉시 관찰 해제 후 로드 트리거
|
|
3071
|
+
this.cleanupIntersectionObserver();
|
|
3072
|
+
this.didScrollSinceLastLoad = false;
|
|
3046
3073
|
this.ngZone.run(() => this.onLoadMoreClicked());
|
|
3047
|
-
}
|
|
3074
|
+
});
|
|
3048
3075
|
}
|
|
3049
3076
|
};
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
if (!container)
|
|
3055
|
-
return;
|
|
3056
|
-
this.ngZone.runOutsideAngular(() => {
|
|
3057
|
-
requestAnimationFrame(() => {
|
|
3058
|
-
const hasNoScroll = container.scrollHeight <= container.clientHeight;
|
|
3059
|
-
if (!hasNoScroll)
|
|
3060
|
-
return;
|
|
3061
|
-
combineLatest([this.lastEvaluatedKey.pipe(take(1)), this.isLoading.pipe(take(1))])
|
|
3062
|
-
.subscribe(([lastKey, isLoading]) => {
|
|
3063
|
-
if (lastKey && !isLoading) {
|
|
3064
|
-
this.ngZone.run(() => this.onLoadMoreClicked());
|
|
3065
|
-
}
|
|
3066
|
-
});
|
|
3067
|
-
});
|
|
3068
|
-
});
|
|
3069
|
-
}
|
|
3077
|
+
onScroll = () => {
|
|
3078
|
+
this.didScrollSinceLastLoad = true;
|
|
3079
|
+
};
|
|
3080
|
+
// Note: 무스크롤 자동 로드는 연속 로딩을 유발할 수 있어 제거
|
|
3070
3081
|
constructor() { }
|
|
3071
3082
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3072
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TableComponent, isStandalone: true, selector: "haloduck-table", inputs: { tableLayout: "tableLayout", showHeader: "showHeader", useLoadMore: "useLoadMore", columns: "columns", rows: "rows", isLoading: "isLoading", isPaging: "isPaging", sort: "sort", expandedTemplate: "expandedTemplate", customTemplates: "customTemplates", lastEvaluatedKey: "lastEvaluatedKey" }, outputs: { onSortChange: "onSortChange", onLoadMore: "onLoadMore", onRowClick: "onRowClick", onRowDblClick: "onRowDblClick" }, providers: [provideTranslocoScope('haloduck')], viewQueries: [{ propertyName: "loadMoreRow", first: true, predicate: ["loadMoreRow"], descendants: true }, { propertyName: "
|
|
3083
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: TableComponent, isStandalone: true, selector: "haloduck-table", inputs: { tableLayout: "tableLayout", showHeader: "showHeader", useLoadMore: "useLoadMore", columns: "columns", rows: "rows", isLoading: "isLoading", isPaging: "isPaging", sort: "sort", expandedTemplate: "expandedTemplate", customTemplates: "customTemplates", lastEvaluatedKey: "lastEvaluatedKey" }, outputs: { onSortChange: "onSortChange", onLoadMore: "onLoadMore", onRowClick: "onRowClick", onRowDblClick: "onRowDblClick" }, providers: [provideTranslocoScope('haloduck')], viewQueries: [{ propertyName: "loadMoreRow", first: true, predicate: ["loadMoreRow"], descendants: true }, { propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, static: true }, { propertyName: "dataRows", predicate: ["dataRow"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div #scrollContainer class=\"w-full h-full shadow border border-light-inactive dark:border-dark-inactive rounded-lg overflow-auto\">\n <table class=\"w-full h-full\"\n [ngClass]=\"{\n 'table-fixed': tableLayout === 'fixed',\n 'table-auto': tableLayout === 'auto',\n }\">\n <colgroup>\n @for (column of columns; track column.key) {\n @if (!column.hidden || !(column.hidden | async)) {\n <col class=\"{{ column.width || '' }}\" />\n }\n }\n </colgroup>\n @if (showHeader) {\n <thead class=\"bg-light-inactive/50 dark:bg-dark-inactive/50 rounded-tl-lg rounded-tr-lg\">\n <tr>\n @for (column of columns; track column.key) {\n @if (!column.hidden || !(column.hidden | async)) {\n <th scope=\"col\"\n class=\"{{ column.align || '' }} py-3.5 pl-4 pr-3 text-sm font-semibold text-light-on-background dark:text-dark-on-background whitespace-nowrap\">\n <span class=\"group inline-flex\">\n {{ column.label }}\n @if (column.sortable) {\n @switch (getSortDirection(column.sort?.field || column.key) | async)\n {\n @case('desc') {\n <span (click)=\"onUpdateSort(column.sort?.field || column.key, 'asc')\"\n class=\"ml-2 flex-none rounded text-light-on-inactive dark:text-dark-on-inactive\">\n <svg class=\"size-5\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n data-slot=\"icon\">\n <path fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\" />\n </svg>\n </span>\n }\n @case('asc') {\n <span (click)=\"onUpdateSort(column.sort?.field || column.key, 'desc')\"\n class=\"ml-2 flex-none rounded text-light-on-inactive dark:text-dark-on-inactive\">\n <svg class=\"size-5\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n data-slot=\"icon\">\n <path fill-rule=\"evenodd\"\n d=\"M14.78 11.78a.75.75 0 0 1-1.06 0L10 8.06l-3.72 3.72a.75.75 0 1 1-1.06-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06Z\"\n clip-rule=\"evenodd\" />\n </svg>\n </span>\n }\n @default {\n <span (click)=\"onUpdateSort(column.sort?.field || column.key, 'asc')\"\n class=\"invisible ml-2 flex-none rounded text-light-on-inactive dark:text-dark-on-inactive group-hover:visible group-focus:visible\">\n <svg class=\"size-5\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n data-slot=\"icon\">\n <path fill-rule=\"evenodd\"\n d=\"M14.78 11.78a.75.75 0 0 1-1.06 0L10 8.06l-3.72 3.72a.75.75 0 1 1-1.06-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06Z\"\n clip-rule=\"evenodd\" />\n </svg>\n </span>\n }\n }\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n }\n <tbody class=\"overflow-scroll\">\n @for (row of rows | async; track row['id']; let lastRow = $last) {\n <tr #dataRow class=\"border-t border-light-inactive dark:border-dark-inactive {{ row.bgColor || '' }}\"\n (click)=\"onRowClicked(row)\"\n (dblclick)=\"onRowDblClicked(row)\"\n [ngClass]=\"{'rounded-b-lg': lastRow && (lastEvaluatedKey | async) === null, 'even:bg-light-alternative dark:even:bg-dark-alternative odd:bg-light-background dark:odd:bg-dark-background': !row.bgColor }\"\n [ngStyle]=\"{ 'background-color': row.bgColor || '' }\">\n @for (column of columns; track column.key) {\n @if (!column.hidden || !(column.hidden | async)) {\n <td class=\"relative overflow-visible {{ column.align || '' }} whitespace-nowrap px-3 text-sm text-light-on-background dark:text-dark-on-background\"\n [ngClass]=\"{\n 'first:rounded-bl-lg last:rounded-br-lg': lastRow && (lastEvaluatedKey | async) === null,\n 'text-wrap': row.isExpanded && column.type !== 'custom',\n 'overflow-x-hidden text-ellipsis' : !row.isExpanded && column.type !== 'custom',\n 'py-2': column.type === 'custom',\n 'py-4': column.type !== 'custom'\n }\">\n @if (column.type === 'custom') {\n @if (column.customRenderTemplate) {\n <ng-container [ngTemplateOutlet]=\"customTemplates[column.customRenderTemplate]\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\"></ng-container>\n }\n }\n @else {\n {{ getColumnValue(row, column) | async }}\n }\n </td>\n }\n }\n </tr>\n\n @if (row.isExpanded && expandedTemplate) {\n <tr>\n <td [attr.colspan]=\"columns.length\">\n </td>\n </tr>\n <tr class=\"even:bg-light-alternative dark:even:bg-dark-alternative odd:bg-light-background dark:odd:bg-dark-background\">\n <td [attr.colspan]=\"columns.length\"\n class=\"whitespace-nowrap px-3 pb-4 text-sm text-center\">\n <ng-container [ngTemplateOutlet]=\"expandedTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\"></ng-container>\n </td>\n </tr>\n }\n\n @if (lastRow && (lastEvaluatedKey | async) && !(isLoading | async) && useLoadMore) {\n <tr #loadMoreRow class=\"border-t border-light-inactive dark:border-dark-inactive bg-light-background dark:bg-dark-background\">\n <td [attr.colspan]=\"columns.length\"\n class=\"whitespace-nowrap px-3 py-4 text-sm text-light-on-background dark:text-dark-on-background text-center h-16 bg-light-background dark:bg-dark-background rounded-bl-lg rounded-br-lg\">\n <div (click)=\"onLoadMoreClicked()\"\n class=\"cursor-pointer\">\n {{ 'haloduck.ui.table.Load More...' | transloco }}\n </div>\n </td>\n </tr>\n }\n\n } @empty {\n @if(!(isLoading | async)) {\n <tr>\n <td [attr.colspan]=\"columns.length\"\n class=\"whitespace-nowrap px-3 py-4 text-sm text-light-on-background dark:text-dark-on-background text-center h-16 bg-light-background dark:bg-dark-background rounded-bl-lg rounded-br-lg\">\n {{ 'haloduck.ui.table.No data available.' | transloco }}\n </td>\n </tr>\n }\n }\n\n @if(isLoading | async) {\n @for ( i of [0,1,2,3,4]; track i; let lastRow = $last)\n {\n <tr class=\"bg-light-background dark:bg-dark-background border-t border-light-inactive/50 dark:border-dark-inactive/50\">\n @for (column of columns; track column.key) {\n <td class=\"whitespace-nowrap py-4 pl-4 pr-3 text-sm\"\n [ngClass]=\"{'first:rounded-bl-lg last:rounded-br-lg': lastRow}\">\n <div class=\"h-4 bg-light-inactive/50 dark:bg-dark-inactive/50 rounded-md animate-pulse\"></div>\n </td>\n }\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3073
3084
|
}
|
|
3074
3085
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TableComponent, decorators: [{
|
|
3075
3086
|
type: Component,
|
|
@@ -3107,8 +3118,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
|
|
|
3107
3118
|
}], loadMoreRow: [{
|
|
3108
3119
|
type: ViewChild,
|
|
3109
3120
|
args: ['loadMoreRow']
|
|
3110
|
-
}],
|
|
3111
|
-
type:
|
|
3121
|
+
}], dataRows: [{
|
|
3122
|
+
type: ViewChildren,
|
|
3112
3123
|
args: ['dataRow', { read: ElementRef }]
|
|
3113
3124
|
}], scrollContainer: [{
|
|
3114
3125
|
type: ViewChild,
|