@simplysm/angular 14.0.47 → 14.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.
Files changed (134) hide show
  1. package/README.md +467 -0
  2. package/dist/controls/dropdown/sd-dropdown-popup.d.ts +1 -0
  3. package/dist/controls/dropdown/sd-dropdown-popup.d.ts.map +1 -1
  4. package/dist/controls/dropdown/sd-dropdown-popup.js +6 -1
  5. package/dist/controls/dropdown/sd-dropdown.d.ts.map +1 -1
  6. package/dist/controls/dropdown/sd-dropdown.js +6 -2
  7. package/dist/controls/select/sd-select.d.ts.map +1 -1
  8. package/dist/controls/select/sd-select.js +12 -34
  9. package/dist/core/routing/injectViewTypeSignal.d.ts +1 -1
  10. package/dist/core/routing/injectViewTypeSignal.d.ts.map +1 -1
  11. package/dist/core/routing/injectViewTypeSignal.js +15 -7
  12. package/dist/core/selection/useSelectionManager.d.ts +1 -0
  13. package/dist/core/selection/useSelectionManager.d.ts.map +1 -1
  14. package/dist/core/selection/useSelectionManager.js +54 -14
  15. package/dist/data/shared-data/sd-shared-data-select-button.d.ts +11 -5
  16. package/dist/data/shared-data/sd-shared-data-select-button.d.ts.map +1 -1
  17. package/dist/data/shared-data/sd-shared-data-select-button.js +68 -29
  18. package/dist/data/sheet/sd-sheet.d.ts +4 -1
  19. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  20. package/dist/data/sheet/sd-sheet.js +23 -4
  21. package/dist/index.d.ts +0 -14
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +0 -19
  24. package/dist/styles.css +9 -0
  25. package/docs/bootstrap.md +38 -0
  26. package/docs/directives.md +236 -0
  27. package/docs/features.md +154 -0
  28. package/docs/pipes.md +32 -0
  29. package/docs/plugins.md +37 -0
  30. package/docs/provider-types.md +283 -0
  31. package/docs/providers.md +379 -0
  32. package/docs/recipes/crud-detail.md +875 -0
  33. package/docs/recipes/crud-list.md +1037 -0
  34. package/docs/recipes/data-select-button.md +318 -0
  35. package/docs/recipes/page-modal-container.md +178 -0
  36. package/docs/styling.md +222 -0
  37. package/docs/type-utilities.md +250 -0
  38. package/docs/ui-data.md +333 -0
  39. package/docs/ui-form.md +502 -0
  40. package/docs/ui-layout.md +140 -0
  41. package/docs/ui-navigation.md +303 -0
  42. package/docs/ui-overlay.md +157 -0
  43. package/docs/ui-visual.md +127 -0
  44. package/docs/utils.md +244 -0
  45. package/package.json +21 -20
  46. package/scss/commons/_styles.scss +12 -0
  47. package/src/controls/dropdown/sd-dropdown-popup.ts +6 -2
  48. package/src/controls/dropdown/sd-dropdown.ts +3 -4
  49. package/src/controls/select/sd-select.ts +6 -30
  50. package/src/core/routing/injectViewTypeSignal.ts +17 -10
  51. package/src/core/selection/useSelectionManager.ts +62 -12
  52. package/src/data/shared-data/sd-shared-data-select-button.ts +49 -14
  53. package/src/data/sheet/sd-sheet.ts +21 -0
  54. package/src/index.ts +0 -32
  55. package/dist/core/commons.d.ts +0 -2
  56. package/dist/core/commons.d.ts.map +0 -1
  57. package/dist/core/commons.js +0 -4
  58. package/dist/core/injectParent.d.ts +0 -7
  59. package/dist/core/injectParent.d.ts.map +0 -1
  60. package/dist/core/injectParent.js +0 -30
  61. package/dist/core/selection/setupCumulateSelectedKeys.d.ts +0 -9
  62. package/dist/core/selection/setupCumulateSelectedKeys.d.ts.map +0 -1
  63. package/dist/core/selection/setupCumulateSelectedKeys.js +0 -27
  64. package/dist/core/withBusy.d.ts +0 -3
  65. package/dist/core/withBusy.d.ts.map +0 -1
  66. package/dist/core/withBusy.js +0 -9
  67. package/dist/data/data-detail/sd-data-detail.base.d.ts +0 -51
  68. package/dist/data/data-detail/sd-data-detail.base.d.ts.map +0 -1
  69. package/dist/data/data-detail/sd-data-detail.base.js +0 -127
  70. package/dist/data/data-detail/sd-data-detail.d.ts +0 -30
  71. package/dist/data/data-detail/sd-data-detail.d.ts.map +0 -1
  72. package/dist/data/data-detail/sd-data-detail.js +0 -495
  73. package/dist/data/data-select-button/sd-data-select-button.base.d.ts +0 -24
  74. package/dist/data/data-select-button/sd-data-select-button.base.d.ts.map +0 -1
  75. package/dist/data/data-select-button/sd-data-select-button.base.js +0 -68
  76. package/dist/data/data-select-button/sd-data-select-button.d.ts +0 -15
  77. package/dist/data/data-select-button/sd-data-select-button.d.ts.map +0 -1
  78. package/dist/data/data-select-button/sd-data-select-button.js +0 -152
  79. package/dist/data/data-sheet/injectDataSheetExcelManager.d.ts +0 -14
  80. package/dist/data/data-sheet/injectDataSheetExcelManager.d.ts.map +0 -1
  81. package/dist/data/data-sheet/injectDataSheetExcelManager.js +0 -31
  82. package/dist/data/data-sheet/injectDataSheetInlineEditManager.d.ts +0 -26
  83. package/dist/data/data-sheet/injectDataSheetInlineEditManager.d.ts.map +0 -1
  84. package/dist/data/data-sheet/injectDataSheetInlineEditManager.js +0 -54
  85. package/dist/data/data-sheet/injectDataSheetModalEditManager.d.ts +0 -19
  86. package/dist/data/data-sheet/injectDataSheetModalEditManager.d.ts.map +0 -1
  87. package/dist/data/data-sheet/injectDataSheetModalEditManager.js +0 -44
  88. package/dist/data/data-sheet/injectDataSheetRefreshManager.d.ts +0 -25
  89. package/dist/data/data-sheet/injectDataSheetRefreshManager.d.ts.map +0 -1
  90. package/dist/data/data-sheet/injectDataSheetRefreshManager.js +0 -50
  91. package/dist/data/data-sheet/sd-data-sheet-column.d.ts +0 -8
  92. package/dist/data/data-sheet/sd-data-sheet-column.d.ts.map +0 -1
  93. package/dist/data/data-sheet/sd-data-sheet-column.js +0 -15
  94. package/dist/data/data-sheet/sd-data-sheet.base.d.ts +0 -75
  95. package/dist/data/data-sheet/sd-data-sheet.base.d.ts.map +0 -1
  96. package/dist/data/data-sheet/sd-data-sheet.base.js +0 -188
  97. package/dist/data/data-sheet/sd-data-sheet.d.ts +0 -49
  98. package/dist/data/data-sheet/sd-data-sheet.d.ts.map +0 -1
  99. package/dist/data/data-sheet/sd-data-sheet.js +0 -968
  100. package/dist/data/data-sheet/sd-data-sheet.types.d.ts +0 -17
  101. package/dist/data/data-sheet/sd-data-sheet.types.d.ts.map +0 -1
  102. package/dist/data/data-sheet/sd-data-sheet.types.js +0 -1
  103. package/dist/data/data-sheet/setupCloserWhenSingleSelectionChange.d.ts +0 -9
  104. package/dist/data/data-sheet/setupCloserWhenSingleSelectionChange.d.ts.map +0 -1
  105. package/dist/data/data-sheet/setupCloserWhenSingleSelectionChange.js +0 -20
  106. package/dist/data/data-sheet/useDataSheetFilterManager.d.ts +0 -13
  107. package/dist/data/data-sheet/useDataSheetFilterManager.d.ts.map +0 -1
  108. package/dist/data/data-sheet/useDataSheetFilterManager.js +0 -19
  109. package/dist/data/getOrmDataEditToastErrorMessage.d.ts +0 -2
  110. package/dist/data/getOrmDataEditToastErrorMessage.d.ts.map +0 -1
  111. package/dist/data/getOrmDataEditToastErrorMessage.js +0 -8
  112. package/dist/layout/base-container/sd-base-container.d.ts +0 -27
  113. package/dist/layout/base-container/sd-base-container.d.ts.map +0 -1
  114. package/dist/layout/base-container/sd-base-container.js +0 -193
  115. package/src/core/commons.ts +0 -4
  116. package/src/core/injectParent.ts +0 -45
  117. package/src/core/selection/setupCumulateSelectedKeys.ts +0 -38
  118. package/src/core/withBusy.ts +0 -13
  119. package/src/data/data-detail/sd-data-detail.base.ts +0 -187
  120. package/src/data/data-detail/sd-data-detail.ts +0 -237
  121. package/src/data/data-select-button/sd-data-select-button.base.ts +0 -104
  122. package/src/data/data-select-button/sd-data-select-button.ts +0 -105
  123. package/src/data/data-sheet/injectDataSheetExcelManager.ts +0 -57
  124. package/src/data/data-sheet/injectDataSheetInlineEditManager.ts +0 -89
  125. package/src/data/data-sheet/injectDataSheetModalEditManager.ts +0 -76
  126. package/src/data/data-sheet/injectDataSheetRefreshManager.ts +0 -90
  127. package/src/data/data-sheet/sd-data-sheet-column.ts +0 -10
  128. package/src/data/data-sheet/sd-data-sheet.base.ts +0 -294
  129. package/src/data/data-sheet/sd-data-sheet.ts +0 -479
  130. package/src/data/data-sheet/sd-data-sheet.types.ts +0 -18
  131. package/src/data/data-sheet/setupCloserWhenSingleSelectionChange.ts +0 -29
  132. package/src/data/data-sheet/useDataSheetFilterManager.ts +0 -30
  133. package/src/data/getOrmDataEditToastErrorMessage.ts +0 -10
  134. package/src/layout/base-container/sd-base-container.ts +0 -121
