@simplysm/angular 14.0.53 → 14.0.55

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.
Files changed (51) hide show
  1. package/dist/controls/button/sd-modal-select-button.d.ts +9 -10
  2. package/dist/controls/button/sd-modal-select-button.d.ts.map +1 -1
  3. package/dist/controls/button/sd-modal-select-button.js +4 -7
  4. package/dist/core/select-modal-output-result.d.ts +2 -3
  5. package/dist/core/select-modal-output-result.d.ts.map +1 -1
  6. package/dist/core/selection/useSelectionManager.d.ts +11 -11
  7. package/dist/core/selection/useSelectionManager.d.ts.map +1 -1
  8. package/dist/core/selection/useSelectionManager.js +29 -28
  9. package/dist/data/crud/sd-base-container.d.ts +21 -0
  10. package/dist/data/crud/sd-base-container.d.ts.map +1 -0
  11. package/dist/data/crud/sd-base-container.js +200 -0
  12. package/dist/data/crud/sd-crud-detail.d.ts +22 -0
  13. package/dist/data/crud/sd-crud-detail.d.ts.map +1 -0
  14. package/dist/data/crud/sd-crud-detail.js +267 -0
  15. package/dist/data/crud/sd-crud-list.d.ts +54 -0
  16. package/dist/data/crud/sd-crud-list.d.ts.map +1 -0
  17. package/dist/data/crud/sd-crud-list.js +592 -0
  18. package/dist/data/shared-data/sd-shared-data-select-button.d.ts.map +1 -1
  19. package/dist/data/shared-data/sd-shared-data-select-button.js +4 -5
  20. package/dist/data/shared-data/sd-shared-data-select-list.js +2 -2
  21. package/dist/data/shared-data/sd-shared-data-select.js +3 -3
  22. package/dist/data/sheet/injectSheetSelectRowIndicator.d.ts +4 -3
  23. package/dist/data/sheet/injectSheetSelectRowIndicator.d.ts.map +1 -1
  24. package/dist/data/sheet/injectSheetSelectRowIndicator.js +12 -4
  25. package/dist/data/sheet/sd-sheet.d.ts +39 -39
  26. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  27. package/dist/data/sheet/sd-sheet.js +96 -74
  28. package/dist/data/sheet/types.d.ts +1 -0
  29. package/dist/data/sheet/types.d.ts.map +1 -1
  30. package/dist/data/sheet/useSheetLayoutEngine.d.ts.map +1 -1
  31. package/dist/data/sheet/useSheetLayoutEngine.js +3 -1
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -0
  35. package/dist/styles.css +2 -2
  36. package/package.json +13 -13
  37. package/scss/commons/_variables.scss +2 -2
  38. package/src/controls/button/sd-modal-select-button.ts +9 -13
  39. package/src/core/select-modal-output-result.ts +2 -3
  40. package/src/core/selection/useSelectionManager.ts +47 -49
  41. package/src/data/crud/sd-base-container.ts +115 -0
  42. package/src/data/crud/sd-crud-detail.ts +122 -0
  43. package/src/data/crud/sd-crud-list.ts +316 -0
  44. package/src/data/shared-data/sd-shared-data-select-button.ts +0 -1
  45. package/src/data/shared-data/sd-shared-data-select-list.ts +2 -2
  46. package/src/data/shared-data/sd-shared-data-select.ts +3 -3
  47. package/src/data/sheet/injectSheetSelectRowIndicator.ts +17 -7
  48. package/src/data/sheet/sd-sheet.ts +59 -42
  49. package/src/data/sheet/types.ts +1 -0
  50. package/src/data/sheet/useSheetLayoutEngine.ts +3 -1
  51. package/src/index.ts +5 -0
