@sd-angular/core 19.0.47 → 19.0.49

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, inject, TemplateRef, Directive, booleanAttribute, computed, ChangeDetectionStrategy, Component, model, output, isSignal, effect, Pipe, ChangeDetectorRef, signal, untracked, viewChild, InjectionToken, Injectable, EventEmitter, Input, Output, ViewChild, ElementRef, DestroyRef, afterNextRender, Renderer2, NgZone, contentChild, contentChildren } from '@angular/core';
2
+ import { input, inject, TemplateRef, Directive, booleanAttribute, computed, ChangeDetectionStrategy, Component, model, output, isSignal, effect, Pipe, ChangeDetectorRef, signal, untracked, viewChild, InjectionToken, Injectable, DestroyRef, EventEmitter, Input, Output, ViewChild, ElementRef, NgZone, afterNextRender, Renderer2, contentChild, contentChildren } from '@angular/core';
3
3
  import { trigger, state, style, transition, animate } from '@angular/animations';
4
4
  import * as i4 from '@angular/material/paginator';
5
5
  import { MatPaginatorIntl, MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
@@ -9,7 +9,7 @@ import * as i6 from '@angular/material/table';
9
9
  import { MatTable, MatTableModule } from '@angular/material/table';
10
10
  import { ɵSD_LOCAL_DATE_TIME as _SD_LOCAL_DATE_TIME, SdDateTimeService } from '@sd-angular/core/services/date-time';
11
11
  import { SdNotifyService } from '@sd-angular/core/services/notify';
12
- import { Subject, fromEvent, Subscription, isObservable, firstValueFrom } from 'rxjs';
12
+ import { Subject, merge, fromEvent, take, Subscription, isObservable, firstValueFrom } from 'rxjs';
13
13
  import { debounceTime, startWith, map, switchMap, filter } from 'rxjs/operators';
14
14
  import { DateUtilities, StringUtilities } from '@sd-angular/core/utilities';
15
15
  import { CdkTableModule, CdkColumnDef } from '@angular/cdk/table';
@@ -1442,15 +1442,12 @@ class ConfigComponent {
1442
1442
  // dragDisabled không nên là signal (mutated trong drag handlers, không trigger render).
1443
1443
  dragDisabled = true;
1444
1444
  #setting;
1445
+ #dragRelease;
1445
1446
  // ==========================================
1446
- // 5. WINDOW MOUSEUP — tự destroy qua takeUntilDestroyed (thay @HostListener)
1447
+ // 5. Dọn listener của phiên kéo hiện tại
1447
1448
  // ==========================================
1448
1449
  constructor() {
1449
- fromEvent(window, 'mouseup')
1450
- .pipe(takeUntilDestroyed())
1451
- .subscribe(() => {
1452
- this.dragDisabled = true;
1453
- });
1450
+ inject(DestroyRef).onDestroy(() => this.#dragRelease?.unsubscribe());
1454
1451
  }
1455
1452
  // ==========================================
1456
1453
  // 6. PUBLIC API
@@ -1464,19 +1461,20 @@ class ConfigComponent {
1464
1461
  this.modal()?.open();
1465
1462
  };
1466
1463
  close = () => {
1464
+ this.handleMouseUp();
1467
1465
  this.modal()?.close();
1468
1466
  };
1469
1467
  onSave = () => {
1470
1468
  const cfg = this.configuration();
1471
1469
  if (cfg)
1472
1470
  this.#setting?.set(cfg);
1473
- this.modal()?.close();
1471
+ this.close();
1474
1472
  this.#ref.detectChanges();
1475
1473
  };
1476
1474
  onReset = async () => {
1477
1475
  this.#confirmService.confirm(this.#i18n.t('core.component.table.config.confirm-reset')).then(() => {
1478
1476
  this.#setting?.remove();
1479
- this.modal()?.close();
1477
+ this.close();
1480
1478
  this.#ref.detectChanges();
1481
1479
  });
1482
1480
  };
@@ -1489,12 +1487,20 @@ class ConfigComponent {
1489
1487
  }
1490
1488
  handleMouseDown() {
1491
1489
  this.dragDisabled = false;
1490
+ if (this.#dragRelease && !this.#dragRelease.closed)
1491
+ return;
1492
+ // why: chỉ cần bắt thả chuột ngoài handle trong lúc kéo; modal đóng không giữ listener toàn cục.
1493
+ this.#dragRelease = merge(fromEvent(window, 'mouseup'), fromEvent(window, 'blur'))
1494
+ .pipe(take(1))
1495
+ .subscribe(() => this.handleMouseUp());
1492
1496
  }
1493
1497
  handleMouseUp() {
1494
1498
  this.dragDisabled = true;
1499
+ this.#dragRelease?.unsubscribe();
1500
+ this.#dragRelease = undefined;
1495
1501
  }
1496
1502
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: ConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1497
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: ConfigComponent, isStandalone: true, selector: "config", inputs: { autoIdInput: { classPropertyName: "autoIdInput", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, tableOption: { classPropertyName: "tableOption", publicName: "tableOption", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changes: "changes" }, providers: [ConfigService], viewQueries: [{ propertyName: "modal", first: true, predicate: SdModal, descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: MatTable, descendants: true, isSignal: true }], ngImport: i0, template: "@let _configuration = configuration();\n@let _autoId = autoId();\n\n<sd-modal width=\"sm\" [title]=\"'core.component.table.setup' | translate\" #modal>\n @if (_configuration) {\n <div class=\"c-container\">\n <div class=\"c-table\" style=\"max-height: 70vh\">\n <table\n mat-table\n [dataSource]=\"_configuration.columns!\"\n cdkDropList\n [cdkDropListData]=\"_configuration.columns\"\n (cdkDropListDropped)=\"dropTable($event)\">\n <ng-container matColumnDef=\"invisible\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 96px\">\n {{ 'core.component.form-builder.display' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <div class=\"d-flex\" style=\"gap: 8px\">\n <div class=\"c-handle\" (mousedown)=\"handleMouseDown()\" (mouseup)=\"handleMouseUp()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#5f6368\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\" />\n </svg>\n </div>\n <sd-switch [model]=\"!item.invisible\" (modelChange)=\"item.invisible = !item.invisible\" />\n </div>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"title\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef>\n {{ 'core.component.table.column-header' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input [clearable]=\"true\" class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.title\" [(model)]=\"item.title\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"width\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.width' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.width\" [(model)]=\"item.width\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"fixed\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.fixed' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.fixed\" />\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"truncate\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 100px\">\n {{ 'core.component.table.char-limit' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.truncate\" />\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"['invisible', 'title', 'width', 'fixed', 'truncate']; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: ['invisible', 'title', 'width', 'fixed', 'truncate']\"\n cdkDrag\n [cdkDragData]=\"row\"\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"dragDisabled\">\n </tr>\n </table>\n </div>\n </div>\n }\n <ng-container sdFooter>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-skip' : null\" class=\"mr-8\" (click)=\"close()\" [title]=\"'core.component.table.skip' | translate\" color=\"secondary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-reset' : null\" class=\"mr-8\" (click)=\"onReset()\" [title]=\"'core.component.form-builder.default' | translate\" color=\"primary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-apply' : null\" (click)=\"onSave()\" [title]=\"'core.component.table.apply' | translate\" type=\"fill\" color=\"primary\"> </sd-button>\n </ng-container>\n</sd-modal>\n", styles: [".c-table{position:relative;overflow:auto}.c-table table{width:100%}.c-table .c-th{font-weight:500;font-size:14px;line-height:20px;color:#000}.c-container{position:relative;min-height:200px}.c-handle{color:#ccc;cursor:move}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i6.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i6.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i6.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i6.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i6.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i6.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i6.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i6.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i6.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i6.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "ngmodule", type: CdkTableModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i11.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i11.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdModal, selector: "sd-modal", inputs: ["autoId", "title", "color", "width", "height", "view", "modalClass", "lazyLoadContent", "hideClose", "disableBackdropClose"], outputs: ["sdClosed"] }, { kind: "component", type: SdSwitch, selector: "sd-switch", inputs: ["autoId", "name", "form", "label", "color", "disabled", "viewed", "hideInlineError", "required", "model"], outputs: ["modelChange", "sdChange"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1503
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: ConfigComponent, isStandalone: true, selector: "config", inputs: { autoIdInput: { classPropertyName: "autoIdInput", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, tableOption: { classPropertyName: "tableOption", publicName: "tableOption", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changes: "changes" }, providers: [ConfigService], viewQueries: [{ propertyName: "modal", first: true, predicate: SdModal, descendants: true, isSignal: true }, { propertyName: "table", first: true, predicate: MatTable, descendants: true, isSignal: true }], ngImport: i0, template: "@let _configuration = configuration();\n@let _autoId = autoId();\n\n<sd-modal width=\"sm\" [title]=\"'core.component.table.setup' | translate\" (sdClosed)=\"handleMouseUp()\" #modal>\n @if (_configuration) {\n <div class=\"c-container\">\n <div class=\"c-table\" style=\"max-height: 70vh\">\n <table\n mat-table\n [dataSource]=\"_configuration.columns!\"\n cdkDropList\n [cdkDropListData]=\"_configuration.columns\"\n (cdkDropListDropped)=\"dropTable($event)\">\n <ng-container matColumnDef=\"invisible\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 96px\">\n {{ 'core.component.form-builder.display' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <div class=\"d-flex\" style=\"gap: 8px\">\n <div class=\"c-handle\" (mousedown)=\"handleMouseDown()\" (mouseup)=\"handleMouseUp()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#5f6368\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\" />\n </svg>\n </div>\n <sd-switch [model]=\"!item.invisible\" (modelChange)=\"item.invisible = !item.invisible\" />\n </div>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"title\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef>\n {{ 'core.component.table.column-header' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input [clearable]=\"true\" class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.title\" [(model)]=\"item.title\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"width\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.width' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.width\" [(model)]=\"item.width\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"fixed\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.fixed' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.fixed\" />\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"truncate\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 100px\">\n {{ 'core.component.table.char-limit' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.truncate\" />\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"['invisible', 'title', 'width', 'fixed', 'truncate']; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: ['invisible', 'title', 'width', 'fixed', 'truncate']\"\n cdkDrag\n [cdkDragData]=\"row\"\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"dragDisabled\">\n </tr>\n </table>\n </div>\n </div>\n }\n <ng-container sdFooter>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-skip' : null\" class=\"mr-8\" (click)=\"close()\" [title]=\"'core.component.table.skip' | translate\" color=\"secondary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-reset' : null\" class=\"mr-8\" (click)=\"onReset()\" [title]=\"'core.component.form-builder.default' | translate\" color=\"primary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-apply' : null\" (click)=\"onSave()\" [title]=\"'core.component.table.apply' | translate\" type=\"fill\" color=\"primary\"> </sd-button>\n </ng-container>\n</sd-modal>\n", styles: [".c-table{position:relative;overflow:auto}.c-table table{width:100%}.c-table .c-th{font-weight:500;font-size:14px;line-height:20px;color:#000}.c-container{position:relative;min-height:200px}.c-handle{color:#ccc;cursor:move}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i6.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i6.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i6.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i6.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i6.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i6.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i6.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i6.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i6.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i6.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "ngmodule", type: CdkTableModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i11.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i11.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdModal, selector: "sd-modal", inputs: ["autoId", "title", "color", "width", "height", "view", "modalClass", "lazyLoadContent", "hideClose", "disableBackdropClose"], outputs: ["sdClosed"] }, { kind: "component", type: SdSwitch, selector: "sd-switch", inputs: ["autoId", "name", "form", "label", "color", "disabled", "viewed", "hideInlineError", "required", "model"], outputs: ["modelChange", "sdChange"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1498
1504
  }
1499
1505
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: ConfigComponent, decorators: [{
1500
1506
  type: Component,
@@ -1510,7 +1516,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
1510
1516
  SdModal,
1511
1517
  SdSwitch,
1512
1518
  TranslatePipe,
1513
- ], providers: [ConfigService], template: "@let _configuration = configuration();\n@let _autoId = autoId();\n\n<sd-modal width=\"sm\" [title]=\"'core.component.table.setup' | translate\" #modal>\n @if (_configuration) {\n <div class=\"c-container\">\n <div class=\"c-table\" style=\"max-height: 70vh\">\n <table\n mat-table\n [dataSource]=\"_configuration.columns!\"\n cdkDropList\n [cdkDropListData]=\"_configuration.columns\"\n (cdkDropListDropped)=\"dropTable($event)\">\n <ng-container matColumnDef=\"invisible\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 96px\">\n {{ 'core.component.form-builder.display' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <div class=\"d-flex\" style=\"gap: 8px\">\n <div class=\"c-handle\" (mousedown)=\"handleMouseDown()\" (mouseup)=\"handleMouseUp()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#5f6368\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\" />\n </svg>\n </div>\n <sd-switch [model]=\"!item.invisible\" (modelChange)=\"item.invisible = !item.invisible\" />\n </div>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"title\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef>\n {{ 'core.component.table.column-header' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input [clearable]=\"true\" class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.title\" [(model)]=\"item.title\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"width\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.width' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.width\" [(model)]=\"item.width\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"fixed\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.fixed' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.fixed\" />\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"truncate\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 100px\">\n {{ 'core.component.table.char-limit' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.truncate\" />\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"['invisible', 'title', 'width', 'fixed', 'truncate']; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: ['invisible', 'title', 'width', 'fixed', 'truncate']\"\n cdkDrag\n [cdkDragData]=\"row\"\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"dragDisabled\">\n </tr>\n </table>\n </div>\n </div>\n }\n <ng-container sdFooter>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-skip' : null\" class=\"mr-8\" (click)=\"close()\" [title]=\"'core.component.table.skip' | translate\" color=\"secondary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-reset' : null\" class=\"mr-8\" (click)=\"onReset()\" [title]=\"'core.component.form-builder.default' | translate\" color=\"primary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-apply' : null\" (click)=\"onSave()\" [title]=\"'core.component.table.apply' | translate\" type=\"fill\" color=\"primary\"> </sd-button>\n </ng-container>\n</sd-modal>\n", styles: [".c-table{position:relative;overflow:auto}.c-table table{width:100%}.c-table .c-th{font-weight:500;font-size:14px;line-height:20px;color:#000}.c-container{position:relative;min-height:200px}.c-handle{color:#ccc;cursor:move}\n"] }]
1519
+ ], providers: [ConfigService], template: "@let _configuration = configuration();\n@let _autoId = autoId();\n\n<sd-modal width=\"sm\" [title]=\"'core.component.table.setup' | translate\" (sdClosed)=\"handleMouseUp()\" #modal>\n @if (_configuration) {\n <div class=\"c-container\">\n <div class=\"c-table\" style=\"max-height: 70vh\">\n <table\n mat-table\n [dataSource]=\"_configuration.columns!\"\n cdkDropList\n [cdkDropListData]=\"_configuration.columns\"\n (cdkDropListDropped)=\"dropTable($event)\">\n <ng-container matColumnDef=\"invisible\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 96px\">\n {{ 'core.component.form-builder.display' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <div class=\"d-flex\" style=\"gap: 8px\">\n <div class=\"c-handle\" (mousedown)=\"handleMouseDown()\" (mouseup)=\"handleMouseUp()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 0 24 24\" width=\"24px\" fill=\"#5f6368\">\n <path d=\"M0 0h24v24H0V0z\" fill=\"none\" />\n <path\n d=\"M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\" />\n </svg>\n </div>\n <sd-switch [model]=\"!item.invisible\" (modelChange)=\"item.invisible = !item.invisible\" />\n </div>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"title\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef>\n {{ 'core.component.table.column-header' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input [clearable]=\"true\" class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.title\" [(model)]=\"item.title\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"width\">\n <th mat-header-cell class=\"c-th px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.width' | translate }}\n </th>\n <td class=\"px-4\" mat-cell *matCellDef=\"let item\">\n <sd-input class=\"py-4\" size=\"sm\" [placeholder]=\"item?.origin?.width\" [(model)]=\"item.width\" hideInlineError></sd-input>\n </td>\n </ng-container>\n <ng-container matColumnDef=\"fixed\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 80px\">\n {{ 'core.component.table.fixed' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.fixed\" />\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"truncate\">\n <th mat-header-cell class=\"c-th text-center px-4\" *matHeaderCellDef style=\"width: 100px\">\n {{ 'core.component.table.char-limit' | translate }}\n </th>\n <td class=\"text-center px-4\" mat-cell *matCellDef=\"let item\">\n <sd-switch [(model)]=\"item.truncate\" />\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"['invisible', 'title', 'width', 'fixed', 'truncate']; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: ['invisible', 'title', 'width', 'fixed', 'truncate']\"\n cdkDrag\n [cdkDragData]=\"row\"\n cdkDragLockAxis=\"y\"\n [cdkDragDisabled]=\"dragDisabled\">\n </tr>\n </table>\n </div>\n </div>\n }\n <ng-container sdFooter>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-skip' : null\" class=\"mr-8\" (click)=\"close()\" [title]=\"'core.component.table.skip' | translate\" color=\"secondary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-reset' : null\" class=\"mr-8\" (click)=\"onReset()\" [title]=\"'core.component.form-builder.default' | translate\" color=\"primary\"> </sd-button>\n <sd-button [autoId]=\"_autoId ? _autoId + '-config-apply' : null\" (click)=\"onSave()\" [title]=\"'core.component.table.apply' | translate\" type=\"fill\" color=\"primary\"> </sd-button>\n </ng-container>\n</sd-modal>\n", styles: [".c-table{position:relative;overflow:auto}.c-table table{width:100%}.c-table .c-th{font-weight:500;font-size:14px;line-height:20px;color:#000}.c-container{position:relative;min-height:200px}.c-handle{color:#ccc;cursor:move}\n"] }]
1514
1520
  }], ctorParameters: () => [] });
1515
1521
 
1516
1522
  /** Segments that must never be traversed, matching the `@sdcorejs/utils` 1.2 safe-key rule. */
@@ -2207,25 +2213,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
2207
2213
  class StickyShadowDirective {
2208
2214
  #el = inject((ElementRef));
2209
2215
  #destroyRef = inject(DestroyRef);
2216
+ #zone = inject(NgZone);
2210
2217
  constructor() {
2211
- afterNextRender(() => {
2218
+ afterNextRender(() => this.#zone.runOutsideAngular(() => {
2212
2219
  const container = this.#el.nativeElement;
2213
2220
  // Dùng MutationObserver để detect khi CDK render rows mới vào table
2214
2221
  // (data load async, phân trang, ...) — lúc này td mới có đủ sticky classes
2215
- const mutationObserver = new MutationObserver(() => {
2216
- this.#updateShadow(container);
2217
- });
2222
+ // why: gộp mutation và scroll vào cùng một lượt quét, không kích hoạt change detection của bảng.
2223
+ const mutations = new Subject();
2224
+ const mutationObserver = new MutationObserver(() => mutations.next());
2218
2225
  mutationObserver.observe(container, {
2219
2226
  childList: true, // Theo dõi thêm/bớt các node con
2220
2227
  subtree: true, // Bao gồm tất cả các cấp con cháu
2221
2228
  });
2222
2229
  // Cập nhật shadow mỗi khi user scroll ngang
2223
- fromEvent(container, 'scroll')
2230
+ merge(mutations, fromEvent(container, 'scroll'))
2224
2231
  .pipe(debounceTime(10), takeUntilDestroyed(this.#destroyRef))
2225
2232
  .subscribe(() => this.#updateShadow(container));
2226
2233
  // Disconnect MutationObserver khi directive bị destroy
2227
- this.#destroyRef.onDestroy(() => mutationObserver.disconnect());
2228
- });
2234
+ this.#destroyRef.onDestroy(() => {
2235
+ mutationObserver.disconnect();
2236
+ mutations.complete();
2237
+ });
2238
+ }));
2229
2239
  }
2230
2240
  /**
2231
2241
  * Toggle class shadow lên tất cả th và td của một cột.
@@ -2707,9 +2717,7 @@ class TableExportService {
2707
2717
  // theo catalog ngôn ngữ hiện tại.
2708
2718
  exportTitle = computed(() => {
2709
2719
  const percent = this.#exportPercent();
2710
- return percent === null
2711
- ? this.#i18n.t('core.component.table.export')
2712
- : this.#i18n.t('core.component.table.exporting', { percent });
2720
+ return percent === null ? this.#i18n.t('core.component.table.export') : this.#i18n.t('core.component.table.exporting', { percent });
2713
2721
  });
2714
2722
  /** Đặt tiến độ export (0-100), hoặc null để trả nút về nhãn mặc định. */
2715
2723
  setExportProgress = (percent) => {
@@ -2826,13 +2834,13 @@ class TableExportService {
2826
2834
  }
2827
2835
  }
2828
2836
  else if (column.type === 'date') {
2829
- obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd');
2837
+ obj[fieldStr] = isCSV ? this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd') : this.#toExcelDate(itemValue, column.type);
2830
2838
  }
2831
2839
  else if (column.type === 'datetime') {
2832
- obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd HH:mm:ss');
2840
+ obj[fieldStr] = isCSV ? this.#dateTime.toFormat(itemValue, 'yyyy/MM/dd HH:mm:ss') : this.#toExcelDate(itemValue, column.type);
2833
2841
  }
2834
2842
  else if (column.type === 'time') {
2835
- obj[fieldStr] = this.#dateTime.toFormat(itemValue, 'HH:mm:ss');
2843
+ obj[fieldStr] = isCSV ? this.#dateTime.toFormat(itemValue, 'HH:mm:ss') : this.#toExcelDate(itemValue, column.type);
2836
2844
  }
2837
2845
  else if (column.type === 'values' || column.type === 'lazy-values') {
2838
2846
  const vals = (Array.isArray(itemValue) ? itemValue : [itemValue]).filter(e => !!e?.toString());
@@ -2880,8 +2888,21 @@ class TableExportService {
2880
2888
  }
2881
2889
  }
2882
2890
  }
2891
+ const allColumns = this.#allColumns(option);
2892
+ const allExportedColumns = this.#allExportedColumns(option);
2893
+ const dateFormats = { date: 'dd/mm/yyyy', datetime: 'dd/mm/yyyy hh:mm:ss', time: 'hh:mm:ss' };
2894
+ const excelColumns = columns.map(exportColumn => {
2895
+ const column = allColumns.find(e => e.field === exportColumn.field);
2896
+ const exportedColumn = allExportedColumns.find(e => e.field === exportColumn.field);
2897
+ if (!column || column.transform || exportedColumn?.transform)
2898
+ return { ...exportColumn };
2899
+ if (column.type === 'date' || column.type === 'datetime' || column.type === 'time') {
2900
+ return { ...exportColumn, numFmt: dateFormats[column.type] };
2901
+ }
2902
+ return { ...exportColumn };
2903
+ });
2883
2904
  await this.#excelService.export({
2884
- columns,
2905
+ columns: excelColumns,
2885
2906
  items,
2886
2907
  fileName: option.export?.fileName,
2887
2908
  sheets,
@@ -2892,6 +2913,26 @@ class TableExportService {
2892
2913
  this.setExportProgress(null);
2893
2914
  }
2894
2915
  }
2916
+ /** Convert the existing formatter's wall clock to an Excel 1900 serial, never reparsing user text. */
2917
+ #toExcelDate(value, type) {
2918
+ const formatted = this.#dateTime.toFormat(value, type === 'time' ? 'HH:mm:ss' : 'yyyy/MM/dd HH:mm:ss');
2919
+ if (!formatted)
2920
+ return '';
2921
+ const [calendar, clock] = type === 'time' ? ['', formatted] : formatted.split(' ');
2922
+ const [hour, minute, second] = clock.split(':').map(Number);
2923
+ const fraction = (hour * 3600 + minute * 60 + second) / 86400;
2924
+ if (type === 'time')
2925
+ return fraction;
2926
+ const [year, month, day] = calendar.split('/').map(Number);
2927
+ if (year < 1900 || year > 9999) {
2928
+ return this.#dateTime.toFormat(value, type === 'date' ? 'dd/MM/yyyy' : 'dd/MM/yyyy HH:mm:ss');
2929
+ }
2930
+ // why: tính từ thành phần UTC để DST của máy không làm lệch wall clock đã được formatter xác định.
2931
+ const days = (Date.UTC(year, month - 1, day) - Date.UTC(1899, 11, 31)) / 86400000;
2932
+ // why: Excel giữ ngày 29/02/1900 không có thật ở serial 60; ngày thực từ 01/03/1900 phải cộng 1.
2933
+ const serial = days + (days >= 60 ? 1 : 0);
2934
+ return type === 'date' ? serial : serial + fraction;
2935
+ }
2895
2936
  #getExportColumns(context, explicitColumns) {
2896
2937
  const { option, configuration } = context;
2897
2938
  let tableColumns = [];