@simplysm/angular 14.2.14 → 14.2.17

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.
@@ -4,20 +4,40 @@ export declare class SdSidebarMenu {
4
4
  menus: import("@angular/core").InputSignal<SdMenu[]>;
5
5
  layout: import("@angular/core").InputSignal<"flat" | "accordion" | "accordion-expanded" | undefined>;
6
6
  getMenuIsSelectedFn: import("@angular/core").InputSignal<((menu: SdMenu) => boolean) | undefined>;
7
+ /**
8
+ * 펼쳐진 메뉴 코드(`codeChain.join(".")`) 목록.
9
+ *
10
+ * 미지정(`undefined`)이면 `layout` 이 정하는 기본 펼침 상태를 따름.
11
+ * 사용자가 토글하거나 호스트가 값을 세팅하면 그 값이 단일 진리원이 되며,
12
+ * 이후 `menus` 가 다시 계산돼도 상태가 유지됨.
13
+ */
14
+ expandedMenuCodes: import("@angular/core").ModelSignal<string[] | undefined>;
7
15
  fullPageCode: import("@angular/core").Signal<string>;
8
16
  rootLayout: import("@angular/core").Signal<"flat" | "accordion" | "accordion-expanded">;
9
- expandInitially: import("@angular/core").Signal<boolean>;
17
+ private readonly _expandableCodes;
18
+ private readonly _expandedCodeSet;
19
+ hasExpandable: import("@angular/core").Signal<boolean>;
20
+ isAllExpanded: import("@angular/core").Signal<boolean>;
21
+ isAllCollapsed: import("@angular/core").Signal<boolean>;
22
+ expandAll(): void;
23
+ collapseAll(): void;
24
+ getIsMenuExpanded(menu: SdMenu): boolean;
25
+ onMenuOpenChange(menu: SdMenu, open: boolean): void;
10
26
  getMenuRouterLinkOption(menu: SdMenu): {
11
27
  link: string;
12
28
  queryParams: Record<string, string> | undefined;
13
29
  } | undefined;
14
30
  getIsMenuSelected(menu: SdMenu): boolean;
15
31
  onMenuClick(menu: SdMenu): void;
32
+ protected readonly icons: {
33
+ tablerFoldDown: string;
34
+ tablerFoldUp: string;
35
+ };
16
36
  protected readonly itemTemplateType: {
17
37
  menus: SdMenu[];
18
38
  depth: number;
19
39
  };
20
40
  static ɵfac: i0.ɵɵFactoryDeclaration<SdSidebarMenu, never>;
21
- static ɵcmp: i0.ɵɵComponentDeclaration<SdSidebarMenu, "sd-sidebar-menu", never, { "menus": { "alias": "menus"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "getMenuIsSelectedFn": { "alias": "getMenuIsSelectedFn"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
41
+ static ɵcmp: i0.ɵɵComponentDeclaration<SdSidebarMenu, "sd-sidebar-menu", never, { "menus": { "alias": "menus"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "getMenuIsSelectedFn": { "alias": "getMenuIsSelectedFn"; "required": false; "isSignal": true; }; "expandedMenuCodes": { "alias": "expandedMenuCodes"; "required": false; "isSignal": true; }; }, { "expandedMenuCodes": "expandedMenuCodesChange"; }, never, never, true, never>;
22
42
  }
23
43
  //# sourceMappingURL=sd-sidebar-menu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sd-sidebar-menu.d.ts","sourceRoot":"","sources":["../../../src/layout/sidebar/sd-sidebar-menu.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,MAAM,EAGZ,MAAM,+BAA+B,CAAC;;AAQvC,qBAkEa,aAAa;IACxB,KAAK,gDAAuB;IAC5B,MAAM,+FAAwD;IAC9D,mBAAmB,8CAAgB,MAAM,KAAK,OAAO,eAAI;IAEzD,YAAY,yCAA8B;IAE1C,UAAU,8EAAsF;IAGhG,eAAe,0CAA8D;IAE7E,uBAAuB,CACrB,IAAI,EAAE,MAAM,GACX;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;KAAE,GAAG,SAAS;IAIhF,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIxC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/B,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAG;QACpC,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;yCA/BS,aAAa;2CAAb,aAAa;CAgCzB"}
1
+ {"version":3,"file":"sd-sidebar-menu.d.ts","sourceRoot":"","sources":["../../../src/layout/sidebar/sd-sidebar-menu.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,MAAM,EAGZ,MAAM,+BAA+B,CAAC;;AAUvC,qBAoGa,aAAa;IACxB,KAAK,gDAAuB;IAC5B,MAAM,+FAAwD;IAC9D,mBAAmB,8CAAgB,MAAM,KAAK,OAAO,eAAI;IAEzD;;;;;;OAMG;IACH,iBAAiB,4DAA0C;IAE3D,YAAY,yCAA8B;IAE1C,UAAU,8EAAsF;IAIhG,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAgB9B;IAGH,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAK9B;IAEH,aAAa,0CAAsD;IAEnE,aAAa,0CAKV;IAEH,cAAc,0CAKX;IAEH,SAAS,IAAI,IAAI;IAIjB,WAAW,IAAI,IAAI;IAInB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIxC,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IAWnD,uBAAuB,CACrB,IAAI,EAAE,MAAM,GACX;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;KAAE,GAAG,SAAS;IAIhF,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIxC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/B,SAAS,CAAC,QAAQ,CAAC,KAAK;;;MAGtB;IAEF,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAG;QACpC,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;yCA7GS,aAAa;2CAAb,aAAa;CA8GzB"}
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, computed, input, ViewEncapsulation, } from "@angular/core";
1
+ import { ChangeDetectionStrategy, Component, computed, input, model, ViewEncapsulation, } from "@angular/core";
2
2
  import { injectFullPageCodeSignal } from "../../core/routing/injectFullPageCodeSignal";
3
3
  import { getMenuRouterLinkOption as menuRouterLinkOption, getIsMenuSelected as menuIsSelected, } from "../../core/routing/menu-utils";
4
4
  import { NgTemplateOutlet } from "@angular/common";
@@ -6,57 +6,81 @@ import { SdTypedTemplate } from "../../core/template/sd-typed-template";
6
6
  import { SdRouterLink } from "../../core/routing/sd-router-link";
7
7
  import { SdList } from "../../controls/list/sd-list";
8
8
  import { SdListItem } from "../../controls/list/sd-list-item";
9
+ import { SdAnchor } from "../../controls/button/sd-anchor";
9
10
  import { NgIcon } from "@ng-icons/core";
11
+ import { tablerFoldDown, tablerFoldUp } from "@ng-icons/tabler-icons";
10
12
  import * as i0 from "@angular/core";
11
13
  const _c0 = a0 => ({ menus: a0, depth: 0 });
12
14
  const _c1 = (a0, a1) => ({ menus: a0, depth: a1 });
13
15
  const _forTrack0 = ($index, $item) => $item.codeChain.join(".");
14
- function SdSidebarMenu_ng_template_4_Template(rf, ctx) { }
15
- function SdSidebarMenu_ng_template_5_For_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
16
+ function SdSidebarMenu_Conditional_4_Template(rf, ctx) { if (rf & 1) {
17
+ const _r1 = i0.ɵɵgetCurrentView();
18
+ i0.ɵɵelementStart(0, "sd-anchor", 6);
19
+ i0.ɵɵlistener("click", function SdSidebarMenu_Conditional_4_Template_sd_anchor_click_0_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.expandAll()); });
20
+ i0.ɵɵelement(1, "ng-icon", 7);
21
+ i0.ɵɵelementEnd();
22
+ i0.ɵɵelementStart(2, "sd-anchor", 6);
23
+ i0.ɵɵlistener("click", function SdSidebarMenu_Conditional_4_Template_sd_anchor_click_2_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.collapseAll()); });
24
+ i0.ɵɵelement(3, "ng-icon", 7);
25
+ i0.ɵɵelementEnd();
26
+ } if (rf & 2) {
27
+ const ctx_r1 = i0.ɵɵnextContext();
28
+ i0.ɵɵproperty("theme", "gray")("disabled", ctx_r1.isAllExpanded());
29
+ i0.ɵɵattribute("title", "\uC804\uCCB4 \uD3BC\uCE58\uAE30");
30
+ i0.ɵɵadvance();
31
+ i0.ɵɵproperty("svg", ctx_r1.icons.tablerFoldDown);
32
+ i0.ɵɵadvance();
33
+ i0.ɵɵproperty("theme", "gray")("disabled", ctx_r1.isAllCollapsed());
34
+ i0.ɵɵattribute("title", "\uC804\uCCB4 \uC811\uAE30");
35
+ i0.ɵɵadvance();
36
+ i0.ɵɵproperty("svg", ctx_r1.icons.tablerFoldUp);
37
+ } }
38
+ function SdSidebarMenu_ng_template_6_Template(rf, ctx) { }
39
+ function SdSidebarMenu_ng_template_7_For_1_Conditional_1_Template(rf, ctx) { if (rf & 1) {
16
40
  i0.ɵɵelement(0, "ng-icon", 7);
17
41
  } if (rf & 2) {
18
- const menu_r2 = i0.ɵɵnextContext().$implicit;
19
- i0.ɵɵproperty("svg", menu_r2.icon);
42
+ const menu_r4 = i0.ɵɵnextContext().$implicit;
43
+ i0.ɵɵproperty("svg", menu_r4.icon);
20
44
  } }
