@simplysm/angular 14.0.10 → 14.0.12

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 (170) hide show
  1. package/README.md +8 -5
  2. package/dist/core/directives/sd-router-link.directive.js +2 -2
  3. package/dist/core/pipes/format.pipe.d.ts.map +1 -1
  4. package/dist/core/pipes/format.pipe.js +2 -0
  5. package/dist/core/plugins/commands/findTopOpenModalEl.d.ts +1 -1
  6. package/dist/core/plugins/commands/findTopOpenModalEl.d.ts.map +1 -1
  7. package/dist/core/plugins/commands/findTopOpenModalEl.js +2 -2
  8. package/dist/core/plugins/commands/sd-insert-command-event.plugin.d.ts +1 -0
  9. package/dist/core/plugins/commands/sd-insert-command-event.plugin.d.ts.map +1 -1
  10. package/dist/core/plugins/commands/sd-insert-command-event.plugin.js +4 -3
  11. package/dist/core/plugins/commands/sd-refresh-command-event.plugin.d.ts +1 -0
  12. package/dist/core/plugins/commands/sd-refresh-command-event.plugin.d.ts.map +1 -1
  13. package/dist/core/plugins/commands/sd-refresh-command-event.plugin.js +4 -3
  14. package/dist/core/plugins/commands/sd-save-command-event.plugin.d.ts +1 -0
  15. package/dist/core/plugins/commands/sd-save-command-event.plugin.d.ts.map +1 -1
  16. package/dist/core/plugins/commands/sd-save-command-event.plugin.js +4 -3
  17. package/dist/core/plugins/events/sd-intersection-event.plugin.d.ts.map +1 -1
  18. package/dist/core/plugins/events/sd-intersection-event.plugin.js +2 -3
  19. package/dist/core/plugins/events/sd-option-event.plugin.d.ts +1 -0
  20. package/dist/core/plugins/events/sd-option-event.plugin.d.ts.map +1 -1
  21. package/dist/core/plugins/events/sd-option-event.plugin.js +2 -1
  22. package/dist/core/plugins/sd-global-error-handler.plugin.d.ts.map +1 -1
  23. package/dist/core/plugins/sd-global-error-handler.plugin.js +3 -1
  24. package/dist/core/provideSdAngular.d.ts.map +1 -1
  25. package/dist/core/provideSdAngular.js +14 -4
  26. package/dist/core/providers/sd-app-structure.provider.d.ts.map +1 -1
  27. package/dist/core/providers/sd-app-structure.provider.js +8 -7
  28. package/dist/core/providers/sd-file-dialog.provider.d.ts.map +1 -1
  29. package/dist/core/providers/sd-file-dialog.provider.js +15 -7
  30. package/dist/core/providers/sd-local-storage.provider.d.ts.map +1 -1
  31. package/dist/core/providers/sd-local-storage.provider.js +6 -1
  32. package/dist/core/providers/sd-navigate-window.provider.js +4 -1
  33. package/dist/core/providers/sd-print.provider.js +2 -2
  34. package/dist/core/providers/sd-shared-data.provider.d.ts.map +1 -1
  35. package/dist/core/providers/sd-shared-data.provider.js +14 -3
  36. package/dist/core/utils/injectParent.js +9 -5
  37. package/dist/core/utils/setups/setupModelHook.d.ts.map +1 -1
  38. package/dist/core/utils/setups/setupModelHook.js +4 -1
  39. package/dist/core/utils/setups/setupRevealOnShow.d.ts.map +1 -1
  40. package/dist/core/utils/setups/setupRevealOnShow.js +3 -2
  41. package/dist/core/utils/useExpandingManager.d.ts.map +1 -1
  42. package/dist/core/utils/useExpandingManager.js +3 -2
  43. package/dist/core/utils/useSdSystemConfigResource.d.ts +1 -1
  44. package/dist/core/utils/useSdSystemConfigResource.d.ts.map +1 -1
  45. package/dist/core/utils/useSdSystemConfigResource.js +13 -12
  46. package/dist/core/utils/withBusy.d.ts +3 -0
  47. package/dist/core/utils/withBusy.d.ts.map +1 -0
  48. package/dist/core/utils/withBusy.js +9 -0
  49. package/dist/features/address/sd-address-search.modal.d.ts.map +1 -1
  50. package/dist/features/address/sd-address-search.modal.js +5 -2
  51. package/dist/features/data-view/sd-data-detail.control.d.ts +7 -4
  52. package/dist/features/data-view/sd-data-detail.control.d.ts.map +1 -1
  53. package/dist/features/data-view/sd-data-detail.control.js +75 -68
  54. package/dist/features/data-view/sd-data-sheet.control.d.ts +2 -0
  55. package/dist/features/data-view/sd-data-sheet.control.d.ts.map +1 -1
  56. package/dist/features/data-view/sd-data-sheet.control.js +134 -110
  57. package/dist/features/permission-table/sd-permission-table.control.d.ts +32 -0
  58. package/dist/features/permission-table/sd-permission-table.control.d.ts.map +1 -0
  59. package/dist/features/permission-table/sd-permission-table.control.js +467 -0
  60. package/dist/features/shared-data/matchesSearchText.d.ts +2 -0
  61. package/dist/features/shared-data/matchesSearchText.d.ts.map +1 -0
  62. package/dist/features/shared-data/matchesSearchText.js +11 -0
  63. package/dist/features/shared-data/sd-shared-data-select-button.control.d.ts +2 -2
  64. package/dist/features/shared-data/sd-shared-data-select-button.control.d.ts.map +1 -1
  65. package/dist/features/shared-data/sd-shared-data-select-list.control.d.ts +1 -0
  66. package/dist/features/shared-data/sd-shared-data-select-list.control.d.ts.map +1 -1
  67. package/dist/features/shared-data/sd-shared-data-select-list.control.js +16 -11
  68. package/dist/features/shared-data/sd-shared-data-select.control.d.ts +2 -1
  69. package/dist/features/shared-data/sd-shared-data-select.control.d.ts.map +1 -1
  70. package/dist/features/shared-data/sd-shared-data-select.control.js +45 -30
  71. package/dist/index.d.ts +5 -3
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +5 -2
  74. package/dist/ui/data/sheet/sd-sheet.control.d.ts +5 -2
  75. package/dist/ui/data/sheet/sd-sheet.control.d.ts.map +1 -1
  76. package/dist/ui/data/sheet/sd-sheet.control.js +48 -47
  77. package/dist/ui/data/sheet/types.d.ts +5 -1
  78. package/dist/ui/data/sheet/types.d.ts.map +1 -1
  79. package/dist/ui/data/sheet/useSheetCellAgent.d.ts.map +1 -1
  80. package/dist/ui/data/sheet/useSheetCellAgent.js +8 -4
  81. package/dist/ui/data/sheet/useSheetColumnFixing.d.ts +6 -0
  82. package/dist/ui/data/sheet/useSheetColumnFixing.d.ts.map +1 -1
  83. package/dist/ui/data/sheet/useSheetColumnFixing.js +6 -0
  84. package/dist/ui/form/editor/sd-tiptap-editor.control.d.ts +2 -2
  85. package/dist/ui/form/editor/sd-tiptap-editor.control.d.ts.map +1 -1
  86. package/dist/ui/form/editor/sd-tiptap-editor.control.js +15 -13
  87. package/dist/ui/form/input/sd-date-range.picker.d.ts.map +1 -1
  88. package/dist/ui/form/input/sd-date-range.picker.js +8 -1
  89. package/dist/ui/form/select/sd-select.control.d.ts.map +1 -1
  90. package/dist/ui/form/select/sd-select.control.js +20 -16
  91. package/dist/ui/layout/dock/sd-dock.control.js +4 -4
  92. package/dist/ui/navigation/pagination/sd-pagination.control.js +1 -1
  93. package/dist/ui/navigation/sidebar/sd-sidebar-menu.control.d.ts +7 -13
  94. package/dist/ui/navigation/sidebar/sd-sidebar-menu.control.d.ts.map +1 -1
  95. package/dist/ui/navigation/sidebar/sd-sidebar-menu.control.js +1 -1
  96. package/dist/ui/navigation/topbar/sd-topbar-menu.control.d.ts +7 -13
  97. package/dist/ui/navigation/topbar/sd-topbar-menu.control.d.ts.map +1 -1
  98. package/dist/ui/navigation/topbar/sd-topbar-menu.control.js +1 -1
  99. package/dist/ui/overlay/dropdown/sd-dropdown-popup.control.d.ts +0 -3
  100. package/dist/ui/overlay/dropdown/sd-dropdown-popup.control.d.ts.map +1 -1
  101. package/dist/ui/overlay/dropdown/sd-dropdown-popup.control.js +8 -21
  102. package/dist/ui/overlay/dropdown/sd-dropdown.control.d.ts +1 -1
  103. package/dist/ui/overlay/dropdown/sd-dropdown.control.d.ts.map +1 -1
  104. package/dist/ui/overlay/dropdown/sd-dropdown.control.js +19 -20
  105. package/dist/ui/overlay/modal/sd-modal.control.d.ts.map +1 -1
  106. package/dist/ui/overlay/modal/sd-modal.control.js +21 -5
  107. package/dist/ui/overlay/modal/sd-modal.provider.d.ts.map +1 -1
  108. package/dist/ui/overlay/modal/sd-modal.provider.js +12 -0
  109. package/dist/ui/overlay/toast/sd-toast.provider.d.ts +1 -0
  110. package/dist/ui/overlay/toast/sd-toast.provider.d.ts.map +1 -1
  111. package/dist/ui/overlay/toast/sd-toast.provider.js +16 -2
  112. package/dist/ui/visual/sd-barcode.control.d.ts +2 -2
  113. package/dist/ui/visual/sd-barcode.control.d.ts.map +1 -1
  114. package/dist/ui/visual/sd-barcode.control.js +25 -18
  115. package/dist/ui/visual/sd-progress.control.d.ts.map +1 -1
  116. package/dist/ui/visual/sd-progress.control.js +1 -1
  117. package/docs/core.md +19 -0
  118. package/docs/features.md +25 -0
  119. package/docs/ui-data.md +20 -4
  120. package/docs/ui-navigation.md +8 -44
  121. package/package.json +25 -25
  122. package/src/core/directives/sd-router-link.directive.ts +1 -1
  123. package/src/core/pipes/format.pipe.ts +1 -0
  124. package/src/core/plugins/commands/findTopOpenModalEl.ts +2 -2
  125. package/src/core/plugins/commands/sd-insert-command-event.plugin.ts +5 -3
  126. package/src/core/plugins/commands/sd-refresh-command-event.plugin.ts +5 -3
  127. package/src/core/plugins/commands/sd-save-command-event.plugin.ts +5 -3
  128. package/src/core/plugins/events/sd-intersection-event.plugin.ts +2 -3
  129. package/src/core/plugins/events/sd-option-event.plugin.ts +3 -1
  130. package/src/core/plugins/sd-global-error-handler.plugin.ts +4 -1
  131. package/src/core/provideSdAngular.ts +17 -4
  132. package/src/core/providers/sd-app-structure.provider.ts +7 -7
  133. package/src/core/providers/sd-file-dialog.provider.ts +18 -9
  134. package/src/core/providers/sd-local-storage.provider.ts +5 -1
  135. package/src/core/providers/sd-navigate-window.provider.ts +3 -3
  136. package/src/core/providers/sd-print.provider.ts +2 -2
  137. package/src/core/providers/sd-shared-data.provider.ts +14 -3
  138. package/src/core/utils/injectParent.ts +10 -6
  139. package/src/core/utils/setups/setupModelHook.ts +6 -1
  140. package/src/core/utils/setups/setupRevealOnShow.ts +3 -2
  141. package/src/core/utils/useExpandingManager.ts +4 -2
  142. package/src/core/utils/useSdSystemConfigResource.ts +13 -11
  143. package/src/core/utils/withBusy.ts +13 -0
  144. package/src/features/address/sd-address-search.modal.ts +5 -2
  145. package/src/features/data-view/sd-data-detail.control.ts +74 -51
  146. package/src/features/data-view/sd-data-sheet.control.ts +84 -60
  147. package/src/features/permission-table/sd-permission-table.control.ts +461 -0
  148. package/src/features/shared-data/matchesSearchText.ts +16 -0
  149. package/src/features/shared-data/sd-shared-data-select-button.control.ts +4 -4
  150. package/src/features/shared-data/sd-shared-data-select-list.control.ts +19 -11
  151. package/src/features/shared-data/sd-shared-data-select.control.ts +51 -31
  152. package/src/index.ts +7 -8
  153. package/src/ui/data/sheet/sd-sheet.control.ts +51 -48
  154. package/src/ui/data/sheet/types.ts +6 -1
  155. package/src/ui/data/sheet/useSheetCellAgent.ts +5 -3
  156. package/src/ui/data/sheet/useSheetColumnFixing.ts +6 -0
  157. package/src/ui/form/editor/sd-tiptap-editor.control.ts +14 -12
  158. package/src/ui/form/input/sd-date-range.picker.ts +7 -1
  159. package/src/ui/form/select/sd-select.control.ts +18 -14
  160. package/src/ui/layout/dock/sd-dock.control.ts +4 -4
  161. package/src/ui/navigation/pagination/sd-pagination.control.ts +1 -1
  162. package/src/ui/navigation/sidebar/sd-sidebar-menu.control.ts +7 -14
  163. package/src/ui/navigation/topbar/sd-topbar-menu.control.ts +7 -14
  164. package/src/ui/overlay/dropdown/sd-dropdown-popup.control.ts +2 -17
  165. package/src/ui/overlay/dropdown/sd-dropdown.control.ts +19 -19
  166. package/src/ui/overlay/modal/sd-modal.control.ts +21 -5
  167. package/src/ui/overlay/modal/sd-modal.provider.ts +13 -0
  168. package/src/ui/overlay/toast/sd-toast.provider.ts +14 -1
  169. package/src/ui/visual/sd-barcode.control.ts +18 -16
  170. package/src/ui/visual/sd-progress.control.ts +1 -1