@@ -0,0 +1,316 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ contentChild,
6
+ contentChildren,
7
+ inject,
8
+ input,
9
+ model,
10
+ output,
11
+ TemplateRef,
12
+ viewChild,
13
+ ViewEncapsulation,
14
+ } from "@angular/core";
15
+ import { SdActivatedModalProvider } from "../../core/modal/sd-activated-modal.provider";
16
+ import { SdAnchor } from "../../controls/button/sd-anchor";
17
+ import { SdButton } from "../../controls/button/sd-button";
18
+ import { SdCommandDirective } from "../../core/commands/sd-command";
19
+ import { SdForm } from "../../controls/form/sd-form";
20
+ import { SdSheet } from "../sheet/sd-sheet";
21
+ import { SdSheetColumn } from "../sheet/sd-sheet-column";
22
+ import { SdSheetColumnCellTemplate } from "../sheet/sd-sheet-column-cell-template";
23
+ import type { SdViewType } from "../../core/routing/injectViewTypeSignal";
24
+ import type { SortingDef } from "../../core/selection/useSortingManager";
25
+ import { SdBaseContainer } from "./sd-base-container";
26
+ import { NgIcon } from "@ng-icons/core";
27
+ import { NgTemplateOutlet } from "@angular/common";
28
+ import {
29
+ tablerCirclePlus,
30
+ tablerDeviceFloppy,
31
+ tablerEraser,
32
+ tablerRestore,
33
+ tablerSearch,
34
+ } from "@ng-icons/tabler-icons";
35
+
36
+ @Component({
37
+ selector: "sd-crud-list",
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ encapsulation: ViewEncapsulation.None,
40
+ standalone: true,
41
+ imports: [
42
+ SdBaseContainer,
43
+ SdButton,
44
+ NgIcon,
45
+ NgTemplateOutlet,
46
+ SdSheet,
47
+ SdForm,
48
+ SdAnchor,
49
+ SdSheetColumn,
50
+ SdSheetColumnCellTemplate,
51
+ ],
52
+ hostDirectives: [{ directive: SdCommandDirective, outputs: ["sdSaveCommand"] }],
53
+ host: {
54
+ "(sdSaveCommand)": "onSaveButtonClick()",
55
+ },
56
+ template: `
57
+ <sd-base-container
58
+ [(ready)]="ready"
59
+ [initialized]="initialized()"
60
+ [(busyCount)]="busyCount"
61
+ [restricted]="restricted()"
62
+ [viewType]="viewType()"
63
+ >
64
+ <!-- TOP -->
65
+ @if (viewType() === "page" && (!readonly() || commandTplRef())) {
66
+ <ng-template #topbarTpl>
67
+ @if (!readonly()) {
68
+ <sd-button [theme]="'link-primary'" (click)="formCtrl()?.requestSubmit()">
69
+ <ng-icon [svg]="tablerDeviceFloppy" />
70
+ 저장
71
+ <small>(CTRL+S)</small>
72
+ </sd-button>
73
+ }
74
+
75
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
76
+ </ng-template>
77
+ } @else if (!readonly() || commandTplRef()) {
78
+ <ng-template #commandTpl>
79
+ @if (!readonly()) {
80
+ <sd-button [theme]="'primary'" (click)="onSaveButtonClick()">
81
+ <ng-icon [svg]="tablerDeviceFloppy" />
82
+ 저장
83
+ <small>(CTRL+S)</small>
84
+ </sd-button>
85
+ }
86
+
87
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
88
+ </ng-template>
89
+ }
90
+
91
+ <!-- BOTTOM -->
92
+ @if ((viewType() === "modal" && selectMode() != null) || bottomCommandTplRef()) {
93
+ <ng-template #bottomCommandTpl>
94
+ @if (bottomCommandTplRef()) {
95
+ <div class="flex-fill flex-row main-align-start gap-sm">
96
+ <ng-template [ngTemplateOutlet]="bottomCommandTplRef()" />
97
+ </div>
98
+ }
99
+
100
+ @if (selectMode() != null) {
101
+ <sd-button
102
+ [size]="'sm'"
103
+ [theme]="'danger'"
104
+ (click)="onModalSelectionCancelClick()"
105
+ [disabled]="selectedKeys().length < 1"
106
+ >
107
+ 선택 해제
108
+ </sd-button>
109
+ @if (selectMode() === "multi") {
110
+ <sd-button [size]="'sm'" [theme]="'primary'" (click)="onModalSelectionConfirmClick()">
111
+ 확인({{ selectedKeys().length }})
112
+ </sd-button>
113
+ }
114
+ }
115
+ </ng-template>
116
+ }
117
+
118
+ <!-- CONTENT -->
119
+ <div class="flex-column fill">
120
+ @if (filterTplRef()) {
121
+ <sd-form (formSubmit)="filterSubmit.emit()" class="block p-default">
122
+ <div class="form-box-inline">
123
+ <div>
124
+ <sd-button [type]="'submit'" [theme]="'info'">
125
+ <ng-icon [svg]="tablerSearch" />
126
+ 조회
127
+ </sd-button>
128
+ </div>
129
+ <ng-template [ngTemplateOutlet]="filterTplRef()" />
130
+ </div>
131
+ </sd-form>
132
+ }
133
+
134
+ @if (!readonly() || toolTplRef()) {
135
+ <div class="flex-row gap-sm p-xs-default">
136
+ @if (!readonly()) {
137
+ <sd-button [size]="'sm'" [theme]="'link-primary'" (click)="create.emit()">
138
+ <ng-icon [svg]="tablerCirclePlus" />
139
+ 등록
140
+ </sd-button>
141
+ @if (selectMode() !== "single") {
142
+ <sd-button
143
+ [size]="'sm'"
144
+ [theme]="'link-danger'"
145
+ (click)="delete.emit(currSelectedItems())"
146
+ [disabled]="!hasSelectedNotDeleted()"
147
+ >
148
+ <ng-icon [svg]="tablerEraser" />
149
+ 선택 삭제
150
+ </sd-button>
151
+ @if (hasSelectedDeleted()) {
152
+ <sd-button
153
+ [size]="'sm'"
154
+ [theme]="'link-warning'"
155
+ (click)="restore.emit(currSelectedItems())"
156
+ >
157
+ <ng-icon [svg]="tablerRestore" />
158
+ 선택 복구
159
+ </sd-button>
160
+ }
161
+ }
162
+ }
163
+
164
+ @if (toolTplRef()) {
165
+ <ng-template [ngTemplateOutlet]="toolTplRef()" />
166
+ }
167
+ </div>
168
+ }
169
+
170
+ @if (readonly()) {
171
+ <div class="flex-fill p-default pt-0">
172
+ <ng-template [ngTemplateOutlet]="sheet" />
173
+ </div>
174
+ } @else {
175
+ <sd-form #formCtrl (formSubmit)="submit.emit()" class="flex-fill p-default pt-0">
176
+ <ng-template [ngTemplateOutlet]="sheet" />
177
+ </sd-form>
178
+ }
179
+ </div>
180
+ </sd-base-container>
181
+
182
+ <ng-template #sheet>
183
+ <sd-sheet
184
+ [key]="key() + '-sheet'"
185
+ [items]="items()"
186
+ [(currentPage)]="currentPage"
187
+ [totalPageCount]="totalPageCount()"
188
+ [(sorts)]="sorts"
189
+ [useAutoSort]="totalPageCount() === 0"
190
+ [selectMode]="selectMode() ?? (readonly() ? undefined : 'multi')"
191
+ [(selectedKeys)]="selectedKeys"
192
+ [trackByFn]="trackByFn()"
193
+ [getItemCellStyleFn]="getItemCellStyleFn"
194
+ [autoSelect]="selectMode() === 'single' ? 'click' : undefined"
195
+ [columnControlsInput]="columnControls()"
196
+ (selectedKeysChange)="onSelectedKeysChange()"
197
+ >
198
+ @if (!readonly()) {
199
+ <sd-sheet-column
200
+ [fixed]="true"
201
+ [key]="'deleteButton'"
202
+ [disableResizing]="true"
203
+ [disableSorting]="true"
204
+ >
205
+ <ng-template #headerTpl>
206
+ <div class="p-xs-sm tx-center">
207
+ <ng-icon [svg]="tablerEraser" />
208
+ </div>
209
+ </ng-template>
210
+ <ng-template [cell]="items()" let-item="item">
211
+ <div class="p-xs-sm">
212
+ <sd-anchor
213
+ [theme]="'danger'"
214
+ (click)="isDeleted(item) ? restore.emit([item]) : delete.emit([item])"
215
+ >
216
+ <ng-icon [svg]="isDeleted(item) ? tablerRestore : tablerEraser" />
217
+ </sd-anchor>
218
+ </div>
219
+ </ng-template>
220
+ </sd-sheet-column>
221
+ }
222
+ </sd-sheet>
223
+ </ng-template>
224
+ `,
225
+ })
226
+ export class SdCrudList<TItem, TKey> {
227
+ private readonly _sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
228
+
229
+ ready = model(false);
230
+ initialized = input(false);
231
+ busyCount = model(0);
232
+ restricted = input(false);
233
+ readonly = input(false);
234
+ viewType = input.required<SdViewType>();
235
+ selectMode = input<"single" | "multi">();
236
+ key = input.required<string>();
237
+
238
+ formCtrl = viewChild<SdForm>("formCtrl");
239
+
240
+ filterSubmit = output();
241
+ submit = output();
242
+ create = output();
243
+ delete = output<TItem[]>();
244
+ restore = output<TItem[]>();
245
+
246
+ items = input<TItem[]>([]);
247
+ selectedKeys = model<NonNullable<TKey>[]>([]);
248
+
249
+ currDeletedItems = input<TItem[]>([]);
250
+ currDeletedSet = computed(() => new Set(this.currDeletedItems()));
251
+
252
+ currSelectedItems = computed(() =>
253
+ this.items().filter((it) => {
254
+ const key = this.trackByFn()(it);
255
+ return key != null && this.selectedKeys().includes(key);
256
+ }),
257
+ );
258
+
259
+ currentPage = model(0);
260
+ totalPageCount = input(0);
261
+ sorts = model<SortingDef[]>([]);
262
+
263
+ trackByFn = input.required<(item: TItem) => TKey>();
264
+
265
+ commandTplRef = contentChild<TemplateRef<void>>("commandTpl");
266
+ filterTplRef = contentChild<TemplateRef<void>>("filterTpl");
267
+ toolTplRef = contentChild<TemplateRef<void>>("toolTpl");
268
+ bottomCommandTplRef = contentChild<TemplateRef<void>>("bottomCommandTpl");
269
+
270
+ columnControls = contentChildren(SdSheetColumn);
271
+
272
+ hasSelectedDeleted = computed(() => this.currSelectedItems().some((it) => this.isDeleted(it)));
273
+ hasSelectedNotDeleted = computed(() =>
274
+ this.currSelectedItems().some((it) => !this.isDeleted(it)),
275
+ );
276
+
277
+ getItemCellStyleFn = (item: TItem): string | undefined =>
278
+ this.isDeleted(item) ? "text-decoration: line-through;" : undefined;
279
+
280
+ isDeleted(item: TItem) {
281
+ return this.currDeletedSet().has(item);
282
+ }
283
+
284
+ onSaveButtonClick() {
285
+ this.formCtrl()?.requestSubmit();
286
+ }
287
+
288
+ onModalSelectionCancelClick(): void {
289
+ this.selectedKeys.set([]);
290
+
291
+ if (this.selectMode() === "single") {
292
+ this._sdActivatedModal?.contentComponent().close.emit({ selectedKeys: [] });
293
+ }
294
+ }
295
+
296
+ onModalSelectionConfirmClick() {
297
+ const selectedKeys = this.selectedKeys();
298
+ this._sdActivatedModal?.contentComponent().close.emit({ selectedKeys });
299
+ }
300
+
301
+ onSelectedKeysChange() {
302
+ if (this.viewType() !== "modal") return;
303
+ if (this.selectMode() !== "single") return;
304
+
305
+ const selectedKeys = this.selectedKeys();
306
+ if (selectedKeys.length !== 1) return;
307
+
308
+ this._sdActivatedModal?.contentComponent().close.emit({ selectedKeys });
309
+ }
310
+
311
+ protected readonly tablerDeviceFloppy = tablerDeviceFloppy;
312
+ protected readonly tablerCirclePlus = tablerCirclePlus;
313
+ protected readonly tablerEraser = tablerEraser;
314
+ protected readonly tablerRestore = tablerRestore;
315
+ protected readonly tablerSearch = tablerSearch;
316
+ }
@@ -32,7 +32,6 @@ import type { SelectModeValue } from "../../controls/select/sd-select";
32
32
  template: `
33
33
  <sd-modal-select-button
34
34
  [(value)]="value"
35
- [(selectedItems)]="_selectedItems"
36
35
  [modal]="modal()"
37
36
  [disabled]="disabled()"
38
37
  [required]="required()"
@@ -234,7 +234,7 @@ export class SdSharedDataSelectList<
234
234
  ...modalInfo,
235
235
  inputs: {
236
236
  selectMode: "single",
237
- selectedItemKeys: [this.selectedItem()]
237
+ selectedKeys: [this.selectedItem()]
238
238
  .filter((v): v is NonNullable<typeof v> => v != null)
239
239
  .map((item) => item.__valueKey),
240
240
  ...modalInfo.inputs,
@@ -243,7 +243,7 @@ export class SdSharedDataSelectList<
243
243
 
244
244
  if (result != null) {
245
245
  const newSelectedItem = this.items().find(
246
- (item) => item.__valueKey === result.selectedItemKeys[0],
246
+ (item) => item.__valueKey === result.selectedKeys[0],
247
247
  );
248
248
  this.selectedItem.set(newSelectedItem);
249
249
  }
@@ -347,7 +347,7 @@ export class SdSharedDataSelect<
347
347
  ...modalInfo,
348
348
  inputs: {
349
349
  selectMode: this.selectMode(),
350
- selectedItemKeys: this.selectedKeys(),
350
+ selectedKeys: this.selectedKeys(),
351
351
  ...modalInfo.inputs,
352
352
  } as any,
353
353
  });
@@ -355,8 +355,8 @@ export class SdSharedDataSelect<
355
355
  if (result != null) {
356
356
  const newValue =
357
357
  this.selectMode() === "multi"
358
- ? result.selectedItemKeys
359
- : result.selectedItemKeys[0];
358
+ ? result.selectedKeys
359
+ : result.selectedKeys[0];
360
360
  this.value.set(newValue);
361
361
  }
362
362
  }
@@ -1,23 +1,33 @@
1
1
  import { afterEveryRender, type Signal } from "@angular/core";
2
+ import { obj } from "@simplysm/core-common";
2
3
  import type { injectSheetDomAccessor } from "./injectSheetDomAccessor";
3
4
 
4
- export function injectSheetSelectRowIndicator<T>(options: {
5
+ function isKeyEqual(a: unknown, b: unknown): boolean {
6
+ if (a === b) return true;
7
+ return obj.equal(a, b);
8
+ }
9
+
10
+ export function injectSheetSelectRowIndicator<TItem, TKey>(options: {
5
11
  domAccessor: ReturnType<typeof injectSheetDomAccessor>;
6
- selectedItems: Signal<T[]>;
7
- displayItems: Signal<T[]>;
12
+ selectedKeys: Signal<NonNullable<TKey>[]>;
13
+ displayItems: Signal<TItem[]>;
14
+ trackByFn: Signal<(item: TItem, index: number) => TKey>;
8
15
  }) {
9
16
  afterEveryRender(() => {
10
17
  const containerEl = options.domAccessor.getSelectRowIndicatorContainer();
11
18
 
12
- if (options.selectedItems().length <= 0) {
19
+ if (options.selectedKeys().length <= 0) {
13
20
  containerEl.innerHTML = "";
14
21
  containerEl.style.display = "none";
15
22
  return;
16
23
  }
17
24
 
18
- const selectedTrInfos = options.selectedItems()
19
- .map((item) => {
20
- const r = options.displayItems().indexOf(item);
25
+ const trackByFn = options.trackByFn();
26
+ const displayItems = options.displayItems();
27
+
28
+ const selectedTrInfos = options.selectedKeys()
29
+ .map((key) => {
30
+ const r = displayItems.findIndex((item, i) => isKeyEqual(trackByFn(item, i), key));
21
31
  return getTrInfo(r);
22
32
  })
23
33
  .filter((info): info is NonNullable<typeof info> => info != null);
@@ -55,7 +55,15 @@ import { SdEvents } from "../../core/events/sd-events";
55
55
  changeDetection: ChangeDetectionStrategy.OnPush,
56
56
  encapsulation: ViewEncapsulation.None,
57
57
  standalone: true,
58
- imports: [NgTemplateOutlet, SdCheckbox, NgIcon, SdPagination, SdAnchor, SdButton, SdResizeDirective],
58
+ imports: [
59
+ NgTemplateOutlet,
60
+ SdCheckbox,
61
+ NgIcon,
62
+ SdPagination,
63
+ SdAnchor,
64
+ SdButton,
65
+ SdResizeDirective,
66
+ ],
59
67
  hostDirectives: [
60
68
  { directive: SdEvents, outputs: ["keydown.capture", "focus.capture", "blur.capture"] },
61
69
  ],
@@ -69,7 +77,7 @@ import { SdEvents } from "../../core/events/sd-events";
69
77
  "(blur.capture)": "onBlurCapture($event)",
70
78
  },
71
79
  template: `