21
- function SdSidebarMenu_ng_template_5_For_1_Conditional_3_ng_template_1_Template(rf, ctx) { }
22
- function SdSidebarMenu_ng_template_5_For_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
23
- i0.ɵɵelementStart(0, "sd-list", 8);
24
- i0.ɵɵtemplate(1, SdSidebarMenu_ng_template_5_For_1_Conditional_3_ng_template_1_Template, 0, 0, "ng-template", 9);
45
+ function SdSidebarMenu_ng_template_7_For_1_Conditional_3_ng_template_1_Template(rf, ctx) { }
46
+ function SdSidebarMenu_ng_template_7_For_1_Conditional_3_Template(rf, ctx) { if (rf & 1) {
47
+ i0.ɵɵelementStart(0, "sd-list", 10);
48
+ i0.ɵɵtemplate(1, SdSidebarMenu_ng_template_7_For_1_Conditional_3_ng_template_1_Template, 0, 0, "ng-template", 11);
25
49
  i0.ɵɵelementEnd();
26
50
  } if (rf & 2) {
27
- const menu_r2 = i0.ɵɵnextContext().$implicit;
28
- const depth_r4 = i0.ɵɵnextContext().depth;
51
+ const menu_r4 = i0.ɵɵnextContext().$implicit;
52
+ const depth_r5 = i0.ɵɵnextContext().depth;
29
53
  i0.ɵɵnextContext();
30
- const itemTpl_r5 = i0.ɵɵreference(6);
54
+ const itemTpl_r6 = i0.ɵɵreference(8);
31
55
  i0.ɵɵproperty("inset", true);
32
56
  i0.ɵɵadvance();
33
- i0.ɵɵproperty("ngTemplateOutlet", itemTpl_r5)("ngTemplateOutletInjector", "outlet")("ngTemplateOutletContext", i0.ɵɵpureFunction2(4, _c1, menu_r2.children, depth_r4 + 1));
57
+ i0.ɵɵproperty("ngTemplateOutlet", itemTpl_r6)("ngTemplateOutletInjector", "outlet")("ngTemplateOutletContext", i0.ɵɵpureFunction2(4, _c1, menu_r4.children, depth_r5 + 1));
34
58
  } }