package/docs/utils.md ADDED
@@ -0,0 +1,244 @@
1
+ # Utils & Setups
2
+
3
+ ## Utility Functions
4
+
5
+ ### `mark`
6
+
7
+ WritableSignal의 변경 알림을 수동으로 트리거한다. 배열/객체의 내부 변경(mutation) 후 consumer에게 변경을 알릴 때 사용. shallow copy로 새 참조를 생성하여 signal을 업데이트한다.
8
+
9
+ ```typescript
10
+ function mark(sig: WritableSignal<any>): void
11
+ ```
12
+
13
+ | Parameter | Type | Description |
14
+ |-----------|------|-------------|
15
+ | `sig` | `WritableSignal<any>` | 대상 signal. 배열이면 `[...v]`, 객체이면 `{...v}`로 shallow copy하여 update |
16
+
17
+ ### `setSafeStyle`
18
+
19
+ Renderer2를 사용하여 여러 CSS 스타일을 일괄 적용한다.
20
+
21
+ ```typescript
22
+ function setSafeStyle(
23
+ renderer: Renderer2,
24
+ el: HTMLElement,
25
+ style: Partial<CSSStyleDeclaration>,
26
+ ): void
27
+ ```
28
+
29
+ ## Resource Functions
30
+
31
+ ### `injectSdSystemConfigResource`
32
+
33
+ 컴포넌트 태그명 기반 키로 시스템 설정을 읽고 쓰는 resource 래퍼. 생성자에서 호출.
34
+
35
+ ```typescript
36
+ function injectSdSystemConfigResource<T>(options: {
37
+ key: Signal<string | undefined>;
38
+ }): {
39
+ value: Signal<T | undefined>;
40
+ isLoading: Signal<boolean>;
41
+ status: Signal<ResourceStatus>;
42
+ hasValue: () => boolean;
43
+ reload: () => void;
44
+ set(value: T | undefined): void;
45
+ update(fn: (prev: T | undefined) => T | undefined): void;
46
+ }
47
+ ```
48
+
49
+ `set()`/`update()` 호출 시 signal을 즉시 업데이트하고, 비동기로 `SdSystemConfigProvider.setAsync()`를 호출하여 영속화한다.
50
+
51
+ ## Page Code / View Signals
52
+
53
+ ### `injectCurrentPageCodeSignal`
54
+
55
+ 현재 활성 라우트의 경로 세그먼트를 `.`으로 연결한 코드 signal. `ActivatedRoute`가 없으면 undefined 반환.
56
+
57
+ ```typescript
58
+ function injectCurrentPageCodeSignal(): Signal<string> | undefined
59
+ ```
60
+
61
+ ### `injectFullPageCodeSignal`
62
+
63
+ 전체 URL 기반 페이지 코드 signal. `NavigationEnd` 이벤트를 구독하여 URL에서 코드를 추출한다.
64
+
65
+ ```typescript
66
+ function injectFullPageCodeSignal(): Signal<string>
67
+ ```
68
+
69
+ ### `injectViewTitleSignal`
70
+
71
+ 현재 뷰의 타이틀 signal. 모달이면 모달 타이틀, 페이지이면 `SdAppStructureProvider`에서 조회.
72
+
73
+ ```typescript
74
+ function injectViewTitleSignal(): Signal<string>
75
+ ```
76
+
77
+ ### `injectViewTypeSignal`
78
+
79
+ 현재 뷰의 타입 signal. 모달/페이지/컨트롤 중 하나를 반환한다.
80
+
81
+ ```typescript
82
+ function injectViewTypeSignal(): Signal<SdViewType>
83
+ ```
84
+
85
+ 내부 판정 로직:
86
+ - `SdActivatedModalProvider`가 inject되면 `"modal"`
87
+ - `ActivatedRoute.component`의 `reflectComponentType().selector`가 호스트 `ElementRef.nativeElement.tagName.toLowerCase()`와 일치하고 `fullPageCode() === currPageCode?.()`이면 `"page"`
88
+ - 그 외 `"control"`
89
+
90
+ ## Manager Functions
91
+
92
+ ### `useSelectionManager`
93
+
94
+ 선택 관리 composable. single/multi 모드를 지원한다. `trackByFn`이 반환하는 key를 기준으로 `obj.equal`(deep equal) 비교를 수행하므로, 같은 key의 다른 reference item도 `isSelected` true로 복원된다.
95
+
96
+ ```typescript
97
+ function useSelectionManager<T>(options: {
98
+ displayItems: Signal<T[]>;
99
+ selectedItems: WritableSignal<T[]>;
100
+ selectMode: Signal<"single" | "multi" | undefined>;
101
+ getItemSelectableFn: Signal<((item: T) => boolean | string) | undefined>;
102
+ trackByFn: Signal<(item: T, index: number) => unknown>;
103
+ }): {
104
+ hasSelectable: Signal<boolean>;
105
+ isAllSelected: Signal<boolean>;
106
+ getSelectable(item: T): true | string | undefined;
107
+ getCanChangeFn(item: T): () => boolean;
108
+ select(item: T): void;
109
+ deselect(item: T): void;
110
+ toggle(item: T): void;
111
+ toggleAll(): void;
112
+ isSelected(item: T): boolean;
113
+ }
114
+ ```
115
+
116
+ | Return | Type | Description |
117
+ |--------|------|-------------|
118
+ | `hasSelectable` | `Signal<boolean>` | selectMode가 설정되어 있는지 여부 |
119
+ | `isAllSelected` | `Signal<boolean>` | 모든 선택 가능 항목이 선택되었는지 |
120
+ | `getSelectable(item)` | `true \| string \| undefined` | 선택 가능 여부. string은 불가 사유 |
121
+ | `getCanChangeFn(item)` | `() => boolean` | 선택 변경 가능 여부 함수 |
122
+ | `select(item)` | `void` | 항목 선택 (single이면 교체) |
123
+ | `deselect(item)` | `void` | 항목 선택 해제 |
124
+ | `toggle(item)` | `void` | 항목 토글 |
125
+ | `toggleAll()` | `void` | 전체 선택/해제 토글 |
126
+ | `isSelected(item)` | `boolean` | 선택 여부 |
127
+
128
+ ### `useSortingManager`
129
+
130
+ 정렬 관리 composable.
131
+
132
+ ```typescript
133
+ function useSortingManager(options: {
134
+ sorts: WritableSignal<SortingDef[]>;
135
+ }): {
136
+ defMap: Signal<Map<string, { indexText?: string; desc: boolean }>>;
137
+ toggle(key: string, multiple: boolean): void;
138
+ sort<T>(items: T[]): T[];
139
+ }
140
+ ```
141
+
142
+ | Return | Type | Description |
143
+ |--------|------|-------------|
144
+ | `defMap` | `Signal<Map<string, ...>>` | 키별 정렬 정의 (인덱스 텍스트, 방향) |
145
+ | `toggle(key, multiple)` | `void` | 정렬 토글. `multiple=true`면 멀티 소트, `false`면 단일 소트. 3단계: asc -> desc -> 없음 |
146
+ | `sort(items)` | `T[]` | 정렬 적용. 원본 배열은 변경하지 않음 |
147
+
148
+ ### `useExpandingManager`
149
+
150
+ 트리 확장/축소 관리 composable.
151
+
152
+ ```typescript
153
+ function useExpandingManager<T>(binding: {
154
+ items: Signal<T[]>;
155
+ expandedItems: WritableSignal<T[]>;
156
+ getChildrenFn: Signal<((item: T, index: number) => T[] | undefined) | undefined>;
157
+ sort: (items: T[]) => T[];
158
+ }): {
159
+ displayItems: Signal<T[]>;
160
+ hasExpandable: Signal<boolean>;
161
+ isAllExpanded: Signal<boolean>;
162
+ toggle(item: T): void;
163
+ toggleAll(): void;
164
+ isVisible(item: T): boolean;
165
+ def(item: T): ExpandItemDef<T>;
166
+ }
167
+ ```
168
+
169
+ | Return | Type | Description |
170
+ |--------|------|-------------|
171
+ | `displayItems` | `Signal<T[]>` | 트리를 평탄화한 전체 항목 (숨김 포함) |
172
+ | `hasExpandable` | `Signal<boolean>` | 확장 가능한 항목 존재 여부 |
173
+ | `isAllExpanded` | `Signal<boolean>` | 모든 확장 가능 항목이 펼쳐졌는지 |
174
+ | `toggle(item)` | `void` | 항목 확장/축소 토글 |
175
+ | `toggleAll()` | `void` | 전체 확장/축소 토글 |
176
+ | `isVisible(item)` | `boolean` | 부모 체인이 모두 펼쳐져 항목이 보이는지 |
177
+ | `def(item)` | `ExpandItemDef<T>` | 항목의 트리 정의 (depth, parentDef 등) |
178
+
179
+ ## Setup Functions
180
+
181
+ 생성자에서 호출하는 설정 함수들. `inject()`, `effect()`, `DestroyRef.onDestroy()`를 사용하여 수명주기를 관리한다.
182
+
183
+ ### `setupBgTheme`
184
+
185
+ body 배경 테마 색상을 설정한다. 파괴 시 자동 복원.
186
+
187
+ ```typescript
188
+ function setupBgTheme(options?: {
189
+ theme?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray";
190
+ lightness?: "lightest" | "lighter";
191
+ }): void
192
+ ```
193
+
194
+ ### `setupRipple`
195
+
196
+ 호스트 요소에 리플 효과를 설정한다. pointer 이벤트 기반.
197
+
198
+ ```typescript
199
+ function setupRipple(enableFn?: () => boolean): void
200
+ ```
201
+
202
+ ### `setupRevealOnShow`
203
+
204
+ 뷰포트 진입 시 reveal 애니메이션을 설정한다. IntersectionObserver 사용.
205
+
206
+ ```typescript
207
+ function setupRevealOnShow(optFn?: () => {
208
+ type?: "l2r" | "t2b";
209
+ enabled?: boolean;
210
+ }): void
211
+ ```
212
+
213
+ ### `setupInvalid`
214
+
215
+ 유효성 검증 표시기를 설정한다. 빨간 점 indicator + hidden input으로 구현.
216
+
217
+ ```typescript
218
+ function setupInvalid(getInvalidMessage: () => string): void
219
+ ```
220
+
221
+ 빈 문자열이면 유효, 비어있지 않으면 무효.
222
+
223
+ ### `setupModelHook`
224
+
225
+ model signal의 `set`/`update`를 가드 함수로 래핑한다. 가드 함수가 `false`를 반환하면 값 변경을 차단.
226
+
227
+ ```typescript
228
+ function setupModelHook<T, S extends WritableSignal<T>>(
229
+ model: S,
230
+ canFn: Signal<(item: T) => boolean | Promise<boolean>>,
231
+ ): void
232
+ ```
233
+
234
+ ### `setupCanDeactivate`
235
+
236
+ 모달 또는 라우트에 canDeactivate 가드를 설정한다.
237
+
238
+ ```typescript
239
+ function setupCanDeactivate(fn: () => boolean): void
240
+ ```
241
+
242
+ 모달 내부이면 `SdActivatedModalProvider.canDeactivateFn`에 설정, 라우트 내부이면 `routeConfig.canDeactivate`에 추가.
243
+
244
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/angular",
3
- "version": "14.0.47",
3
+ "version": "14.0.49",
4
4
  "description": "심플리즘 패키지 - Angular",