72
- @if ((key() || effectivePageCount() > 0) && !hideConfigBar()) {
80
+ @if ((key() || effectivePageCount() > 1) && !hideConfigBar()) {
73
81
  <div class="_tool flex-row gap-sm p-xs">
74
82
  @if (key()) {
75
83
  <sd-button [theme]="'link-primary'" [size]="'sm'" (click)="onConfigButtonClick()">
@@ -138,13 +146,13 @@ import { SdEvents } from "../../core/events/sd-events";
138
146
  @for (cell of row; track $index) {
139
147
  @if (!cell.isLastRow) {
140
148
  <th
141
- [class._fixed]="cell.colDef?.fixed"
149
+ [class._fixed]="cell.fixed"
142
150
  [attr.colspan]="cell.colspan > 1 ? cell.colspan : null"
143
151
  [attr.rowspan]="cell.rowspan > 1 ? cell.rowspan : null"
144
152
  [attr.data-c]="cell.colIndex"
145
153
  [attr.title]="cell.text"
146
154
  [style.left.px]="
147
- cell.colDef?.fixed ? fixing.fixedLeftMap().get(cell.colIndex) : null
155
+ cell.fixed ? fixing.fixedLeftMap().get(cell.colIndex) : null
148
156
  "
149
157
  >
150
158
  <div class="_p-sheet">
@@ -153,13 +161,14 @@ import { SdEvents } from "../../core/events/sd-events";
153
161
  </th>
154
162
  } @else {
155
163
  <th
156
- [class._fixed]="cell.colDef?.fixed"
164
+ [class._fixed]="cell.fixed"
157
165
  class="_last-depth"
158
166
  [class._sort]="cell.colDef && !cell.colDef.disableSorting"
159
167
  [attr.colspan]="cell.colspan > 1 ? cell.colspan : null"
160
168
  [attr.rowspan]="cell.rowspan > 1 ? cell.rowspan : null"
161
169
  [attr.data-c]="cell.colIndex"
162
170
  [attr.title]="cell.colDef?.tooltip ?? cell.text"
171
+ [attr.aria-sort]="getAriaSortValue(cell)"
163
172
  [class.help]="cell.colDef?.tooltip"
164
173
  [style]="getHeaderCellStyle(cell)"
165
174
  (sdResize)="onFixedCellResize($event, cell.colIndex)"
@@ -231,7 +240,11 @@ import { SdEvents } from "../../core/events/sd-events";
231
240
  }
232
241
  </thead>
233
242
  <tbody>
234
- @for (item of displayItems(); track trackByFn()(item, $index); let rowIdx = $index) {
243
+ @for (
244
+ item of displayItems();
245
+ track trackByFn()(item, $index) ?? item;
246
+ let rowIdx = $index
247
+ ) {
235
248
  <tr
236
249
  [attr.data-r]="rowIdx"
237
250
  (click)="onRowClick(item)"
@@ -254,15 +267,16 @@ import { SdEvents } from "../../core/events/sd-events";
254
267
  [inline]="true"
255
268
  [theme]="'white'"
256
269
  [disabled]="_selectable !== true"
257
- [attr.title]="_selectable"
270
+ [attr.title]="getSelectableTooltip(item)"
258
271
  />
259
272
  } @else if (selectMode() === "single") {
260
273
  @let _selectable = selection.getSelectable(item);
261
274
  @if (_selectable === true && selection.getCanChangeFn(item)) {
262
275
  <sd-anchor
263
276
  [theme]="selection.isSelected(item) ? 'primary' : 'gray'"
264
- (pointerdown)="selection.toggle(item)"
265
- [attr.title]="_selectable"
277
+ (pointerdown)="$event.stopPropagation(); selection.toggle(item)"
278
+ (click)="$event.stopPropagation()"
279
+ [attr.title]="getSelectableTooltip(item)"
266
280
  >
267
281
  <ng-icon [svg]="icons.tablerArrowRight" />
268
282
  </sd-anchor>
@@ -288,6 +302,7 @@ import { SdEvents } from "../../core/events/sd-events";
288
302
  [svg]="icons.tablerCaretRight"
289
303
  [style.transform]="isExpanded(item) ? 'rotate(90deg)' : undefined"
290
304
  [class.tx-theme-primary-default]="isExpanded(item)"
305
+ [attr.aria-expanded]="getAriaExpanded(item)"
291
306
  (click)="onExpandClick($event, item)"
292
307
  />
293
308
  }
@@ -567,15 +582,15 @@ import { SdEvents } from "../../core/events/sd-events";
567
582
  `,
568
583
  ],
569
584
  })
570
- export class SdSheet<T> {
585
+ export class SdSheet<TItem, TKey> {
571
586
  // Inputs
572
587
  key = input<string>();
573
- items = input<T[]>([]);
574
- trackByFn = input<(item: T, index: number) => unknown>((item) => item);
588
+ items = input<TItem[]>([]);
589
+ trackByFn = input.required<(item: TItem, index: number) => TKey>();
575
590
  selectMode = input<"single" | "multi">();
576
591
  autoSelect = input<"click" | "focus">();
577
- getItemSelectableFn = input<(item: T) => boolean | string>();
578
- getChildrenFn = input<(item: T, index: number) => T[] | undefined>();
592
+ getItemSelectableFn = input<(item: TItem) => boolean | string>();
593
+ getChildrenFn = input<(item: TItem, index: number) => TItem[] | undefined>();
579
594
  useAutoSort = input(false, { transform: booleanAttribute });
580
595
  visiblePageCount = input(10);
581
596
  totalPageCount = input(0);
@@ -583,26 +598,27 @@ export class SdSheet<T> {
583
598
  focusMode = input<"row" | "cell">("cell");
584
599
  inset = input(false, { transform: booleanAttribute });
585
600
  contentStyle = input<string>();
586
- getItemCellClassFn = input<(item: T, colKey: string) => string>();
587
- getItemCellStyleFn = input<(item: T, colKey: string) => string | undefined>();
601
+ getItemCellClassFn = input<(item: TItem, colKey: string) => string>();
602
+ getItemCellStyleFn = input<(item: TItem, colKey: string) => string | undefined>();
588
603
  hideConfigBar = input(false, { transform: booleanAttribute });
589
604
 
590
605
  // Outputs
591
- itemKeydown = output<SdSheetItemKeydownEventParam<T>>();
592
- cellKeydown = output<SdSheetCellKeydownEventParam<T>>();
606
+ itemKeydown = output<SdSheetItemKeydownEventParam<TItem>>();
607
+ cellKeydown = output<SdSheetCellKeydownEventParam<TItem>>();
593
608
 
594
609
  // Models
595
- selectedItems = model<T[]>([]);
596
- expandedItems = model<T[]>([]);
610
+ selectedKeys = model<NonNullable<TKey>[]>([]);
611
+ expandedItems = model<TItem[]>([]);
597
612
  sorts = model<SortingDef[]>([]); // Re-exported from useSortingManager
598
613
  currentPage = model(0);
599
614
 
600
615
  // Content query
601
616
  columnControls = contentChildren(SdSheetColumn);
602
- columnControlsInput = input<readonly SdSheetColumn[]>();
603
- private readonly _effectiveColumnControls = computed(
604
- () => this.columnControlsInput() ?? this.columnControls(),
605
- );
617
+ columnControlsInput = input<readonly SdSheetColumn[]>([]);
618
+ private readonly _effectiveColumnControls = computed(() => [
619
+ ...this.columnControls(),
620
+ ...this.columnControlsInput(),
621
+ ]);
606
622
 
607
623
  // Injected providers
608
624
  private readonly _sdModal = inject(SdModalProvider);
@@ -638,7 +654,7 @@ export class SdSheet<T> {
638
654
  });
639
655
 
640
656
  // Display pipeline (sort → page → expand → display)
641
- private readonly _pipeline = useSheetDisplayPipeline<T>({
657
+ private readonly _pipeline = useSheetDisplayPipeline<TItem>({
642
658
  items: this.items,
643
659
  useAutoSort: this.useAutoSort,
644
660
  sortItems: (items) => this.sorting.sort(items),
@@ -662,7 +678,7 @@ export class SdSheet<T> {
662
678
  });
663
679
 
664
680
  // Cell styling composable
665
- private readonly _styling = useSheetCellStyling<T>({
681
+ private readonly _styling = useSheetCellStyling<TItem>({
666
682
  columnDefs: this.layout.columnDefs,
667
683
  fixedLeftMap: this.fixing.fixedLeftMap,
668
684
  getItemCellStyleFn: this.getItemCellStyleFn,
@@ -683,16 +699,17 @@ export class SdSheet<T> {
683
699
  });
684
700
 
685
701
  // Select row indicator
686
- private readonly _selectRowIndicator = injectSheetSelectRowIndicator<T>({
702
+ private readonly _selectRowIndicator = injectSheetSelectRowIndicator<TItem, TKey>({
687
703
  domAccessor: this.domAccessor,
688
- selectedItems: this.selectedItems,
704
+ selectedKeys: this.selectedKeys,
689
705
  displayItems: this.displayItems,
706
+ trackByFn: this.trackByFn,
690
707
  });
691
708
 
692
709
  // Selection manager
693
- selection = useSelectionManager<T>({
710
+ selection = useSelectionManager<TItem, TKey>({
694
711
  displayItems: this.displayItems,
695
- selectedItems: this.selectedItems,
712
+ selectedKeys: this.selectedKeys,
696
713
  selectMode: this.selectMode,
697
714
  getItemSelectableFn: this.getItemSelectableFn,
698
715
  trackByFn: this.trackByFn,
@@ -731,25 +748,25 @@ export class SdSheet<T> {
731
748
  return col?.summaryTplRef() ?? null;
732
749
  }
733
750
 
734
- getSelectableTooltip(item: T): string | null {
751
+ getSelectableTooltip(item: TItem): string | undefined {
735
752
  const result = this.selection.getSelectable(item);
736
753
  if (typeof result === "string") return result;
737
- return null;
754
+ return undefined;
738
755
  }
739
756
 
740
- onRowClick(item: T): void {
757
+ onRowClick(item: TItem): void {
741
758
  if (this.autoSelect() === "click") {
742
759
  this.selection.select(item);
743
760
  }
744
761
  }
745
762
 
746
- onCellClick(event: Event, item: T): void {
763
+ onCellClick(event: Event, item: TItem): void {
747
764
  if (this.autoSelect() === "click") {
748
765
  this.selection.select(item);
749
766
  }
750
767
  }
751
768
 
752
- onCellFocus(item: T): void {
769
+ onCellFocus(item: TItem): void {
753
770
  if (this.autoSelect() === "focus") {
754
771
  this.selection.select(item);
755
772
  }
@@ -766,25 +783,25 @@ export class SdSheet<T> {
766
783
  return this.sorting.defMap().get(key) ?? null;
767
784
  }
768
785
 
769
- getItemDef(item: T) {
786
+ getItemDef(item: TItem) {
770
787
  return this.expanding.def(item);
771
788
  }
772
789
 
773
790
  // PERF-005: Set-based lookup for O(1) isExpanded check
774
791
  private readonly _expandedSet = computed(() => new Set(this.expandedItems()));
775
792
 
776
- isExpanded(item: T): boolean {
793
+ isExpanded(item: TItem): boolean {
777
794
  return this._expandedSet().has(item);
778
795
  }
779
796
 
780
- getAriaExpanded(item: T): string | null {
797
+ getAriaExpanded(item: TItem): string | null {
781
798
  if (this.getChildrenFn() == null) return null;
782
799
  const def = this.getItemDef(item);
783
800
  if (!def.hasChildren) return null;
784
801
  return this.isExpanded(item) ? "true" : "false";
785
802
  }
786
803
 
787
- onExpandClick(event: Event, item: T): void {
804
+ onExpandClick(event: Event, item: TItem): void {
788
805
  event.stopPropagation();
789
806
  this.expanding.toggle(item);
790
807
  }
@@ -819,7 +836,7 @@ export class SdSheet<T> {
819
836
  }
820
837
 
821
838
  onTableResize(event: SdResizeEvent): void {
822
- if (!event.widthChanged) return;
839
+ if (!event.widthChanged && !event.heightChanged) return;
823
840
  this._focusIndicator.redraw();
824
841
  this._selectRowIndicator.redraw();
825
842
  }
@@ -888,11 +905,11 @@ export class SdSheet<T> {
888
905
  );
889
906
  }
890
907
 
891
- onItemKeydown(event: KeyboardEvent, item: T): void {
908
+ onItemKeydown(event: KeyboardEvent, item: TItem): void {
892
909
  this.itemKeydown.emit({ item, event });
893
910
  }
894
911
 
895
- onCellKeydown(event: KeyboardEvent, item: T, colKey: string): void {
912
+ onCellKeydown(event: KeyboardEvent, item: TItem, colKey: string): void {
896
913
  this.cellKeydown.emit({ item, key: colKey, event });
897
914
  }
898
915