35
- function SdSidebarMenu_ng_template_5_For_1_Template(rf, ctx) { if (rf & 1) {
36
- const _r1 = i0.ɵɵgetCurrentView();
37
- i0.ɵɵelementStart(0, "sd-list-item", 6);
38
- i0.ɵɵlistener("click", function SdSidebarMenu_ng_template_5_For_1_Template_sd_list_item_click_0_listener() { const menu_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onMenuClick(menu_r2)); });
39
- i0.ɵɵconditionalCreate(1, SdSidebarMenu_ng_template_5_For_1_Conditional_1_Template, 1, 1, "ng-icon", 7);
59
+ function SdSidebarMenu_ng_template_7_For_1_Template(rf, ctx) { if (rf & 1) {
60
+ const _r3 = i0.ɵɵgetCurrentView();
61
+ i0.ɵɵelementStart(0, "sd-list-item", 9);
62
+ i0.ɵɵlistener("click", function SdSidebarMenu_ng_template_7_For_1_Template_sd_list_item_click_0_listener() { const menu_r4 = i0.ɵɵrestoreView(_r3).$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onMenuClick(menu_r4)); })("openChange", function SdSidebarMenu_ng_template_7_For_1_Template_sd_list_item_openChange_0_listener($event) { const menu_r4 = i0.ɵɵrestoreView(_r3).$implicit; const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.onMenuOpenChange(menu_r4, $event)); });
63
+ i0.ɵɵconditionalCreate(1, SdSidebarMenu_ng_template_7_For_1_Conditional_1_Template, 1, 1, "ng-icon", 7);
40
64
  i0.ɵɵtext(2);
41
- i0.ɵɵconditionalCreate(3, SdSidebarMenu_ng_template_5_For_1_Conditional_3_Template, 2, 7, "sd-list", 8);
65
+ i0.ɵɵconditionalCreate(3, SdSidebarMenu_ng_template_7_For_1_Conditional_3_Template, 2, 7, "sd-list", 10);
42
66
  i0.ɵɵelementEnd();
43
67
  } if (rf & 2) {
44
- const menu_r2 = ctx.$implicit;
45
- const depth_r4 = i0.ɵɵnextContext().depth;
46
- const ctx_r2 = i0.ɵɵnextContext();
47
- i0.ɵɵproperty("sdRouterLink", ctx_r2.getMenuRouterLinkOption(menu_r2))("selected", ctx_r2.getIsMenuSelected(menu_r2))("layout", depth_r4 === 0 && ctx_r2.rootLayout() === "flat" ? "flat" : "accordion")("open", ctx_r2.expandInitially() && menu_r2.children != null);
68
+ const menu_r4 = ctx.$implicit;
69
+ const depth_r5 = i0.ɵɵnextContext().depth;
70
+ const ctx_r1 = i0.ɵɵnextContext();
71
+ i0.ɵɵproperty("sdRouterLink", ctx_r1.getMenuRouterLinkOption(menu_r4))("selected", ctx_r1.getIsMenuSelected(menu_r4))("layout", depth_r5 === 0 && ctx_r1.rootLayout() === "flat" ? "flat" : "accordion")("open", ctx_r1.getIsMenuExpanded(menu_r4));
48
72
  i0.ɵɵadvance();
49
- i0.ɵɵconditional(menu_r2.icon ? 1 : -1);
73
+ i0.ɵɵconditional(menu_r4.icon ? 1 : -1);
50
74
  i0.ɵɵadvance();
51
- i0.ɵɵtextInterpolate1(" ", menu_r2.title, " ");
75
+ i0.ɵɵtextInterpolate1(" ", menu_r4.title, " ");
52
76
  i0.ɵɵadvance();
53
- i0.ɵɵconditional(menu_r2.children ? 3 : -1);
77
+ i0.ɵɵconditional(menu_r4.children ? 3 : -1);
54
78
  } }