5
5
  "author": "심플리즘",
6
6
  "license": "Apache-2.0",
@@ -16,7 +16,8 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "src",
19
- "scss"
19
+ "scss",
20
+ "docs"
20
21
  ],
21
22
  "sideEffects": [
22
23
  "./src/index.ts",
@@ -30,30 +31,30 @@
30
31
  "@angular/platform-browser": "^21.2.9",
31
32
  "@angular/router": "^21.2.9",
32
33
  "@angular/service-worker": "^21.2.9",
33
- "@ng-icons/core": "^33.2.1",
34
- "@ng-icons/tabler-icons": "^33.2.1",
35
- "@tiptap/core": "^3.22.3",
36
- "@tiptap/extension-color": "^3.22.3",
37
- "@tiptap/extension-document": "^3.22.3",
38
- "@tiptap/extension-highlight": "^3.22.3",
39
- "@tiptap/extension-image": "^3.22.3",
40
- "@tiptap/extension-paragraph": "^3.22.3",
41
- "@tiptap/extension-placeholder": "^3.22.3",
42
- "@tiptap/extension-text": "^3.22.3",
43
- "@tiptap/extension-text-align": "^3.22.3",
44
- "@tiptap/extension-text-style": "^3.22.3",
45
- "@tiptap/extension-underline": "^3.22.3",
46
- "@tiptap/starter-kit": "^3.22.3",
34
+ "@ng-icons/core": "^33.2.2",
35
+ "@ng-icons/tabler-icons": "^33.2.2",
36
+ "@tiptap/core": "^3.22.4",
37
+ "@tiptap/extension-color": "^3.22.4",
38
+ "@tiptap/extension-document": "^3.22.4",
39
+ "@tiptap/extension-highlight": "^3.22.4",
40
+ "@tiptap/extension-image": "^3.22.4",
41
+ "@tiptap/extension-paragraph": "^3.22.4",
42
+ "@tiptap/extension-placeholder": "^3.22.4",
43
+ "@tiptap/extension-text": "^3.22.4",
44
+ "@tiptap/extension-text-align": "^3.22.4",
45
+ "@tiptap/extension-text-style": "^3.22.4",
46
+ "@tiptap/extension-underline": "^3.22.4",
47
+ "@tiptap/starter-kit": "^3.22.4",
47
48
  "bwip-js": "^4.9.2",
48
49
  "echarts": "^6.0.0",
49
50
  "html-to-image": "^1.11.13",
50
51
  "jspdf": "^4.2.1",
51
52
  "rxjs": "^7.8.2",
52
53
  "tabbable": "^6.4.0",
53
- "@simplysm/core-browser": "14.0.47",
54
- "@simplysm/service-common": "14.0.47",
55
- "@simplysm/core-common": "14.0.47",
56
- "@simplysm/service-client": "14.0.47"
54
+ "@simplysm/core-browser": "14.0.49",
55
+ "@simplysm/core-common": "14.0.49",
56
+ "@simplysm/service-client": "14.0.49",
57
+ "@simplysm/service-common": "14.0.49"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@angular/compiler": "^21.2.9",
@@ -111,6 +111,18 @@
111
111
  } // @layer base