@@ -0,0 +1,16 @@
1
+ export function matchesSearchText(
2
+ itemText: string,
3
+ searchQuery: string | undefined,
4
+ ): boolean {
5
+ const terms =
6
+ searchQuery
7
+ ?.trim()
8
+ .split(" ")
9
+ .map((t) => t.trim())
10
+ .filter((t) => t !== "") ?? [];
11
+
12
+ if (terms.length === 0) return true;
13
+
14
+ const lowerItemText = itemText.toLowerCase();
15
+ return terms.every((term) => lowerItemText.includes(term.toLowerCase()));
16
+ }
@@ -46,10 +46,10 @@ import type { TSelectModeValue } from "../../ui/form/select/sd-select.control";
46
46
  `,
47
47
  })
48
48
  export class SdSharedDataSelectButtonControl<
49
- TItem extends ISharedDataBase<number>,
50
- TMode extends keyof TSelectModeValue<number>,
49
+ TItem extends ISharedDataBase<string | number>,
50
+ TMode extends keyof TSelectModeValue<string | number>,
51
51
  TModal extends ISdSelectModal<any>,
52
- > extends AbsSdDataSelectButton<TItem, number, TMode> {
52
+ > extends AbsSdDataSelectButton<TItem, string | number, TMode> {
53
53
  items = input<TItem[]>([]);
54
54
  modal = input.required<TSdSelectModalInfo<TModal>>();
55
55
 
@@ -58,7 +58,7 @@ export class SdSharedDataSelectButtonControl<
58
58
  { read: TemplateRef },
59
59
  );
60
60
 
61
- override load(keys: number[]): TItem[] {
61
+ override load(keys: (string | number)[]): TItem[] {
62
62
  return this.items().filter((item) => keys.includes(item.__valueKey));
63
63
  }
64
64
  }
@@ -15,6 +15,7 @@ import {
15
15
  ViewEncapsulation,
16
16
  } from "@angular/core";
17
17
  import { str } from "@simplysm/core-common";
18
+ import { matchesSearchText } from "./matchesSearchText";
18
19
  import type { ISharedDataBase } from "../../core/providers/sd-shared-data.provider";
19
20
  import {
20
21
  SdItemOfTemplateDirective,
@@ -163,20 +164,13 @@ export class SdSharedDataSelectListControl<
163
164
 
164
165
  pageItemCount = input<number>();
165
166
  page = signal(0);
166
- pageLength = computed(() => {
167
- const pic = this.pageItemCount();
168
- if (pic != null && pic > 0) {
169
- return Math.ceil(this.items().length / pic);
170
- }
171
- return 0;
172
- });
173
167
 
174
- displayItems = computed(() => {
168
+ private readonly _filteredItems = computed(() => {
175
169
  let result = this.items().filter((item) => !item.__isHidden);
176
170
 
177
171
  if (!str.isNullOrEmpty(this.searchText())) {
178
172
  result = result.filter((item) =>
179
- item.__searchText.includes(this.searchText()!),
173
+ matchesSearchText(item.__searchText, this.searchText()),
180
174
  );
181
175
  }
182
176
 
@@ -184,12 +178,26 @@ export class SdSharedDataSelectListControl<
184
178
  result = result.filter((item, i) => this.filterFn()!(item, i));
185
179
  }
186
180
 
181
+ return result;
182
+ });
183
+
184
+ pageLength = computed(() => {
185
+ const pic = this.pageItemCount();
186
+ if (pic != null && pic > 0) {
187
+ return Math.ceil(this._filteredItems().length / pic);
188
+ }
189
+ return 0;
190
+ });
191
+
192
+ displayItems = computed(() => {
193
+ const filtered = this._filteredItems();
194
+
187
195
  const pic = this.pageItemCount();
188
196
  if (pic != null && pic > 0) {
189
- result = result.slice(pic * this.page(), pic * (this.page() + 1));
197
+ return filtered.slice(pic * this.page(), pic * (this.page() + 1));
190
198
  }
191
199
 
192
- return result;
200
+ return filtered;
193
201
  });
194
202
 
195
203
  constructor() {
@@ -35,6 +35,7 @@ import type {
35
35
  } from "../../ui/form/button/sd-modal-select-button.control";
36
36
  import { NgIcon } from "@ng-icons/core";
37
37
  import { tablerEdit, tablerSearch } from "@ng-icons/tabler-icons";
38
+ import { matchesSearchText } from "./matchesSearchText";
38
39
 
39
40
  @Component({
40
41
  selector: "sd-shared-data-select",
@@ -235,6 +236,46 @@ export class SdSharedDataSelectControl<
235
236
  return result;
236
237
  });
237
238
 
239
+ private readonly _searchTextMatchCache = computed(() => {
240
+ const cache = new Map<TItem["__valueKey"], boolean>();
241
+ const searchText = this.searchText();
242
+ const getSearchTextFn = this.getSearchTextFn();
243
+ const hasParent = this.hasParentKey();
244
+ const parentMap = this.itemByParentKeyMap();
245
+ const items = this.items();
246
+
247
+ const check = (item: TItem, index: number): boolean => {
248
+ const key = item.__valueKey;
249
+ const cached = cache.get(key);
250
+ if (cached !== undefined) return cached;
251
+
252
+ const itemText = getSearchTextFn(item, index);
253
+ if (matchesSearchText(itemText, searchText)) {
254
+ cache.set(key, true);
255
+ return true;
256
+ }
257
+
258
+ if (hasParent && parentMap != null) {
259
+ const children = parentMap.get(key as TItem["__valueKey"]) ?? [];
260
+ for (let i = 0; i < children.length; i++) {
261
+ if (check(children[i], i)) {
262
+ cache.set(key, true);
263
+ return true;
264
+ }
265
+ }
266
+ }
267
+
268
+ cache.set(key, false);
269
+ return false;
270
+ };
271
+
272
+ for (let i = 0; i < items.length; i++) {
273
+ check(items[i], i);
274
+ }
275
+
276
+ return cache;
277
+ });
278
+
238
279
  selectedKeys = computed((): any[] => {
239
280
  const val = this.value();
240
281
  if (val == null) return [];
@@ -243,19 +284,22 @@ export class SdSharedDataSelectControl<
243
284
  });
244
285
 
245
286
  constructor() {
246
- // 드롭다운 닫힘 시 검색어 초기화
287
+ // 드롭다운 닫힘 시 검색어 초기화 (open → closed 전환 시에만)
288
+ let prevOpen = false;
247
289
  effect(() => {
248
290
  const ctrl = this._selectCtrl();
249
- if (ctrl != null) {
250
- ctrl.dropdownOpen();
291
+ const currentOpen = ctrl != null ? ctrl.dropdownOpen() : false;
292
+
293
+ if (prevOpen && !currentOpen) {
294
+ untracked(() => this.searchText.set(undefined));
251
295
  }
252
- untracked(() => this.searchText.set(undefined));
296
+ prevOpen = currentOpen;
253
297
  });
254
298
  }
255
299
 
256
300
  getItemSelectable(item: TItem, _index: number, depth: number): boolean {
257
301
  if (!this.hasParentKey()) return true;
258
- // depth가 0이면서 자식을 가진 항목(카테고리)은 선택 불가
302
+ // 트리 구조에서 depth가 0이면서 __parentKey가 있는 항목은 선택 불가
259
303
  return depth !== 0 || item.__parentKey == null;
260
304
  }
261
305
 
@@ -273,32 +317,8 @@ export class SdSharedDataSelectControl<
273
317
  return false;
274
318
  }
275
319
 
276
- isIncludeSearchText(item: TItem, index: number): boolean {
277
- const splitSearchTexts =
278
- this.searchText()
279
- ?.trim()
280
- .split(" ")
281
- .map((t) => t.trim())
282
- .filter((t) => t !== "") ?? [];
283
-
284
- if (splitSearchTexts.length === 0) return true;
285
-
286
- const itemText = this.getSearchTextFn()(item, index);
287
- for (const term of splitSearchTexts) {
288
- if (!itemText.toLowerCase().includes(term.toLowerCase())) {
289
- // 트리 구조에서 자식 중 매칭 항목 확인
290
- if (this.hasParentKey()) {
291
- const children = this.getChildren(item);
292
- for (let i = 0; i < children.length; i++) {
293
- if (this.isIncludeSearchText(children[i], i)) {
294
- return true;
295
- }
296
- }
297
- }
298
- return false;
299
- }
300
- }
301
- return true;
320
+ isIncludeSearchText(item: TItem, _index: number): boolean {
321
+ return this._searchTextMatchCache().get(item.__valueKey) ?? false;
302
322
  }
303
323
 
304
324
  getChildren = (item: ISharedDataBase<string | number>): TItem[] => {
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  } from "./core/utils/useExpandingManager";
66
66
  export { useSelectionManager } from "./core/utils/useSelectionManager";
67
67
  export { injectParent } from "./core/utils/injectParent";
68
+ export { withBusy } from "./core/utils/withBusy";
68
69
 
69
70
  // features/address
70
71
  export {
@@ -72,6 +73,9 @@ export {
72
73
  type IAddress,
73
74
  } from "./features/address/sd-address-search.modal";
74
75
 
76
+ // features/permission-table
77
+ export { SdPermissionTableControl } from "./features/permission-table/sd-permission-table.control";
78
+
75
79
  // features
76
80
  export { SdBaseContainerControl } from "./features/base/sd-base-container.control";
77
81
  export {
@@ -172,10 +176,7 @@ export { SdPaginationControl } from "./ui/navigation/pagination/sd-pagination.co
172
176
  // ui/navigation/sidebar
173
177
  export { SdSidebarContainerControl } from "./ui/navigation/sidebar/sd-sidebar-container.control";
174
178
  export { SdSidebarControl } from "./ui/navigation/sidebar/sd-sidebar.control";
175
- export {
176
- SdSidebarMenuControl,
177
- type ISdSidebarMenu,
178
- } from "./ui/navigation/sidebar/sd-sidebar-menu.control";
179
+ export { SdSidebarMenuControl } from "./ui/navigation/sidebar/sd-sidebar-menu.control";
179
180
  export {
180
181
  SdSidebarUserControl,
181
182
  type ISidebarUserMenu,
@@ -184,10 +185,7 @@ export {
184
185
  // ui/navigation/topbar
185
186
  export { SdTopbarContainerControl } from "./ui/navigation/topbar/sd-topbar-container.control";
186
187
  export { SdTopbarControl } from "./ui/navigation/topbar/sd-topbar.control";
187
- export {
188
- SdTopbarMenuControl,
189
- type ISdTopbarMenu,
190
- } from "./ui/navigation/topbar/sd-topbar-menu.control";
188
+ export { SdTopbarMenuControl } from "./ui/navigation/topbar/sd-topbar-menu.control";
191
189
  export {
192
190
  SdTopbarUserControl,
193
191
  type ISdTopbarUserMenu,
@@ -206,6 +204,7 @@ export type {
206
204
  ISdSheetConfig,
207
205
  ISdSheetHeaderDef,
208
206
  ISdSheetItemKeydownEventParam,
207
+ ISdSheetCellKeydownEventParam,
209
208
  } from "./ui/data/sheet/types";
210
209
 
211
210
  // ui/visual
@@ -35,6 +35,7 @@ import { SdAnchorControl } from "../../form/button/sd-anchor.control";
35
35
  import { SdButtonControl } from "../../form/button/sd-button.control";
36
36
  import type {
37
37
  ISdSheetColumnDef,
38
+ ISdSheetCellKeydownEventParam,
38
39
  ISdSheetConfig,
39
40
  ISdSheetHeaderDef,
40
41
  ISdSheetItemKeydownEventParam,
@@ -356,7 +357,7 @@ export class SdSheetControl<T> {
356
357
 
357
358
  // Outputs
358
359
  itemKeydown = output<ISdSheetItemKeydownEventParam<T>>();
359
- cellKeydown = output<ISdSheetItemKeydownEventParam<T>>();
360
+ cellKeydown = output<ISdSheetCellKeydownEventParam<T>>();
360
361
 
361
362
  // Models
362
363
  selectedItems = model<T[]>([]);
@@ -496,41 +497,49 @@ export class SdSheetControl<T> {
496
497
  return col?.summaryTplRef() ?? null;
497
498
  }
498
499
 
499
- getHeaderCellStyle(cell: ISdSheetHeaderDef) {
500
- const parts: string[] = [];
501
- if (cell.colDef != null) {
502
- const baseStyle = this._getColDefStyle(cell.colDef);
503
- if (baseStyle != null) {
504
- parts.push(baseStyle);
505
- }
506
- const fixedStyle = this._getFixedStyle(cell.colDef);
507
- if (fixedStyle != null) {
508
- parts.push(fixedStyle);
509
- }
500
+ // Pre-computed column styles: header/footer (fixed z-index:3)
501
+ private readonly _headerColumnStyles = computed(() => {
502
+ const map = new Map<string, string | null>();
503
+ for (const colDef of this.layout.columnDefs()) {
504
+ const parts: string[] = [];
505
+ const colStyle = this._getColDefStyle(colDef);
506
+ if (colStyle != null) parts.push(colStyle);
507
+ const fixedStyle = this._getFixedStyle(colDef, 3, "var(--theme-secondary-lightest)");
508
+ if (fixedStyle != null) parts.push(fixedStyle);
509
+ map.set(colDef.key, parts.length > 0 ? parts.join("; ") : null);
510
510
  }
511
- return parts.length > 0 ? parts.join("; ") : null;
511
+ return map;
512
+ });
513
+
514
+ // Pre-computed column styles: body (fixed z-index:1)
515
+ private readonly _dataColumnBaseStyles = computed(() => {
516
+ const map = new Map<string, string | null>();
517
+ for (const colDef of this.layout.columnDefs()) {
518
+ const parts: string[] = [];
519
+ const colStyle = this._getColDefStyle(colDef);
520
+ if (colStyle != null) parts.push(colStyle);
521
+ const fixedStyle = this._getFixedStyle(colDef);
522
+ if (fixedStyle != null) parts.push(fixedStyle);
523
+ map.set(colDef.key, parts.length > 0 ? parts.join("; ") : null);
524
+ }
525
+ return map;
526
+ });
527
+
528
+ getHeaderCellStyle(cell: ISdSheetHeaderDef) {
529
+ if (cell.colDef == null) return null;
530
+ return this._headerColumnStyles().get(cell.colDef.key) ?? null;
512
531
  }
513
532
 
514
533
  getCellStyle(item: T, colDef: ISdSheetColumnDef) {
515
- const parts: string[] = [];
516
- const baseStyle = this._getColDefStyle(colDef);
517
- if (baseStyle != null) {
518
- parts.push(baseStyle);
519
- }
520
- const fixedStyle = this._getFixedStyle(colDef);
521
- if (fixedStyle != null) {
522
- parts.push(fixedStyle);
523
- }
534
+ const baseStyle = this._dataColumnBaseStyles().get(colDef.key) ?? null;
524
535
  const styleFn = this.getItemCellStyleFn();
525
536
  const customStyle = styleFn != null ? styleFn(item, colDef.key) : undefined;
526
- if (customStyle != null) {
527
- parts.push(customStyle);
528
- }
529
- return parts.length > 0 ? parts.join("; ") : null;
537
+ if (baseStyle != null && customStyle != null) return `${baseStyle}; ${customStyle}`;
538
+ return customStyle ?? baseStyle ?? null;
530
539
  }
531
540
 
532
541
  getFixedCellStyle(colDef: ISdSheetColumnDef) {
533
- return this._getFixedStyle(colDef);
542
+ return this._getFixedStyle(colDef, 3);
534
543
  }
535
544
 
536
545
  getSelectableTooltip(item: T): string | null {
@@ -593,8 +602,11 @@ export class SdSheetControl<T> {
593
602
  return this.expanding.def(item);
594
603
  }
595
604
 
605
+ // PERF-005: Set-based lookup for O(1) isExpanded check
606
+ private readonly _expandedSet = computed(() => new Set(this.expandedItems()));
607
+
596
608
  isExpanded(item: T): boolean {
597
- return this.expandedItems().includes(item);
609
+ return this._expandedSet().has(item);
598
610
  }
599
611
 
600
612
  getAriaExpanded(item: T): string | null {
@@ -617,34 +629,25 @@ export class SdSheetControl<T> {
617
629
  }
618
630
 
619
631
  private _getColDefStyle(colDef: { width: string | undefined; collapse: boolean }): string | null {
620
- const parts: string[] = [];
621
- if (colDef.width != null) {
622
- parts.push(`width: ${colDef.width}`);
623
- parts.push(`min-width: ${colDef.width}`);
624
- parts.push(`max-width: ${colDef.width}`);
625
- }
626
632
  if (colDef.collapse) {
627
- parts.push("padding: 0");
628
- parts.push("width: 0");
629
- parts.push("min-width: 0");
630
- parts.push("max-width: 0");
631
- parts.push("overflow: hidden");
632
- parts.push("border: none");
633
+ return "padding: 0; width: 0; min-width: 0; max-width: 0; overflow: hidden; border: none";
633
634
  }
634
- return parts.length > 0 ? parts.join("; ") : null;
635
+ if (colDef.width != null) {
636
+ return `width: ${colDef.width}; min-width: ${colDef.width}; max-width: ${colDef.width}`;
637
+ }
638
+ return null;
635
639
  }
636
640
 
637
- private _getFixedStyle(colDef: ISdSheetColumnDef): string | null {
641
+ private _getFixedStyle(
642
+ colDef: ISdSheetColumnDef,
643
+ zIndex: number = 1,
644
+ background: string = "var(--control-color)",
645
+ ): string | null {
638
646
  const fixedLeftMap = this.fixing.fixedLeftMap();
639
647
  const leftValue = fixedLeftMap.get(colDef.key);
640
648
  if (leftValue == null) return null;
641
649
 
642
- const parts: string[] = [];
643
- parts.push("position: sticky");
644
- parts.push(`left: ${leftValue}px`);
645
- parts.push("z-index: 1");
646
- parts.push("background: var(--control-color)");
647
- return parts.join("; ");
650
+ return `position: sticky; left: ${leftValue}px; z-index: ${zIndex}; background: ${background}`;
648
651
  }
649
652
 
650
653
  getDataCellClass(item: T, colDef: ISdSheetColumnDef, r: number, c: number): string | null {
@@ -32,6 +32,11 @@ export interface ISdSheetConfig {
32
32
 
33
33
  export interface ISdSheetItemKeydownEventParam<T> {
34
34
  item: T;
35
- key?: string;
35
+ event: KeyboardEvent;
36
+ }
37
+
38
+ export interface ISdSheetCellKeydownEventParam<T> {
39
+ item: T;
40
+ key: string;
36
41
  event: KeyboardEvent;
37
42
  }
@@ -20,11 +20,11 @@ export function useSheetCellAgent(options: {
20
20
  }
21
21
 
22
22
  function _enterEditMode(r: number, c: number): void {
23
- const cell = options.domAccessor.getCell(r, c);
24
- if (cell == null) return;
25
23
  editModeCellAddr.set({ r, c });
26
- // Focus first focusable child after edit mode is set
24
+ // Re-query DOM inside queueMicrotask to avoid stale reference after Angular re-render
27
25
  queueMicrotask(() => {
26
+ const cell = options.domAccessor.getCell(r, c);
27
+ if (cell == null) return;
28
28
  const focusable = cell.findFirstFocusableChild();
29
29
  if (focusable !== undefined) {
30
30
  focusable.focus();
@@ -181,6 +181,7 @@ export function useSheetCellAgent(options: {
181
181
 
182
182
  // Ctrl+C (copy)
183
183
  if (event.key === "c" && event.ctrlKey && !event.altKey && !event.shiftKey) {
184
+ if (!("clipboard" in navigator)) return;
184
185
  const td = _getClosestDataCell(target);
185
186
  if (td == null) return;
186
187
  if (td !== target) return; // Only when td itself is focused
@@ -198,6 +199,7 @@ export function useSheetCellAgent(options: {
198
199
 
199
200
  // Ctrl+V (paste)
200
201
  if (event.key === "v" && event.ctrlKey && !event.altKey && !event.shiftKey) {
202
+ if (!("clipboard" in navigator)) return;
201
203
  const td = _getClosestDataCell(target);
202
204
  if (td == null) return;
203
205
  if (td !== target) return; // Only when td itself is focused
@@ -1,6 +1,12 @@
1
1
  import { computed, type Signal } from "@angular/core";
2
2
  import type { ISdSheetColumnDef } from "./types";
3
3
 
4
+ /**
5
+ * Fixed column의 left offset을 계산한다.
6
+ *
7
+ * **주의:** fixed column의 `width`는 반드시 px 단위여야 정확한 offset이 계산된다.
8
+ * em, rem, % 등 non-px 단위의 width는 offset 누적에 반영되지 않는다 (0으로 처리).
9
+ */
4
10
  export function useSheetColumnFixing(options: {
5
11
  columnDefs: Signal<ISdSheetColumnDef[]>;
6
12
  }) {
@@ -132,7 +132,7 @@ const DEFAULT_EXTENSIONS: AnyExtension[] = [
132
132
  <button type="button" data-cmd="clean" (click)="execCmd('clean')">Tx</button>
133
133
  </div>
134
134
  </div>
135
- @if (colorPickerMode !== undefined) {
135
+ @if (colorPickerMode() !== undefined) {
136
136
  <div class="_color-picker">
137
137
  @for (color of colorPresets; track color) {
138
138
  <button type="button" class="_color-swatch"
@@ -288,11 +288,11 @@ export class SdTiptapEditorControl {
288
288
  activeStates: WritableSignal<TiptapActiveStates> = signal(DEFAULT_ACTIVE_STATES);
289
289
  activeColor = signal("");
290
290
  activeBgColor = signal("");
291
- colorPickerMode: "text" | "bg" | undefined;
291
+ colorPickerMode = signal<"text" | "bg" | undefined>(undefined);
292
292
 
293
293
  /** @internal -- TipTap Editor 인스턴스. 테스트 및 고급 사용자용 */
294
294
  editor: WritableSignal<Editor | undefined> = signal(undefined);
295
- private updatingFromEditor = false;
295
+ private lastEditorHtml: string | undefined;
296
296
  private lastExtensions: AnyExtension[] | undefined;
297
297
 
298
298
  private readonly resolvedExtensions = computed(() => {
@@ -312,9 +312,6 @@ export class SdTiptapEditorControl {
312
312
  const extensions = this.resolvedExtensions();
313
313
  const val = this.value();
314
314
 
315
- // Skip if value change originated from editor input
316
- if (this.updatingFromEditor) return;
317
-
318
315
  // Recreate editor if extensions changed
319
316
  if (this.lastExtensions !== extensions) {
320
317
  this.lastExtensions = extensions;
@@ -323,12 +320,16 @@ export class SdTiptapEditorControl {
323
320
  return;
324
321
  }
325
322
 
323
+ // Skip if value matches last editor output (editor-originated change)
324
+ if (val === this.lastEditorHtml) return;
325
+
326
326
  // Sync value to existing editor
327
327
  const currentEditor = untracked(() => this.editor());
328
328
  if (currentEditor == null) return;
329
329
  const currentHtml = this.getEditorHtmlFrom(currentEditor);
330
330
  if (currentHtml === val) return;
331
331
  currentEditor.commands.setContent(val ?? "", { emitUpdate: false });
332
+ this.lastEditorHtml = undefined;
332
333
  });
333
334
 
334
335
  // disabled/readonly → editor.setEditable()
@@ -374,9 +375,8 @@ export class SdTiptapEditorControl {
374
375
  editable: untracked(() => !this.disabled() && !this.readonly()),
375
376
  onUpdate: ({ editor }) => {
376
377
  const html = this.getEditorHtmlFrom(editor);
377
- this.updatingFromEditor = true;
378
+ this.lastEditorHtml = html;
378
379
  this.value.set(html);
379
- this.updatingFromEditor = false;
380
380
  },
381
381
  onTransaction: () => {
382
382
  this.refreshActiveStates();
@@ -390,6 +390,7 @@ export class SdTiptapEditorControl {
390
390
  ed.destroy();
391
391
  this.editor.set(undefined);
392
392
  }
393
+ this.lastEditorHtml = undefined;
393
394
  }
394
395
 
395
396
  private getEditorHtmlFrom(editor: Editor): string | undefined {
@@ -459,7 +460,7 @@ export class SdTiptapEditorControl {
459
460
  }
460
461
 
461
462
  toggleColorPicker(mode: "text" | "bg"): void {
462
- this.colorPickerMode = this.colorPickerMode === mode ? undefined : mode;
463
+ this.colorPickerMode.set(this.colorPickerMode() === mode ? undefined : mode);
463
464
  }
464
465
 
465
466
  applyColor(color: string | undefined): void {
@@ -467,20 +468,21 @@ export class SdTiptapEditorControl {
467
468
  if (ed == null) return;
468
469
 
469
470
  const chain = ed.chain().focus();
470
- if (this.colorPickerMode === "text") {
471
+ const mode = this.colorPickerMode();
472
+ if (mode === "text") {
471
473
  if (color !== undefined) {
472
474
  chain.setColor(color).run();
473
475
  } else {
474
476
  chain.unsetColor().run();
475
477
  }
476
- } else if (this.colorPickerMode === "bg") {
478
+ } else if (mode === "bg") {
477
479
  if (color !== undefined) {
478
480
  chain.setHighlight({ color }).run();
479
481
  } else {
480
482
  chain.unsetHighlight().run();
481
483
  }
482
484
  }
483
- this.colorPickerMode = undefined;
485
+ this.colorPickerMode.set(undefined);
484
486
  }
485
487
 
486
488
  private refreshActiveStates(): void {
@@ -84,7 +84,13 @@ export class SdDateRangePicker {
84
84
  handleFromDateChanged(): void {
85
85
  if (this.periodType() === "월") {
86
86
  const fromDate = this.from();
87
- this.to.set(fromDate?.setDay(1).addMonths(1).addDays(-1));
87
+ if (fromDate) {
88
+ const firstOfMonth = fromDate.setDay(1);
89
+ this.from.set(firstOfMonth);
90
+ this.to.set(firstOfMonth.addMonths(1).addDays(-1));
91
+ } else {
92
+ this.to.set(undefined);
93
+ }
88
94
  } else if (this.periodType() === "일") {
89
95
  this.to.set(this.from());
90
96
  } else if (
@@ -10,6 +10,7 @@ import {
10
10
  model,
11
11
  signal,
12
12
  TemplateRef,
13
+ untracked,
13
14
  viewChild,
14
15
  ViewEncapsulation,
15
16
  } from "@angular/core";
@@ -319,6 +320,8 @@ export class SdSelectControl<M extends "single" | "multi", T> {
319
320
  });
320
321
 
321
322
  // Mirror selected item's contentHTML to the trigger display area
323
+ // PERF-004: item.value() reads are untracked to reduce signal subscriptions from O(N) to O(K).
324
+ // _itemControls() already tracks item additions/removals, value() tracks selection changes.
322
325
  effect(() => {
323
326
  const items = this._itemControls();
324
327
  const currentValue = this.value();
@@ -335,14 +338,14 @@ export class SdSelectControl<M extends "single" | "multi", T> {
335
338
  return;
336
339
  }
337
340
 
341
+ const selectedItems = untracked(() => items.filter((item) => arr.includes(item.value())));
342
+
338
343
  const separator = this.multiSelectionDisplayDirection() === "vertical" ? "<br>" : ", ";
339
344
  const htmlParts: string[] = [];
340
- for (const item of items) {
341
- if (arr.includes(item.value())) {
342
- const html = item.contentHTML();
343
- if (html !== "") {
344
- htmlParts.push(html);
345
- }
345
+ for (const item of selectedItems) {
346
+ const html = item.contentHTML();
347
+ if (html !== "") {
348
+ htmlParts.push(html);
346
349
  }
347
350
  }
348
351
  if (htmlParts.length > 0) {
@@ -353,16 +356,17 @@ export class SdSelectControl<M extends "single" | "multi", T> {
353
356
  return;
354
357
  }
355
358
 
356
- for (const item of items) {
357
- if (item.value() === currentValue) {
358
- const html = item.contentHTML();
359
- if (html !== "") {
360
- this._selectedItemContentHTML.set(html);
361
- }
362
- return;
359
+ const selectedItem = untracked(() => items.find((item) => item.value() === currentValue));
360
+ if (selectedItem != null) {
361
+ const html = selectedItem.contentHTML();
362
+ if (html !== "") {
363
+ this._selectedItemContentHTML.set(html);
364
+ } else {
365
+ this._selectedItemContentHTML.set(undefined);
363
366
  }
367
+ } else {
368
+ this._selectedItemContentHTML.set(undefined);
364
369
  }
365
- this._selectedItemContentHTML.set(undefined);
366
370
  });
367
371
  }
368
372