55
- function SdSidebarMenu_ng_template_5_Template(rf, ctx) { if (rf & 1) {
56
- i0.ɵɵrepeaterCreate(0, SdSidebarMenu_ng_template_5_For_1_Template, 4, 7, "sd-list-item", 5, _forTrack0);
79
+ function SdSidebarMenu_ng_template_7_Template(rf, ctx) { if (rf & 1) {
80
+ i0.ɵɵrepeaterCreate(0, SdSidebarMenu_ng_template_7_For_1_Template, 4, 7, "sd-list-item", 8, _forTrack0);
57
81
  } if (rf & 2) {
58
- const currMenus_r6 = ctx.menus;
59
- i0.ɵɵrepeater(currMenus_r6);
82
+ const currMenus_r7 = ctx.menus;
83
+ i0.ɵɵrepeater(currMenus_r7);
60
84
  } }
61
85
  export class SdSidebarMenu {
62
86
  menus = input([], /* @ts-ignore */
@@ -65,12 +89,82 @@ export class SdSidebarMenu {
65
89
  ...(ngDevMode ? [undefined, { debugName: "layout" }] : /* istanbul ignore next */ []));
66
90
  getMenuIsSelectedFn = input(/* @ts-ignore */
67
91
  ...(ngDevMode ? [undefined, { debugName: "getMenuIsSelectedFn" }] : /* istanbul ignore next */ []));
92
+ /**
93
+ * 펼쳐진 메뉴 코드(`codeChain.join(".")`) 목록.
94
+ *
95
+ * 미지정(`undefined`)이면 `layout` 이 정하는 기본 펼침 상태를 따름.
96
+ * 사용자가 토글하거나 호스트가 값을 세팅하면 그 값이 단일 진리원이 되며,
97
+ * 이후 `menus` 가 다시 계산돼도 상태가 유지됨.
98
+ */
99
+ expandedMenuCodes = model(undefined, /* @ts-ignore */
100
+ ...(ngDevMode ? [{ debugName: "expandedMenuCodes" }] : /* istanbul ignore next */ []));
68
101
  fullPageCode = injectFullPageCodeSignal();
69
102
  rootLayout = computed(() => this.layout() ?? (this.menus().length <= 3 ? "flat" : "accordion"), /* @ts-ignore */
70
103
  ...(ngDevMode ? [{ debugName: "rootLayout" }] : /* istanbul ignore next */ []));
71
- // accordion-expanded: 모든 깊이 항목을 펼친 채로 시작(이후 클릭 토글). 그 외는 접힘 시작.
72
- expandInitially = computed(() => this.rootLayout() === "accordion-expanded", /* @ts-ignore */
73
- ...(ngDevMode ? [{ debugName: "expandInitially" }] : /* istanbul ignore next */ []));
104
+ // 접기/펼치기가 가능한 메뉴 코드.
105
+ // flat 렌더되는 depth-0 그룹은 항상 펼쳐진 구조라 대상에서 제외(SdListItem.childrenOpen).
106
+ _expandableCodes = computed(() => {
107
+ const rootIsFlat = this.rootLayout() === "flat";
108
+ const result = [];
109
+ function walk(menus, depth) {
110
+ for (const menu of menus) {
111
+ if (menu.children == null)
112
+ continue;
113
+ if (!(rootIsFlat && depth === 0)) {
114
+ result.push(menu.codeChain.join("."));
115
+ }
116
+ walk(menu.children, depth + 1);
117
+ }
118
+ }
119
+ walk(this.menus(), 0);
120
+ return result;
121
+ }, /* @ts-ignore */
122
+ ...(ngDevMode ? [{ debugName: "_expandableCodes" }] : /* istanbul ignore next */ []));
123
+ // accordion-expanded: 하위 보유 메뉴 전체를 펼친 채로 시작. 그 외는 접힘 시작.
124
+ _expandedCodeSet = computed(() => {
125
+ const codes = this.expandedMenuCodes() ??
126
+ (this.rootLayout() === "accordion-expanded" ? this._expandableCodes() : []);
127
+ return new Set(codes);
128
+ }, /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "_expandedCodeSet" }] : /* istanbul ignore next */ []));
130
+ hasExpandable = computed(() => this._expandableCodes().length > 0, /* @ts-ignore */
131
+ ...(ngDevMode ? [{ debugName: "hasExpandable" }] : /* istanbul ignore next */ []));
132
+ isAllExpanded = computed(() => {
133
+ const codes = this._expandableCodes();
134
+ if (codes.length === 0)
135
+ return false;
136
+ const expandedSet = this._expandedCodeSet();
137
+ return codes.every((code) => expandedSet.has(code));
138
+ }, /* @ts-ignore */
139
+ ...(ngDevMode ? [{ debugName: "isAllExpanded" }] : /* istanbul ignore next */ []));
140
+ isAllCollapsed = computed(() => {
141
+ const codes = this._expandableCodes();
142
+ if (codes.length === 0)
143
+ return true;
144
+ const expandedSet = this._expandedCodeSet();
145
+ return codes.every((code) => !expandedSet.has(code));
146
+ }, /* @ts-ignore */
147
+ ...(ngDevMode ? [{ debugName: "isAllCollapsed" }] : /* istanbul ignore next */ []));
148
+ expandAll() {
149
+ this.expandedMenuCodes.set([...this._expandableCodes()]);
150
+ }
151
+ collapseAll() {
152
+ this.expandedMenuCodes.set([]);
153
+ }
154
+ getIsMenuExpanded(menu) {
155
+ return this._expandedCodeSet().has(menu.codeChain.join("."));
156
+ }
157
+ onMenuOpenChange(menu, open) {
158
+ const code = menu.codeChain.join(".");
159
+ const nextSet = new Set(this._expandedCodeSet());
160
+ if (open) {
161
+ nextSet.add(code);
162
+ }
163
+ else {
164
+ nextSet.delete(code);
165
+ }
166
+ this.expandedMenuCodes.set([...nextSet]);
167
+ }
74
168
  getMenuRouterLinkOption(menu) {
75
169
  return menuRouterLinkOption(menu);
76
170
  }
@@ -82,35 +176,77 @@ export class SdSidebarMenu {
82
176
  window.open(menu.url, "_blank");
83
177
  }
84
178
  }
