@haloduck/ui 2.0.24 → 2.0.26
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 +48 -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,38 @@ 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
|
+
const container = this.scrollContainer?.nativeElement;
|
|
3058
|
+
const allowWithoutScroll = !container || container.scrollHeight <= container.clientHeight;
|
|
3059
|
+
if (!this.hasObservedOnce && !allowWithoutScroll) {
|
|
3060
|
+
// 내부 스크롤 컨테이너가 있고 첫 교차는 무시
|
|
3061
|
+
this.hasObservedOnce = true;
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
// 마지막 키가 존재, 로딩 아님, 그리고 (실제 스크롤 발생 or 스크롤 불필요)일 때만 로드
|
|
3065
|
+
Promise.all([
|
|
3066
|
+
firstValueFrom(this.lastEvaluatedKey.pipe(take(1))),
|
|
3067
|
+
firstValueFrom(this.isLoading.pipe(take(1))),
|
|
3068
|
+
]).then(([lastKey, isLoading]) => {
|
|
3069
|
+
const scrolledOk = this.didScrollSinceLastLoad || allowWithoutScroll;
|
|
3070
|
+
if (!(lastKey && !isLoading && scrolledOk)) {
|
|
3071
|
+
return;
|
|
3045
3072
|
}
|
|
3073
|
+
// 중복 호출 방지: 즉시 관찰 해제 후 로드 트리거
|
|
3074
|
+
this.cleanupIntersectionObserver();
|
|
3075
|
+
this.didScrollSinceLastLoad = false;
|
|
3046
3076
|
this.ngZone.run(() => this.onLoadMoreClicked());
|
|
3047
|
-
}
|
|
3077
|
+
});
|
|
3048
3078
|
}
|
|
3049
3079
|
};
|
|
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
|
-
}
|
|
3080
|
+
onScroll = () => {
|
|
3081
|
+
this.didScrollSinceLastLoad = true;
|
|
3082
|
+
};
|
|
3083
|
+
// Note: 무스크롤 자동 로드는 연속 로딩을 유발할 수 있어 제거
|
|
3070
3084
|
constructor() { }
|
|
3071
3085
|
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: "
|
|
3086
|
+
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
3087
|
}
|
|
3074
3088
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: TableComponent, decorators: [{
|
|
3075
3089
|
type: Component,
|
|
@@ -3107,8 +3121,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
|
|
|
3107
3121
|
}], loadMoreRow: [{
|
|
3108
3122
|
type: ViewChild,
|
|
3109
3123
|
args: ['loadMoreRow']
|
|
3110
|
-
}],
|
|
3111
|
-
type:
|
|
3124
|
+
}], dataRows: [{
|
|
3125
|
+
type: ViewChildren,
|
|
3112
3126
|
args: ['dataRow', { read: ElementRef }]
|
|
3113
3127
|
}], scrollContainer: [{
|
|
3114
3128
|
type: ViewChild,
|