112
112
 
113
113
  @layer utilities {
114
+ .block {
115
+ display: block;
116
+ }
117
+
118
+ .inline-block {
119
+ display: inline-block;
120
+ }
121
+
122
+ .inline {
123
+ display: inline;
124
+ }
125
+
114
126
  //-- FONT SIZE
115
127
 
116
128
  @each $h in (h1, h2, h3, h4, h5, h6) {
@@ -91,6 +91,7 @@ export class SdDropdownPopup {
91
91
  forwardRef(() => SdDropdown),
92
92
  );
93
93
  private readonly _elRef = inject(ElementRef<HTMLElement>);
94
+ private _capped = false;
94
95
 
95
96
  onKeydown(event: KeyboardEvent): void {
96
97
  this._parentControl.onPopupKeydown(event);
@@ -98,8 +99,11 @@ export class SdDropdownPopup {
98
99
 
99
100
  onResize(): void {
100
101
  const el = this._elRef.nativeElement;
101
- const divEl = el.firstElementChild!;
102
- if (divEl.clientHeight > 300) {
102
+ const divEl = el.firstElementChild as HTMLElement;
103
+ const shouldCap = divEl.scrollHeight > 300;
104
+ if (shouldCap === this._capped) return;
105
+ this._capped = shouldCap;
106
+ if (shouldCap) {
103
107
  el.style.height = "300px";
104
108
  } else {
105
109
  el.style.removeProperty("height");
@@ -37,10 +37,9 @@ export class SdDropdown {
37
37
  open = model(false);
38
38
  disabled = input(false, { transform: booleanAttribute });
39
39
 
40
- popupElRef = contentChild.required<SdDropdownPopup, ElementRef<HTMLElement>>(
41
- SdDropdownPopup,
42
- { read: ElementRef },
43
- );
40
+ popupElRef = contentChild.required<SdDropdownPopup, ElementRef<HTMLElement>>(SdDropdownPopup, {
41
+ read: ElementRef,
42
+ });
44
43
 
45
44
  private _popupEl?: HTMLElement;
46
45
  private _mouseoverEl?: HTMLElement;
@@ -52,7 +52,12 @@ export type SelectModeValue<T> = {
52
52
  ],
53
53
  template: `
54
54
  <sd-dropdown [disabled]="disabled()" [(open)]="dropdownOpen">
55
- <div class="_sd-select-control" [sdRipple]="!disabled()">
55
+ <div
56
+ class="_sd-select-control"
57
+ [class]="contentClass()"
58
+ [style]="contentStyle()"
59
+ [sdRipple]="!disabled()"
60
+ >
56
61
  <div class="_sd-select-control-content" #contentEl>
57
62
  @if (_selectedItemContentHTML() != null) {
58
63
  <div [innerHTML]="_selectedItemContentHTML()"></div>
@@ -321,30 +326,9 @@ export class SdSelect<M extends "single" | "multi", T> {
321
326
  return "";
322
327
  });
323
328
 
324
- // D3: contentClass/contentStyle via effect() on popup element
325
- // + D4: keyboard navigation via tabbable()
326
329
  effect((onCleanup) => {
327
330
  if (this.dropdownOpen()) {
328
331
  const popupEl = this._dropdownControl().popupElRef().nativeElement;
329
- const cls = this.contentClass();
330
- const addedClasses: string[] = [];
331
- if (cls != null) {
332
- for (const c of cls.split(" ").filter(Boolean)) {
333
- popupEl.classList.add(c);
334
- addedClasses.push(c);
335
- }
336
- }
337
- const style = this.contentStyle();
338
- const addedStyleProps: string[] = [];
339
- if (style != null) {
340
- const tempEl = document.createElement("div");
341
- tempEl.style.cssText = style;
342
- for (let i = 0; i < tempEl.style.length; i++) {
343
- const prop = tempEl.style[i];
344
- addedStyleProps.push(prop);
345
- popupEl.style.setProperty(prop, tempEl.style.getPropertyValue(prop));
346
- }
347
- }
348
332
 
349
333
  const onKeydown = (event: KeyboardEvent) => {
350
334
  if (event.ctrlKey || event.altKey) return;
@@ -364,9 +348,7 @@ export class SdSelect<M extends "single" | "multi", T> {
364
348
  tabbableEls[nextIndex].focus();
365
349
  }
366
350
  } else {
367
- // ArrowUp
368
351
  if (currIndex <= 0) {
369
- // Return focus to dropdown trigger
370
352
  this._dropdownElRef().nativeElement.focus();
371
353
  } else {
372
354
  tabbableEls[currIndex - 1].focus();
@@ -378,12 +360,6 @@ export class SdSelect<M extends "single" | "multi", T> {
378
360
  popupEl.addEventListener("keydown", onKeydown);
379
361
  onCleanup(() => {
380
362
  popupEl.removeEventListener("keydown", onKeydown);
381
- for (const c of addedClasses) {
382
- popupEl.classList.remove(c);
383
- }
384
- for (const prop of addedStyleProps) {
385
- popupEl.style.removeProperty(prop);
386
- }
387
363
  });
388
364
  }
389
365
  });
@@ -1,12 +1,13 @@
1
- import { computed, inject, type Signal } from "@angular/core";
1
+ import { computed, ElementRef, inject, reflectComponentType, type Signal } from "@angular/core";
2
2
  import { ActivatedRoute } from "@angular/router";
3
3
  import { SdActivatedModalProvider } from "../modal/sd-activated-modal.provider";
4
4
  import { injectCurrentPageCodeSignal } from "./injectCurrentPageCodeSignal";
5
5
  import { injectFullPageCodeSignal } from "./injectFullPageCodeSignal";
6
6
 
7
- export function injectViewTypeSignal(getComp: () => object): Signal<SdViewType> {
7
+ export function injectViewTypeSignal(): Signal<SdViewType> {
8
8
  const sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
9
9
  const activatedRoute = inject(ActivatedRoute, { optional: true });
10
+ const elRef = inject(ElementRef);
10
11
 
11
12
  const fullPageCode = injectFullPageCodeSignal();
12
13
  const currPageCode = injectCurrentPageCodeSignal();
@@ -16,14 +17,20 @@ export function injectViewTypeSignal(getComp: () => object): Signal<SdViewType>
16
17
  return "modal";
17
18
  }
18
19
 
19
- const comp = getComp();
20
-
21
- if (
22
- activatedRoute &&
23
- activatedRoute.component === comp.constructor &&
24
- fullPageCode() === currPageCode?.()
25
- ) {
26
- return "page";
20
+ if (activatedRoute) {
21
+ const component = activatedRoute.component;
22
+ if (component == null || typeof component === "string") {
23
+ return "control";
24
+ }
25
+ if (
26
+ reflectComponentType(component)?.selector !==
27
+ elRef.nativeElement.tagName.toLowerCase()
28
+ ) {
29
+ return "control";
30
+ }
31
+ if (fullPageCode() === currPageCode?.()) {
32
+ return "page";
33
+ }
27
34
  }
28
35
 
29
36
  return "control";
@@ -1,10 +1,12 @@
1
1
  import { computed, type Signal, type WritableSignal } from "@angular/core";
2
+ import { obj } from "@simplysm/core-common";
2
3
 
3
4
  export function useSelectionManager<T>(options: {
4
5
  displayItems: Signal<T[]>;
5
6
  selectedItems: WritableSignal<T[]>;
6
7
  selectMode: Signal<"single" | "multi" | undefined>;
7
8
  getItemSelectableFn: Signal<((item: T) => boolean | string) | undefined>;
9
+ trackByFn: Signal<(item: T, index: number) => unknown>;
8
10
  }): {
9
11
  hasSelectable: Signal<boolean>;
10
12
  isAllSelected: Signal<boolean>;
@@ -26,13 +28,36 @@ export function useSelectionManager<T>(options: {
26
28
  return options.selectMode() != null;
27
29
  });
28
30
 
29
- const selectedItemsSet = computed(() => new Set(options.selectedItems()));
31
+ const displayItemIndexMap = computed(() => {
32
+ const map = new Map<T, number>();
33
+ options.displayItems().forEach((it, i) => map.set(it, i));
34
+ return map;
35
+ });
36
+
37
+ const selectedKeys = computed(() => {
38
+ const fn = options.trackByFn();
39
+ return options.selectedItems().map((it, i) => fn(it, i));
40
+ });
41
+
42
+ function isKeyEqual(a: unknown, b: unknown): boolean {
43
+ if (a === b) return true;
44
+ return obj.equal(a, b);
45
+ }
46
+
47
+ function keyOf(item: T): unknown {
48
+ const idx = displayItemIndexMap().get(item);
49
+ return options.trackByFn()(item, idx ?? 0);
50
+ }
30
51
 
31
52
  const isAllSelected = computed(() => {
32
53
  const items = selectableItems();
33
54
  if (items.length === 0) return false;
34
- const set = selectedItemsSet();
35
- return items.every((item) => set.has(item));
55
+ const keys = selectedKeys();
56
+ const fn = options.trackByFn();
57
+ return items.every((it, i) => {
58
+ const key = fn(it, i);
59
+ return keys.some((k) => isKeyEqual(k, key));
60
+ });
36
61
  });
37
62
 
38
63
  function getSelectable(item: T): true | string | undefined {
@@ -60,16 +85,23 @@ export function useSelectionManager<T>(options: {
60
85
 
61
86
  if (mode === "single") {
62
87
  options.selectedItems.set([item]);
63
- } else {
64
- options.selectedItems.update((arr) => {
65
- if (arr.includes(item)) return arr;
66
- return [...arr, item];
67
- });
88
+ return;
68
89
  }
90
+
91
+ const key = keyOf(item);
92
+ const fn = options.trackByFn();
93
+ options.selectedItems.update((arr) => {
94
+ if (arr.some((it, i) => isKeyEqual(fn(it, i), key))) return arr;
95
+ return [...arr, item];
96
+ });
69
97
  }
70
98
 
71
99
  function deselect(item: T): void {
72
- options.selectedItems.update((arr) => arr.filter((i) => i !== item));
100
+ const key = keyOf(item);
101
+ const fn = options.trackByFn();
102
+ options.selectedItems.update((arr) =>
103
+ arr.filter((it, i) => !isKeyEqual(fn(it, i), key)),
104
+ );
73
105
  }
74
106
 
75
107
  function toggle(item: T): void {
@@ -81,15 +113,33 @@ export function useSelectionManager<T>(options: {
81
113
  }
82
114
 
83
115
  function toggleAll(): void {
116
+ const selectable = selectableItems();
117
+ const fn = options.trackByFn();
118
+ const idxMap = displayItemIndexMap();
119
+
84
120
  if (isAllSelected()) {
85
- options.selectedItems.set([]);
121
+ const keysToRemove = selectable.map((it) => fn(it, idxMap.get(it) ?? 0));
122
+ options.selectedItems.update((arr) =>
123
+ arr.filter((it, i) => {
124
+ const k = fn(it, i);
125
+ return !keysToRemove.some((rk) => isKeyEqual(rk, k));
126
+ }),
127
+ );
86
128
  } else {
87
- options.selectedItems.set([...selectableItems()]);
129
+ options.selectedItems.update((arr) => {
130
+ const existingKeys = arr.map((it, i) => fn(it, i));
131
+ const toAdd = selectable.filter((it) => {
132
+ const k = fn(it, idxMap.get(it) ?? 0);
133
+ return !existingKeys.some((ek) => isKeyEqual(ek, k));
134
+ });
135
+ return [...arr, ...toAdd];
136
+ });
88
137
  }
89
138
  }
90
139
 
91
140
  function isSelected(item: T): boolean {
92
- return selectedItemsSet().has(item);
141
+ const key = keyOf(item);
142
+ return selectedKeys().some((k) => isKeyEqual(k, key));
93
143
  }
94
144
 
95
145
  return {