179
+ icons = {
180
+ tablerFoldDown,
181
+ tablerFoldUp,
182
+ };
85
183
  itemTemplateType;
86
184
  static ɵfac = function SdSidebarMenu_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SdSidebarMenu)(); };
87
185
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SdSidebarMenu, selectors: [["sd-sidebar-menu"]], hostAttrs: [1, "flex-column", "fill"], hostVars: 1, hostBindings: function SdSidebarMenu_HostBindings(rf, ctx) { if (rf & 2) {
88
186
  i0.ɵɵattribute("data-sd-root-layout", ctx.rootLayout());
89
- } }, inputs: { menus: [1, "menus"], layout: [1, "layout"], getMenuIsSelectedFn: [1, "getMenuIsSelectedFn"] }, decls: 7, vars: 6, consts: [["itemTpl", ""], [1, "control-header", "p-default"], [1, "flex-fill", 3, "inset"], [3, "ngTemplateOutlet", "ngTemplateOutletContext"], [3, "typed"], [3, "sdRouterLink", "selected", "layout", "open"], [3, "click", "sdRouterLink", "selected", "layout", "open"], [3, "svg"], [3, "inset"], [3, "ngTemplateOutlet", "ngTemplateOutletInjector", "ngTemplateOutletContext"]], template: function SdSidebarMenu_Template(rf, ctx) { if (rf & 1) {
90
- i0.ɵɵelementStart(0, "div", 1)(1, "b");
91
- i0.ɵɵtext(2, "MENU");
187
+ } }, inputs: { menus: [1, "menus"], layout: [1, "layout"], getMenuIsSelectedFn: [1, "getMenuIsSelectedFn"], expandedMenuCodes: [1, "expandedMenuCodes"] }, outputs: { expandedMenuCodes: "expandedMenuCodesChange" }, decls: 9, vars: 7, consts: [["itemTpl", ""], [1, "control-header", "p-default", "flex-row"], [1, "flex-fill"], [1, "flex-fill", 3, "inset"], [3, "ngTemplateOutlet", "ngTemplateOutletContext"], [3, "typed"], [3, "click", "theme", "disabled"], [3, "svg"], [3, "sdRouterLink", "selected", "layout", "open"], [3, "click", "openChange", "sdRouterLink", "selected", "layout", "open"], [3, "inset"], [3, "ngTemplateOutlet", "ngTemplateOutletInjector", "ngTemplateOutletContext"]], template: function SdSidebarMenu_Template(rf, ctx) { if (rf & 1) {
188
+ i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "b");
189
+ i0.ɵɵtext(3, "MENU");
92
190
  i0.ɵɵelementEnd()();
93
- i0.ɵɵelementStart(3, "sd-list", 2);
94
- i0.ɵɵtemplate(4, SdSidebarMenu_ng_template_4_Template, 0, 0, "ng-template", 3);
191
+ i0.ɵɵconditionalCreate(4, SdSidebarMenu_Conditional_4_Template, 4, 8);
95
192
  i0.ɵɵelementEnd();
96
- i0.ɵɵtemplate(5, SdSidebarMenu_ng_template_5_Template, 2, 0, "ng-template", 4, 0, i0.ɵɵtemplateRefExtractor);
193
+ i0.ɵɵelementStart(5, "sd-list", 3);
194
+ i0.ɵɵtemplate(6, SdSidebarMenu_ng_template_6_Template, 0, 0, "ng-template", 4);
195
+ i0.ɵɵelementEnd();
196
+ i0.ɵɵtemplate(7, SdSidebarMenu_ng_template_7_Template, 2, 0, "ng-template", 5, 0, i0.ɵɵtemplateRefExtractor);
97
197
  } if (rf & 2) {
98
- const itemTpl_r5 = i0.ɵɵreference(6);
99
- i0.ɵɵadvance(3);
198
+ const itemTpl_r6 = i0.ɵɵreference(8);
199
+ i0.ɵɵadvance(4);
200
+ i0.ɵɵconditional(ctx.hasExpandable() ? 4 : -1);
201
+ i0.ɵɵadvance();
100
202
  i0.ɵɵproperty("inset", true);
101
203
  i0.ɵɵadvance();
102
- i0.ɵɵproperty("ngTemplateOutlet", itemTpl_r5)("ngTemplateOutletContext", i0.ɵɵpureFunction1(4, _c0, ctx.menus()));
204
+ i0.ɵɵproperty("ngTemplateOutlet", itemTpl_r6)("ngTemplateOutletContext", i0.ɵɵpureFunction1(5, _c0, ctx.menus()));
103
205
  i0.ɵɵadvance();
104
206
  i0.ɵɵproperty("typed", ctx.itemTemplateType);
105
- } }, dependencies: [NgTemplateOutlet, SdTypedTemplate, SdRouterLink, SdList, SdListItem, NgIcon], styles: ["sd-sidebar-menu[data-sd-root-layout=flat] > sd-list[data-sd-inset=true] > sd-list-item > sd-collapse > ._content > ._children > sd-list {\n background-color: transparent;\n}"], encapsulation: 2 });
207
+ } }, dependencies: [NgTemplateOutlet,
208
+ SdTypedTemplate,
209
+ SdRouterLink,
210
+ SdList,
211
+ SdListItem,
212
+ SdAnchor,
213
+ NgIcon], styles: ["sd-sidebar-menu > .control-header {\n align-items: center;\n gap: var(--%NS%sd-gap-xs);\n}\nsd-sidebar-menu[data-sd-root-layout=flat] > sd-list[data-sd-inset=true] > sd-list-item > sd-collapse > ._content > ._children > sd-list {\n background-color: transparent;\n}"], encapsulation: 2 });
106
214
  }
107
215
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SdSidebarMenu, [{
108
216
  type: Component,
109
- args: [{ selector: "sd-sidebar-menu", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, standalone: true, imports: [NgTemplateOutlet, SdTypedTemplate, SdRouterLink, SdList, SdListItem, NgIcon], host: {
217
+ args: [{ selector: "sd-sidebar-menu", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, standalone: true, imports: [
218
+ NgTemplateOutlet,
219
+ SdTypedTemplate,
220
+ SdRouterLink,
221
+ SdList,
222
+ SdListItem,
223
+ SdAnchor,
224
+ NgIcon,
225
+ ], host: {
110
226
  "class": "flex-column fill",
111
227
  "[attr.data-sd-root-layout]": "rootLayout()",
112
228
  }, template: `
113
- <div class="control-header p-default"><b>MENU</b></div>
229
+ <div class="control-header p-default flex-row">
230
+ <div class="flex-fill"><b>MENU</b></div>
231
+ @if (hasExpandable()) {
232
+ <sd-anchor
233
+ [theme]="'gray'"
234
+ [disabled]="isAllExpanded()"
235
+ [attr.title]="'전체 펼치기'"
236
+ (click)="expandAll()"
237
+ >
238
+ <ng-icon [svg]="icons.tablerFoldDown" />
239
+ </sd-anchor>
240
+ <sd-anchor
241
+ [theme]="'gray'"
242
+ [disabled]="isAllCollapsed()"
243
+ [attr.title]="'전체 접기'"
244
+ (click)="collapseAll()"
245
+ >
246
+ <ng-icon [svg]="icons.tablerFoldUp" />
247
+ </sd-anchor>
248
+ }
249
+ </div>
114
250
 
115
251
  <sd-list class="flex-fill" [inset]="true">
116
252
  <ng-template
@@ -126,7 +262,8 @@ export class SdSidebarMenu {
126
262
  (click)="onMenuClick(menu)"
127
263
  [selected]="getIsMenuSelected(menu)"
128
264
  [layout]="depth === 0 && rootLayout() === 'flat' ? 'flat' : 'accordion'"
129
- [open]="expandInitially() && menu.children != null"
265
+ [open]="getIsMenuExpanded(menu)"
266
+ (openChange)="onMenuOpenChange(menu, $event)"
130
267
  >
131
268
  @if (menu.icon) {
132
269
  <ng-icon [svg]="menu.icon" />
@@ -147,6 +284,6 @@ export class SdSidebarMenu {
147
284
  </sd-list-item>
148
285
  }
149
286
  </ng-template>
150
- `, styles: ["sd-sidebar-menu[data-sd-root-layout=flat] > sd-list[data-sd-inset=true] > sd-list-item > sd-collapse > ._content > ._children > sd-list {\n background-color: transparent;\n}"] }]
151
- }], null, { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], getMenuIsSelectedFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "getMenuIsSelectedFn", required: false }] }] }); })();
152
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SdSidebarMenu, { className: "SdSidebarMenu", filePath: "packages/angular/src/layout/sidebar/sd-sidebar-menu.ts", lineNumber: 87 }); })();
287
+ `, styles: ["sd-sidebar-menu > .control-header {\n align-items: center;\n gap: var(--sd-gap-xs);\n}\nsd-sidebar-menu[data-sd-root-layout=flat] > sd-list[data-sd-inset=true] > sd-list-item > sd-collapse > ._content > ._children > sd-list {\n background-color: transparent;\n}"] }]
288
+ }], null, { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], getMenuIsSelectedFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "getMenuIsSelectedFn", required: false }] }], expandedMenuCodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedMenuCodes", required: false }] }, { type: i0.Output, args: ["expandedMenuCodesChange"] }] }); })();
289
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SdSidebarMenu, { className: "SdSidebarMenu", filePath: "packages/angular/src/layout/sidebar/sd-sidebar-menu.ts", lineNumber: 124 }); })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/angular",
3
- "version": "14.2.14",
3
+ "version": "14.2.17",
4
4
  "description": "심플리즘 패키지 - Angular",
5
5
  "license": "Apache-2.0",
6
6
  "author": "심플리즘",
@@ -58,10 +58,10 @@
58
58
  "jspdf": "^4.2.1",
59
59
  "rxjs": "^7.8.2",
60
60
  "tabbable": "^6.5.0",
61
- "@simplysm/service-client": "14.2.14",
62
- "@simplysm/core-browser": "14.2.14",
63
- "@simplysm/core-common": "14.2.14",
64
- "@simplysm/service-common": "14.2.14"
61
+ "@simplysm/core-common": "14.2.17",
62
+ "@simplysm/service-client": "14.2.17",
63
+ "@simplysm/core-browser": "14.2.17",
64
+ "@simplysm/service-common": "14.2.17"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@angular/compiler": "^22.1.0"
@@ -3,6 +3,7 @@ import {
3
3
  Component,
4
4
  computed,
5
5
  input,
6
+ model,
6
7
  ViewEncapsulation,
7
8
  } from "@angular/core";
8
9
  import { injectFullPageCodeSignal } from "../../core/routing/injectFullPageCodeSignal";
@@ -16,20 +17,50 @@ import { SdTypedTemplate } from "../../core/template/sd-typed-template";
16
17
  import { SdRouterLink } from "../../core/routing/sd-router-link";
17
18
  import { SdList } from "../../controls/list/sd-list";
18
19
  import { SdListItem } from "../../controls/list/sd-list-item";
20
+ import { SdAnchor } from "../../controls/button/sd-anchor";
19
21
  import { NgIcon } from "@ng-icons/core";
22
+ import { tablerFoldDown, tablerFoldUp } from "@ng-icons/tabler-icons";
20
23
 
21
24
  @Component({
22
25
  selector: "sd-sidebar-menu",
23
26
  changeDetection: ChangeDetectionStrategy.OnPush,
24
27
  encapsulation: ViewEncapsulation.None,
25
28
  standalone: true,
26
- imports: [NgTemplateOutlet, SdTypedTemplate, SdRouterLink, SdList, SdListItem, NgIcon],
29
+ imports: [
30
+ NgTemplateOutlet,
31
+ SdTypedTemplate,
32
+ SdRouterLink,
33
+ SdList,
34
+ SdListItem,
35
+ SdAnchor,
36
+ NgIcon,
37
+ ],
27
38
  host: {
28
39
  "class": "flex-column fill",
29
40
  "[attr.data-sd-root-layout]": "rootLayout()",
30
41
  },
31
42
  template: `
32
- <div class="control-header p-default"><b>MENU</b></div>
43
+ <div class="control-header p-default flex-row">
44
+ <div class="flex-fill"><b>MENU</b></div>
45
+ @if (hasExpandable()) {
46
+ <sd-anchor
47
+ [theme]="'gray'"
48
+ [disabled]="isAllExpanded()"
49
+ [attr.title]="'전체 펼치기'"
50
+ (click)="expandAll()"
51
+ >
52
+ <ng-icon [svg]="icons.tablerFoldDown" />
53
+ </sd-anchor>
54
+ <sd-anchor
55
+ [theme]="'gray'"
56
+ [disabled]="isAllCollapsed()"
57
+ [attr.title]="'전체 접기'"
58
+ (click)="collapseAll()"
59
+ >
60
+ <ng-icon [svg]="icons.tablerFoldUp" />
61
+ </sd-anchor>
62
+ }
63
+ </div>
33
64
 
34
65
  <sd-list class="flex-fill" [inset]="true">
35
66
  <ng-template
@@ -45,7 +76,8 @@ import { NgIcon } from "@ng-icons/core";
45
76
  (click)="onMenuClick(menu)"
46
77
  [selected]="getIsMenuSelected(menu)"
47
78
  [layout]="depth === 0 && rootLayout() === 'flat' ? 'flat' : 'accordion'"
48
- [open]="expandInitially() && menu.children != null"
79
+ [open]="getIsMenuExpanded(menu)"
80
+ (openChange)="onMenuOpenChange(menu, $event)"
49
81
  >
50
82
  @if (menu.icon) {
51
83
  <ng-icon [svg]="menu.icon" />
@@ -70,6 +102,11 @@ import { NgIcon } from "@ng-icons/core";
70
102
  styles: [
71
103
  /* language=SCSS */ `
72
104
  sd-sidebar-menu {
105
+ > .control-header {
106
+ align-items: center;
107
+ gap: var(--sd-gap-xs);
108
+ }
109
+
73
110
  &[data-sd-root-layout="flat"] {
74
111
  > sd-list[data-sd-inset="true"]
75
112
  > sd-list-item
@@ -89,12 +126,85 @@ export class SdSidebarMenu {
89
126
  layout = input<"accordion" | "accordion-expanded" | "flat">();
90
127
  getMenuIsSelectedFn = input<(menu: SdMenu) => boolean>();
91
128
 
129
+ /**
130
+ * 펼쳐진 메뉴 코드(`codeChain.join(".")`) 목록.
131
+ *
132
+ * 미지정(`undefined`)이면 `layout` 이 정하는 기본 펼침 상태를 따름.
133
+ * 사용자가 토글하거나 호스트가 값을 세팅하면 그 값이 단일 진리원이 되며,
134
+ * 이후 `menus` 가 다시 계산돼도 상태가 유지됨.
135
+ */
136
+ expandedMenuCodes = model<string[] | undefined>(undefined);
137
+
92
138
  fullPageCode = injectFullPageCodeSignal();
93
139
 
94
140
  rootLayout = computed(() => this.layout() ?? (this.menus().length <= 3 ? "flat" : "accordion"));
95
141
 
96
- // accordion-expanded: 모든 깊이 항목을 펼친 채로 시작(이후 클릭 토글). 그 외는 접힘 시작.
97
- expandInitially = computed(() => this.rootLayout() === "accordion-expanded");
142
+ // 접기/펼치기가 가능한 메뉴 코드.
143
+ // flat 렌더되는 depth-0 그룹은 항상 펼쳐진 구조라 대상에서 제외(SdListItem.childrenOpen).
144
+ private readonly _expandableCodes = computed(() => {
145
+ const rootIsFlat = this.rootLayout() === "flat";
146
+ const result: string[] = [];
147
+
148
+ function walk(menus: SdMenu[], depth: number): void {
149
+ for (const menu of menus) {
150
+ if (menu.children == null) continue;
151
+ if (!(rootIsFlat && depth === 0)) {
152
+ result.push(menu.codeChain.join("."));
153
+ }
154
+ walk(menu.children, depth + 1);
155
+ }
156
+ }
157
+
158
+ walk(this.menus(), 0);
159
+ return result;
160
+ });
161
+
162
+ // accordion-expanded: 하위 보유 메뉴 전체를 펼친 채로 시작. 그 외는 접힘 시작.
163
+ private readonly _expandedCodeSet = computed(() => {
164
+ const codes =
165
+ this.expandedMenuCodes() ??
166
+ (this.rootLayout() === "accordion-expanded" ? this._expandableCodes() : []);
167
+ return new Set(codes);
168
+ });
169
+
170
+ hasExpandable = computed(() => this._expandableCodes().length > 0);
171
+
172
+ isAllExpanded = computed(() => {
173
+ const codes = this._expandableCodes();
174
+ if (codes.length === 0) return false;
175
+ const expandedSet = this._expandedCodeSet();
176
+ return codes.every((code) => expandedSet.has(code));
177
+ });
178
+
179
+ isAllCollapsed = computed(() => {
180
+ const codes = this._expandableCodes();
181
+ if (codes.length === 0) return true;
182
+ const expandedSet = this._expandedCodeSet();
183
+ return codes.every((code) => !expandedSet.has(code));
184
+ });
185
+
186
+ expandAll(): void {
187
+ this.expandedMenuCodes.set([...this._expandableCodes()]);
188
+ }
189
+
190
+ collapseAll(): void {
191
+ this.expandedMenuCodes.set([]);
192
+ }
193
+
194
+ getIsMenuExpanded(menu: SdMenu): boolean {
195
+ return this._expandedCodeSet().has(menu.codeChain.join("."));
196
+ }
197
+
198
+ onMenuOpenChange(menu: SdMenu, open: boolean): void {
199
+ const code = menu.codeChain.join(".");
200
+ const nextSet = new Set(this._expandedCodeSet());
201
+ if (open) {
202
+ nextSet.add(code);
203
+ } else {
204
+ nextSet.delete(code);
205
+ }
206
+ this.expandedMenuCodes.set([...nextSet]);
207
+ }
98
208
 
99
209
  getMenuRouterLinkOption(
100
210
  menu: SdMenu,
@@ -112,6 +222,11 @@ export class SdSidebarMenu {
112
222
  }
113
223
  }
114
224
 
225
+ protected readonly icons = {
226
+ tablerFoldDown,
227
+ tablerFoldUp,
228
+ };
229
+
115
230
  protected readonly itemTemplateType!: {
116
231
  menus: SdMenu[];
117
232
  depth: number;