rails_modal_manager 1.0.9 → 1.0.10
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.
- checksums.yaml +4 -4
- data/README.md +16 -11
- data/app/assets/stylesheets/rails_modal_manager.css +129 -23
- data/app/helpers/rails_modal_manager/modal_helper.rb +41 -1
- data/app/javascript/rails_modal_manager/controllers/rmm_header_controller.js +37 -2
- data/app/javascript/rails_modal_manager/controllers/rmm_modal_controller.js +1 -1
- data/app/javascript/rails_modal_manager/controllers/rmm_sidebar_controller.js +116 -3
- data/app/javascript/rails_modal_manager/controllers/rmm_submenu_controller.js +188 -3
- data/app/javascript/rails_modal_manager/index.js +20 -1
- data/app/javascript/rails_modal_manager/modal_store.js +12 -1
- data/app/views/rails_modal_manager/_header.html.erb +19 -17
- data/app/views/rails_modal_manager/_modal.html.erb +5 -2
- data/app/views/rails_modal_manager/_sidebar.html.erb +7 -1
- data/app/views/rails_modal_manager/_submenu.html.erb +39 -21
- data/lib/rails_modal_manager/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02b2719477c30526ae7b96cff7a9f494855477568ec11279bb48c6009370b3f7
|
|
4
|
+
data.tar.gz: d88ca69cd83c2db76f2b5c9132bb652aeacdddbcb2afdc69189b9688e35081ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f82434fb170f66af13e95d040dfaf4c381360c345d6cbf55e70022624733e808e2aa422eb9d4dcdd862428578f5cc217b232b5d7ecf2145493671c8b6cc02939
|
|
7
|
+
data.tar.gz: 5e1c4d5fe59746d264a1a99ae443f383943a30e31aa660eda90527eeff99cd81f60e120cc46eee5cf633e92c58d32e7a267cc52374a07623268fd54ebf562e11
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Rails 애플리케이션을 위한 고급 모달 매니저입니다.
|
|
4
4
|
`@reshacs/react-modal-manager`에서 포팅되었습니다.
|
|
5
5
|
|
|
6
|
-
**Version:** 1.0.
|
|
6
|
+
**Version:** 1.0.10
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -47,7 +47,7 @@ bundle install
|
|
|
47
47
|
|
|
48
48
|
```javascript
|
|
49
49
|
import { Application } from "@hotwired/stimulus"
|
|
50
|
-
import { registerRMMControllers, initHistoryStack } from "rails_modal_manager"
|
|
50
|
+
import { registerRMMControllers, initHistoryStack, initGlobalClickHandler } from "rails_modal_manager"
|
|
51
51
|
|
|
52
52
|
const application = Application.start()
|
|
53
53
|
|
|
@@ -56,6 +56,9 @@ registerRMMControllers(application)
|
|
|
56
56
|
|
|
57
57
|
// 히스토리 스택 초기화 (브라우저 뒤로가기 지원)
|
|
58
58
|
initHistoryStack()
|
|
59
|
+
|
|
60
|
+
// 글로벌 클릭 핸들러 초기화 (data-rmm-modal-id 버튼 지원)
|
|
61
|
+
initGlobalClickHandler()
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
### 4. CSS 설정
|
|
@@ -180,7 +183,7 @@ closeModal('my-modal')
|
|
|
180
183
|
|------|------|--------|------|
|
|
181
184
|
| `modal-id` | String | - | 모달 고유 ID (필수) |
|
|
182
185
|
| `title` | String | "Modal" | 모달 제목 |
|
|
183
|
-
| `size` | String | "md" | 크기 (xs, sm, md, lg, xl, full) |
|
|
186
|
+
| `size` | String | "md" | 크기 (fit, xss, xs, sm, md, lg, xl, full) |
|
|
184
187
|
| `position` | String | "center" | 위치 (center, top, bottom, top-left, top-right, bottom-left, bottom-right) |
|
|
185
188
|
| `height` | String | "auto" | 높이 (auto, 300px, 50vh 등) |
|
|
186
189
|
| `draggable` | Boolean | false | 드래그 가능 여부 |
|
|
@@ -202,14 +205,16 @@ closeModal('my-modal')
|
|
|
202
205
|
|
|
203
206
|
### 크기 (Size)
|
|
204
207
|
|
|
205
|
-
| 값 | 너비 | 최소 너비 |
|
|
206
|
-
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
208
|
+
| 값 | 너비 | 최소 너비 | 설명 |
|
|
209
|
+
|----|------|----------|------|
|
|
210
|
+
| fit | auto | 150px | 콘텐츠에 맞춰 자동 조절 |
|
|
211
|
+
| xss | 20% | 200px | 매우 작은 크기 |
|
|
212
|
+
| xs | 30% | 280px | 작은 크기 |
|
|
213
|
+
| sm | 40% | 360px | 중소 크기 |
|
|
214
|
+
| md | 50% | 480px | 중간 크기 (기본값) |
|
|
215
|
+
| lg | 60% | 600px | 큰 크기 |
|
|
216
|
+
| xl | 70% | 720px | 매우 큰 크기 |
|
|
217
|
+
| full | 100% | 100% | 전체 화면 |
|
|
213
218
|
|
|
214
219
|
---
|
|
215
220
|
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
--rmm-sidebar-item-active-text: #3b82f6;
|
|
42
42
|
|
|
43
43
|
/* Submenu */
|
|
44
|
-
--rmm-submenu-bg: #
|
|
45
|
-
--rmm-submenu-
|
|
46
|
-
--rmm-submenu-height:
|
|
44
|
+
--rmm-submenu-bg: #f1f5f9;
|
|
45
|
+
--rmm-submenu-container-bg: #ffffff;
|
|
46
|
+
--rmm-submenu-height: 56px;
|
|
47
47
|
--rmm-submenu-item-text: #64748b;
|
|
48
|
-
--rmm-submenu-item-hover: rgba(0, 0, 0, 0.
|
|
49
|
-
--rmm-submenu-item-active-bg: #
|
|
50
|
-
--rmm-submenu-item-active-text: #
|
|
48
|
+
--rmm-submenu-item-hover-bg: rgba(0, 0, 0, 0.04);
|
|
49
|
+
--rmm-submenu-item-active-bg: #3b82f6;
|
|
50
|
+
--rmm-submenu-item-active-text: #ffffff;
|
|
51
51
|
|
|
52
52
|
/* Content */
|
|
53
53
|
--rmm-content-bg: #ffffff;
|
|
@@ -121,11 +121,12 @@
|
|
|
121
121
|
--rmm-sidebar-item-hover: rgba(255, 255, 255, 0.05);
|
|
122
122
|
--rmm-sidebar-item-active-bg: rgba(59, 130, 246, 0.2);
|
|
123
123
|
|
|
124
|
-
--rmm-submenu-bg: #
|
|
125
|
-
--rmm-submenu-
|
|
124
|
+
--rmm-submenu-bg: #1e293b;
|
|
125
|
+
--rmm-submenu-container-bg: #0f172a;
|
|
126
126
|
--rmm-submenu-item-text: #94a3b8;
|
|
127
|
-
--rmm-submenu-item-hover: rgba(255, 255, 255, 0.
|
|
128
|
-
--rmm-submenu-item-active-bg: #
|
|
127
|
+
--rmm-submenu-item-hover-bg: rgba(255, 255, 255, 0.08);
|
|
128
|
+
--rmm-submenu-item-active-bg: #3b82f6;
|
|
129
|
+
--rmm-submenu-item-active-text: #ffffff;
|
|
129
130
|
|
|
130
131
|
--rmm-content-bg: #1e293b;
|
|
131
132
|
--rmm-content-text: #f1f5f9;
|
|
@@ -201,11 +202,12 @@
|
|
|
201
202
|
transform: scale(1) translateY(0);
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/* Minimized state with fade animation */
|
|
205
|
+
/* Minimized state with fade animation - animates to bottom-left */
|
|
205
206
|
.rmm-modal.rmm-minimized {
|
|
206
207
|
opacity: 0 !important;
|
|
207
208
|
visibility: hidden !important;
|
|
208
|
-
transform: scale(0.
|
|
209
|
+
transform: scale(0.2) !important;
|
|
210
|
+
transform-origin: 0% 100% !important;
|
|
209
211
|
pointer-events: none !important;
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -217,6 +219,8 @@
|
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
/* Size variants */
|
|
222
|
+
.rmm-modal.rmm-size-fit { width: auto; min-width: 150px; }
|
|
223
|
+
.rmm-modal.rmm-size-xss { width: 20%; min-width: 200px; }
|
|
220
224
|
.rmm-modal.rmm-size-xs { width: 30%; min-width: 280px; }
|
|
221
225
|
.rmm-modal.rmm-size-sm { width: 40%; min-width: 360px; }
|
|
222
226
|
.rmm-modal.rmm-size-md { width: 50%; min-width: 480px; }
|
|
@@ -685,6 +689,7 @@
|
|
|
685
689
|
transition: opacity var(--rmm-animation-duration) var(--rmm-animation-timing),
|
|
686
690
|
visibility var(--rmm-animation-duration) var(--rmm-animation-timing);
|
|
687
691
|
z-index: 9;
|
|
692
|
+
cursor: pointer;
|
|
688
693
|
}
|
|
689
694
|
|
|
690
695
|
.rmm-sidebar:not(.rmm-sidebar-collapsed) ~ .rmm-sidebar-overlay {
|
|
@@ -694,20 +699,29 @@
|
|
|
694
699
|
}
|
|
695
700
|
|
|
696
701
|
/* ============================================
|
|
697
|
-
Modal Submenu
|
|
702
|
+
Modal Submenu (Pill/Tab Style)
|
|
698
703
|
============================================ */
|
|
699
704
|
.rmm-submenu {
|
|
700
705
|
display: flex;
|
|
701
706
|
align-items: center;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
+
justify-content: flex-start;
|
|
708
|
+
gap: 6px;
|
|
709
|
+
padding: 8px 12px;
|
|
710
|
+
min-height: var(--rmm-submenu-height);
|
|
711
|
+
background: var(--rmm-submenu-container-bg);
|
|
707
712
|
overflow-x: auto;
|
|
708
713
|
flex-shrink: 0;
|
|
709
714
|
}
|
|
710
715
|
|
|
716
|
+
.rmm-submenu-inner {
|
|
717
|
+
display: flex;
|
|
718
|
+
align-items: center;
|
|
719
|
+
gap: 4px;
|
|
720
|
+
padding: 4px;
|
|
721
|
+
background: var(--rmm-submenu-bg);
|
|
722
|
+
border-radius: 8px;
|
|
723
|
+
}
|
|
724
|
+
|
|
711
725
|
.rmm-submenu::-webkit-scrollbar {
|
|
712
726
|
height: 4px;
|
|
713
727
|
}
|
|
@@ -724,9 +738,10 @@
|
|
|
724
738
|
.rmm-submenu-item {
|
|
725
739
|
display: flex;
|
|
726
740
|
align-items: center;
|
|
741
|
+
justify-content: center;
|
|
727
742
|
gap: 6px;
|
|
728
|
-
padding: 8px
|
|
729
|
-
font-size:
|
|
743
|
+
padding: 8px 16px;
|
|
744
|
+
font-size: 14px;
|
|
730
745
|
font-weight: 500;
|
|
731
746
|
color: var(--rmm-submenu-item-text);
|
|
732
747
|
background: transparent;
|
|
@@ -734,16 +749,23 @@
|
|
|
734
749
|
border-radius: 6px;
|
|
735
750
|
cursor: pointer;
|
|
736
751
|
white-space: nowrap;
|
|
737
|
-
transition: background-color 0.
|
|
752
|
+
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
|
738
753
|
}
|
|
739
754
|
|
|
740
|
-
.rmm-submenu-item:hover {
|
|
741
|
-
background: var(--rmm-submenu-item-hover);
|
|
755
|
+
.rmm-submenu-item:hover:not(.rmm-active):not(.rmm-disabled) {
|
|
756
|
+
background: var(--rmm-submenu-item-hover-bg);
|
|
757
|
+
color: var(--rmm-content-text);
|
|
742
758
|
}
|
|
743
759
|
|
|
744
760
|
.rmm-submenu-item.rmm-active {
|
|
745
761
|
background: var(--rmm-submenu-item-active-bg);
|
|
746
762
|
color: var(--rmm-submenu-item-active-text);
|
|
763
|
+
box-shadow: 0 1px 3px rgba(59, 130, 246, 0.3);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.rmm-submenu-item.rmm-disabled {
|
|
767
|
+
opacity: 0.5;
|
|
768
|
+
cursor: not-allowed;
|
|
747
769
|
}
|
|
748
770
|
|
|
749
771
|
.rmm-submenu-item-icon {
|
|
@@ -754,6 +776,90 @@
|
|
|
754
776
|
height: 16px;
|
|
755
777
|
}
|
|
756
778
|
|
|
779
|
+
/* ============================================
|
|
780
|
+
Tab Panels (Submenu Content)
|
|
781
|
+
============================================ */
|
|
782
|
+
.rmm-tab-panel {
|
|
783
|
+
display: none;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.rmm-tab-panel.rmm-tab-panel-active {
|
|
787
|
+
display: block;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* AJAX Content Container */
|
|
791
|
+
.rmm-tab-content {
|
|
792
|
+
min-height: 100px;
|
|
793
|
+
position: relative;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.rmm-tab-content.rmm-tab-loading {
|
|
797
|
+
min-height: 150px;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/* Loading State */
|
|
801
|
+
.rmm-tab-loader {
|
|
802
|
+
display: flex;
|
|
803
|
+
flex-direction: column;
|
|
804
|
+
align-items: center;
|
|
805
|
+
justify-content: center;
|
|
806
|
+
gap: 12px;
|
|
807
|
+
padding: 40px 20px;
|
|
808
|
+
color: var(--rmm-submenu-item-text);
|
|
809
|
+
font-size: 14px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.rmm-spinner {
|
|
813
|
+
width: 32px;
|
|
814
|
+
height: 32px;
|
|
815
|
+
border: 3px solid var(--rmm-modal-border);
|
|
816
|
+
border-top-color: var(--rmm-btn-primary-bg);
|
|
817
|
+
border-radius: 50%;
|
|
818
|
+
animation: rmm-spin 0.8s linear infinite;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* Error State */
|
|
822
|
+
.rmm-tab-error {
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
align-items: center;
|
|
826
|
+
justify-content: center;
|
|
827
|
+
gap: 16px;
|
|
828
|
+
padding: 40px 20px;
|
|
829
|
+
color: var(--rmm-btn-danger-bg);
|
|
830
|
+
font-size: 14px;
|
|
831
|
+
text-align: center;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* ============================================
|
|
835
|
+
Sidebar Panels (Sidebar Content Switching)
|
|
836
|
+
============================================ */
|
|
837
|
+
.rmm-sidebar-panel {
|
|
838
|
+
display: none;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.rmm-sidebar-panel.rmm-sidebar-panel-active {
|
|
842
|
+
display: block;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* ============================================
|
|
846
|
+
Submenu Groups (Sidebar-linked Submenus)
|
|
847
|
+
============================================ */
|
|
848
|
+
.rmm-submenu-group {
|
|
849
|
+
display: none;
|
|
850
|
+
flex-shrink: 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.rmm-submenu-group.rmm-submenu-group-active {
|
|
854
|
+
display: block;
|
|
855
|
+
flex-shrink: 0;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/* Wrapper for submenu groups - contains multiple submenus */
|
|
859
|
+
.rmm-submenu-container {
|
|
860
|
+
display: contents;
|
|
861
|
+
}
|
|
862
|
+
|
|
757
863
|
/* ============================================
|
|
758
864
|
Modal Footer
|
|
759
865
|
============================================ */
|
|
@@ -94,12 +94,18 @@ module RailsModalManager
|
|
|
94
94
|
# @param items [Array<Hash>] Array of item definitions
|
|
95
95
|
# @return [Array<Hash>] Formatted sidebar items
|
|
96
96
|
#
|
|
97
|
-
# @example
|
|
97
|
+
# @example Basic sidebar
|
|
98
98
|
# rmm_sidebar_items([
|
|
99
99
|
# { id: "home", label: "Home", icon: "home", active: true },
|
|
100
100
|
# { id: "settings", label: "Settings", icon: "settings", badge: "3" }
|
|
101
101
|
# ])
|
|
102
102
|
#
|
|
103
|
+
# @example With panel and submenu linking
|
|
104
|
+
# rmm_sidebar_items([
|
|
105
|
+
# { id: "home", label: "Home", panel_id: "home-panel", submenu_id: "home-submenu", active: true },
|
|
106
|
+
# { id: "settings", label: "Settings", panel_id: "settings-panel" }
|
|
107
|
+
# ])
|
|
108
|
+
#
|
|
103
109
|
def rmm_sidebar_items(items)
|
|
104
110
|
items.map do |item|
|
|
105
111
|
{
|
|
@@ -107,6 +113,8 @@ module RailsModalManager
|
|
|
107
113
|
label: item[:label],
|
|
108
114
|
icon_svg: item[:icon_svg] || item[:icon],
|
|
109
115
|
badge: item[:badge],
|
|
116
|
+
panel_id: item[:panel_id],
|
|
117
|
+
submenu_id: item[:submenu_id],
|
|
110
118
|
active: item[:active] || false,
|
|
111
119
|
disabled: item[:disabled] || false
|
|
112
120
|
}
|
|
@@ -137,6 +145,38 @@ module RailsModalManager
|
|
|
137
145
|
end
|
|
138
146
|
end
|
|
139
147
|
|
|
148
|
+
# Helper to create submenu items array
|
|
149
|
+
#
|
|
150
|
+
# @param items [Array<Hash>] Array of item definitions
|
|
151
|
+
# @return [Array<Hash>] Formatted submenu items
|
|
152
|
+
#
|
|
153
|
+
# @example Preload mode (with panel_id)
|
|
154
|
+
# rmm_submenu_items([
|
|
155
|
+
# { id: "tab1", label: "Tab 1", panel_id: "panel-1", active: true },
|
|
156
|
+
# { id: "tab2", label: "Tab 2", panel_id: "panel-2" },
|
|
157
|
+
# { id: "tab3", label: "Tab 3", panel_id: "panel-3", disabled: true }
|
|
158
|
+
# ])
|
|
159
|
+
#
|
|
160
|
+
# @example AJAX mode (with url)
|
|
161
|
+
# rmm_submenu_items([
|
|
162
|
+
# { id: "tab1", label: "Tab 1", url: "/content/tab1", active: true },
|
|
163
|
+
# { id: "tab2", label: "Tab 2", url: "/content/tab2" }
|
|
164
|
+
# ])
|
|
165
|
+
#
|
|
166
|
+
def rmm_submenu_items(items)
|
|
167
|
+
items.map do |item|
|
|
168
|
+
{
|
|
169
|
+
id: item[:id],
|
|
170
|
+
label: item[:label],
|
|
171
|
+
icon_svg: item[:icon_svg] || item[:icon],
|
|
172
|
+
panel_id: item[:panel_id],
|
|
173
|
+
url: item[:url],
|
|
174
|
+
active: item[:active] || false,
|
|
175
|
+
disabled: item[:disabled] || false
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
140
180
|
# Common icon SVGs for sidebar/submenu
|
|
141
181
|
module Icons
|
|
142
182
|
def self.home
|
|
@@ -11,10 +11,22 @@ export default class extends Controller {
|
|
|
11
11
|
defaultSize: { type: String, default: "md" },
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
static targets = ["maximizeBtn"]
|
|
14
|
+
static targets = ["maximizeBtn", "sidebarToggle"]
|
|
15
15
|
|
|
16
16
|
connect() {
|
|
17
|
-
//
|
|
17
|
+
// Listen for sidebar toggled event to update icon
|
|
18
|
+
this.handleSidebarToggled = this.handleSidebarToggled.bind(this)
|
|
19
|
+
const modal = this.element.closest('.rmm-modal')
|
|
20
|
+
if (modal) {
|
|
21
|
+
modal.addEventListener('rmm-sidebar:toggled', this.handleSidebarToggled)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
disconnect() {
|
|
26
|
+
const modal = this.element.closest('.rmm-modal')
|
|
27
|
+
if (modal) {
|
|
28
|
+
modal.removeEventListener('rmm-sidebar:toggled', this.handleSidebarToggled)
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
// Helper method to find the modal element
|
|
@@ -123,4 +135,27 @@ export default class extends Controller {
|
|
|
123
135
|
controller.changeSize(newSize)
|
|
124
136
|
}
|
|
125
137
|
}
|
|
138
|
+
|
|
139
|
+
// Toggle sidebar and dispatch event
|
|
140
|
+
toggleSidebar() {
|
|
141
|
+
const modal = this.getModalElement()
|
|
142
|
+
if (!modal) return
|
|
143
|
+
|
|
144
|
+
const sidebar = modal.querySelector('.rmm-sidebar')
|
|
145
|
+
if (sidebar) {
|
|
146
|
+
sidebar.dispatchEvent(new CustomEvent('rmm-sidebar:toggle', { bubbles: false }))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle sidebar toggled event to update icon
|
|
151
|
+
handleSidebarToggled(e) {
|
|
152
|
+
if (!this.hasSidebarToggleTarget) return
|
|
153
|
+
|
|
154
|
+
const isCollapsed = e.detail.collapsed
|
|
155
|
+
const collapseIcon = this.sidebarToggleTarget.querySelector('.rmm-icon-collapse')
|
|
156
|
+
const expandIcon = this.sidebarToggleTarget.querySelector('.rmm-icon-expand')
|
|
157
|
+
|
|
158
|
+
if (collapseIcon) collapseIcon.style.display = isCollapsed ? 'none' : 'block'
|
|
159
|
+
if (expandIcon) expandIcon.style.display = isCollapsed ? 'block' : 'none'
|
|
160
|
+
}
|
|
126
161
|
}
|
|
@@ -369,7 +369,7 @@ export default class extends Controller {
|
|
|
369
369
|
const posConfig = POSITION_CONFIG[config.position] || POSITION_CONFIG.center
|
|
370
370
|
|
|
371
371
|
// Update size CSS class
|
|
372
|
-
const sizeClasses = ['xs', 'sm', 'md', 'lg', 'xl', 'full']
|
|
372
|
+
const sizeClasses = ['fit', 'xss', 'xs', 'sm', 'md', 'lg', 'xl', 'full']
|
|
373
373
|
sizeClasses.forEach(s => modal.classList.remove(`rmm-size-${s}`))
|
|
374
374
|
modal.classList.add(`rmm-size-${config.size}`)
|
|
375
375
|
|
|
@@ -3,6 +3,23 @@ import modalStore from "rails_modal_manager/modal_store"
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Rails Modal Manager - Sidebar Controller
|
|
6
|
+
*
|
|
7
|
+
* Supports content panel switching and submenu integration.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```html
|
|
11
|
+
* <div class="rmm-sidebar" data-controller="rmm-sidebar">
|
|
12
|
+
* <button data-item-id="home" data-panel-id="home-panel" data-submenu-id="home-submenu">Home</button>
|
|
13
|
+
* <button data-item-id="settings" data-panel-id="settings-panel">Settings (no submenu)</button>
|
|
14
|
+
* </div>
|
|
15
|
+
*
|
|
16
|
+
* <!-- Panels -->
|
|
17
|
+
* <div id="home-panel" class="rmm-sidebar-panel rmm-sidebar-panel-active">...</div>
|
|
18
|
+
* <div id="settings-panel" class="rmm-sidebar-panel">...</div>
|
|
19
|
+
*
|
|
20
|
+
* <!-- Submenus -->
|
|
21
|
+
* <div id="home-submenu" class="rmm-submenu-group rmm-submenu-group-active">...</div>
|
|
22
|
+
* ```
|
|
6
23
|
*/
|
|
7
24
|
export default class extends Controller {
|
|
8
25
|
static targets = ["nav", "overlay"]
|
|
@@ -12,10 +29,18 @@ export default class extends Controller {
|
|
|
12
29
|
}
|
|
13
30
|
|
|
14
31
|
connect() {
|
|
32
|
+
this.currentItemId = null
|
|
33
|
+
|
|
15
34
|
if (this.collapsedValue) {
|
|
16
35
|
this.element.classList.add('rmm-sidebar-collapsed')
|
|
17
36
|
}
|
|
18
37
|
|
|
38
|
+
// Find initial active item
|
|
39
|
+
const activeItem = this.element.querySelector('.rmm-sidebar-item.rmm-active')
|
|
40
|
+
if (activeItem) {
|
|
41
|
+
this.currentItemId = activeItem.dataset.itemId
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
// Listen for toggle event from header button
|
|
20
45
|
this.handleToggleEvent = () => this.toggle()
|
|
21
46
|
this.element.addEventListener('rmm-sidebar:toggle', this.handleToggleEvent)
|
|
@@ -33,20 +58,43 @@ export default class extends Controller {
|
|
|
33
58
|
modalStore.updateModal(this.modalIdValue, {
|
|
34
59
|
sidebarOpen: !this.collapsedValue,
|
|
35
60
|
})
|
|
61
|
+
|
|
62
|
+
// Dispatch toggled event so toggle button can update its icon
|
|
63
|
+
const modal = this.element.closest('.rmm-modal')
|
|
64
|
+
if (modal) {
|
|
65
|
+
modal.dispatchEvent(new CustomEvent('rmm-sidebar:toggled', {
|
|
66
|
+
detail: { collapsed: this.collapsedValue },
|
|
67
|
+
bubbles: false
|
|
68
|
+
}))
|
|
69
|
+
}
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
selectItem(e) {
|
|
39
73
|
e.preventDefault()
|
|
40
74
|
e.stopPropagation()
|
|
41
75
|
|
|
76
|
+
const item = e.currentTarget
|
|
77
|
+
const itemId = item.dataset.itemId
|
|
78
|
+
|
|
79
|
+
// Skip if already active
|
|
80
|
+
if (itemId === this.currentItemId) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
// Remove active from all items
|
|
43
|
-
this.element.querySelectorAll('.rmm-sidebar-item').forEach(
|
|
44
|
-
|
|
85
|
+
this.element.querySelectorAll('.rmm-sidebar-item').forEach(el => {
|
|
86
|
+
el.classList.remove('rmm-active')
|
|
45
87
|
})
|
|
46
88
|
|
|
47
89
|
// Add active to clicked item
|
|
48
|
-
const item = e.currentTarget
|
|
49
90
|
item.classList.add('rmm-active')
|
|
91
|
+
this.currentItemId = itemId
|
|
92
|
+
|
|
93
|
+
// Switch content panel
|
|
94
|
+
this.switchPanel(item)
|
|
95
|
+
|
|
96
|
+
// Switch submenu group
|
|
97
|
+
this.switchSubmenuGroup(item)
|
|
50
98
|
|
|
51
99
|
// On mobile, close sidebar after selection
|
|
52
100
|
if (window.innerWidth < 768) {
|
|
@@ -60,10 +108,75 @@ export default class extends Controller {
|
|
|
60
108
|
modalId: this.modalIdValue,
|
|
61
109
|
itemId: item.dataset.itemId,
|
|
62
110
|
itemLabel: item.dataset.itemLabel,
|
|
111
|
+
hasSubmenu: !!item.dataset.submenuId,
|
|
63
112
|
}
|
|
64
113
|
})
|
|
65
114
|
}
|
|
66
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Switch content panel based on sidebar selection
|
|
118
|
+
*/
|
|
119
|
+
switchPanel(item) {
|
|
120
|
+
const panelId = item.dataset.panelId
|
|
121
|
+
if (!panelId) return
|
|
122
|
+
|
|
123
|
+
const modal = this.element.closest('.rmm-modal')
|
|
124
|
+
if (!modal) return
|
|
125
|
+
|
|
126
|
+
// Hide all sidebar panels
|
|
127
|
+
modal.querySelectorAll('.rmm-sidebar-panel').forEach(panel => {
|
|
128
|
+
panel.classList.remove('rmm-sidebar-panel-active')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Show target panel
|
|
132
|
+
const targetPanel = modal.querySelector(`#${panelId}`)
|
|
133
|
+
if (targetPanel) {
|
|
134
|
+
targetPanel.classList.add('rmm-sidebar-panel-active')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Switch submenu group based on sidebar selection
|
|
140
|
+
*/
|
|
141
|
+
switchSubmenuGroup(item) {
|
|
142
|
+
const submenuId = item.dataset.submenuId
|
|
143
|
+
const modal = this.element.closest('.rmm-modal')
|
|
144
|
+
if (!modal) return
|
|
145
|
+
|
|
146
|
+
// Hide all submenu groups
|
|
147
|
+
modal.querySelectorAll('.rmm-submenu-group').forEach(group => {
|
|
148
|
+
group.classList.remove('rmm-submenu-group-active')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Show target submenu group (if exists)
|
|
152
|
+
if (submenuId) {
|
|
153
|
+
const targetSubmenu = modal.querySelector(`#${submenuId}`)
|
|
154
|
+
if (targetSubmenu) {
|
|
155
|
+
targetSubmenu.classList.add('rmm-submenu-group-active')
|
|
156
|
+
|
|
157
|
+
// Auto-select first submenu item if none is active
|
|
158
|
+
const submenuController = targetSubmenu.querySelector('[data-controller="rmm-submenu"]')
|
|
159
|
+
if (submenuController) {
|
|
160
|
+
const activeSubmenuItem = submenuController.querySelector('.rmm-submenu-item.rmm-active')
|
|
161
|
+
if (!activeSubmenuItem) {
|
|
162
|
+
const firstItem = submenuController.querySelector('.rmm-submenu-item')
|
|
163
|
+
if (firstItem) {
|
|
164
|
+
firstItem.click()
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Ensure the panel for active submenu item is shown
|
|
168
|
+
const panelId = activeSubmenuItem.dataset.panelId
|
|
169
|
+
if (panelId) {
|
|
170
|
+
modal.querySelectorAll('.rmm-tab-panel').forEach(p => p.classList.remove('rmm-tab-panel-active'))
|
|
171
|
+
const panel = modal.querySelector(`#${panelId}`)
|
|
172
|
+
if (panel) panel.classList.add('rmm-tab-panel-active')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
67
180
|
// Mobile overlay click
|
|
68
181
|
closeOverlay() {
|
|
69
182
|
this.collapsedValue = true
|
|
@@ -2,21 +2,80 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Rails Modal Manager - Submenu Controller
|
|
5
|
+
*
|
|
6
|
+
* Supports two content loading modes:
|
|
7
|
+
* 1. preload (default): All panels are pre-rendered, show/hide on tab switch
|
|
8
|
+
* 2. ajax: Load content from URL when tab is selected
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* Preload mode (default):
|
|
13
|
+
* ```html
|
|
14
|
+
* <div class="rmm-submenu" data-controller="rmm-submenu">
|
|
15
|
+
* <button data-item-id="tab1" data-panel-id="panel-1">Tab 1</button>
|
|
16
|
+
* <button data-item-id="tab2" data-panel-id="panel-2">Tab 2</button>
|
|
17
|
+
* </div>
|
|
18
|
+
* <div class="rmm-content">
|
|
19
|
+
* <div id="panel-1" class="rmm-tab-panel rmm-tab-panel-active">Content 1</div>
|
|
20
|
+
* <div id="panel-2" class="rmm-tab-panel">Content 2</div>
|
|
21
|
+
* </div>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* AJAX mode:
|
|
25
|
+
* ```html
|
|
26
|
+
* <div class="rmm-submenu" data-controller="rmm-submenu" data-rmm-submenu-load-mode-value="ajax">
|
|
27
|
+
* <button data-item-id="tab1" data-url="/content/tab1">Tab 1</button>
|
|
28
|
+
* <button data-item-id="tab2" data-url="/content/tab2">Tab 2</button>
|
|
29
|
+
* </div>
|
|
30
|
+
* <div class="rmm-content">
|
|
31
|
+
* <div class="rmm-tab-content"></div>
|
|
32
|
+
* </div>
|
|
33
|
+
* ```
|
|
5
34
|
*/
|
|
6
35
|
export default class extends Controller {
|
|
7
36
|
static values = {
|
|
8
37
|
modalId: String,
|
|
38
|
+
loadMode: { type: String, default: 'preload' }, // 'preload' or 'ajax'
|
|
39
|
+
cacheAjax: { type: Boolean, default: true }, // Cache AJAX responses
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static targets = ['content']
|
|
43
|
+
|
|
44
|
+
connect() {
|
|
45
|
+
this.ajaxCache = new Map()
|
|
46
|
+
this.currentItemId = null
|
|
47
|
+
|
|
48
|
+
// Find initial active item
|
|
49
|
+
const activeItem = this.element.querySelector('.rmm-submenu-item.rmm-active')
|
|
50
|
+
if (activeItem) {
|
|
51
|
+
this.currentItemId = activeItem.dataset.itemId
|
|
52
|
+
}
|
|
9
53
|
}
|
|
10
54
|
|
|
11
55
|
selectItem(e) {
|
|
56
|
+
const item = e.currentTarget
|
|
57
|
+
const itemId = item.dataset.itemId
|
|
58
|
+
|
|
59
|
+
// Skip if already active
|
|
60
|
+
if (itemId === this.currentItemId) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
// Remove active from all items
|
|
13
|
-
this.element.querySelectorAll('.rmm-submenu-item').forEach(
|
|
14
|
-
|
|
65
|
+
this.element.querySelectorAll('.rmm-submenu-item').forEach(el => {
|
|
66
|
+
el.classList.remove('rmm-active')
|
|
15
67
|
})
|
|
16
68
|
|
|
17
69
|
// Add active to clicked item
|
|
18
|
-
const item = e.currentTarget
|
|
19
70
|
item.classList.add('rmm-active')
|
|
71
|
+
this.currentItemId = itemId
|
|
72
|
+
|
|
73
|
+
// Handle content switching based on load mode
|
|
74
|
+
if (this.loadModeValue === 'ajax') {
|
|
75
|
+
this.loadAjaxContent(item)
|
|
76
|
+
} else {
|
|
77
|
+
this.switchPanel(item)
|
|
78
|
+
}
|
|
20
79
|
|
|
21
80
|
// Dispatch event
|
|
22
81
|
this.dispatch('itemSelect', {
|
|
@@ -24,7 +83,133 @@ export default class extends Controller {
|
|
|
24
83
|
modalId: this.modalIdValue,
|
|
25
84
|
itemId: item.dataset.itemId,
|
|
26
85
|
itemLabel: item.dataset.itemLabel,
|
|
86
|
+
loadMode: this.loadModeValue,
|
|
27
87
|
}
|
|
28
88
|
})
|
|
29
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Preload mode: Switch between pre-rendered panels
|
|
93
|
+
*/
|
|
94
|
+
switchPanel(item) {
|
|
95
|
+
const panelId = item.dataset.panelId
|
|
96
|
+
if (!panelId) return
|
|
97
|
+
|
|
98
|
+
// Find the content container (modal's content area)
|
|
99
|
+
const modal = this.element.closest('.rmm-modal')
|
|
100
|
+
if (!modal) return
|
|
101
|
+
|
|
102
|
+
// Hide all panels
|
|
103
|
+
modal.querySelectorAll('.rmm-tab-panel').forEach(panel => {
|
|
104
|
+
panel.classList.remove('rmm-tab-panel-active')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Show target panel
|
|
108
|
+
const targetPanel = modal.querySelector(`#${panelId}`)
|
|
109
|
+
if (targetPanel) {
|
|
110
|
+
targetPanel.classList.add('rmm-tab-panel-active')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* AJAX mode: Load content from URL
|
|
116
|
+
*/
|
|
117
|
+
async loadAjaxContent(item) {
|
|
118
|
+
const url = item.dataset.url
|
|
119
|
+
if (!url) return
|
|
120
|
+
|
|
121
|
+
const modal = this.element.closest('.rmm-modal')
|
|
122
|
+
if (!modal) return
|
|
123
|
+
|
|
124
|
+
// Find content container
|
|
125
|
+
const contentContainer = modal.querySelector('.rmm-tab-content')
|
|
126
|
+
if (!contentContainer) return
|
|
127
|
+
|
|
128
|
+
// Check cache
|
|
129
|
+
if (this.cacheAjaxValue && this.ajaxCache.has(url)) {
|
|
130
|
+
contentContainer.innerHTML = this.ajaxCache.get(url)
|
|
131
|
+
this.dispatch('contentLoaded', {
|
|
132
|
+
detail: { modalId: this.modalIdValue, itemId: item.dataset.itemId, fromCache: true }
|
|
133
|
+
})
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Show loading state
|
|
138
|
+
contentContainer.classList.add('rmm-tab-loading')
|
|
139
|
+
contentContainer.innerHTML = '<div class="rmm-tab-loader"><div class="rmm-spinner"></div><span>로딩 중...</span></div>'
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
headers: {
|
|
144
|
+
'Accept': 'text/html',
|
|
145
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const html = await response.text()
|
|
154
|
+
|
|
155
|
+
// Cache the response
|
|
156
|
+
if (this.cacheAjaxValue) {
|
|
157
|
+
this.ajaxCache.set(url, html)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Update content
|
|
161
|
+
contentContainer.innerHTML = html
|
|
162
|
+
contentContainer.classList.remove('rmm-tab-loading')
|
|
163
|
+
|
|
164
|
+
this.dispatch('contentLoaded', {
|
|
165
|
+
detail: { modalId: this.modalIdValue, itemId: item.dataset.itemId, fromCache: false }
|
|
166
|
+
})
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('RMM Submenu: Failed to load content', error)
|
|
169
|
+
contentContainer.innerHTML = `
|
|
170
|
+
<div class="rmm-tab-error">
|
|
171
|
+
<span>컨텐츠를 불러오는데 실패했습니다.</span>
|
|
172
|
+
<button type="button" class="rmm-btn rmm-btn-secondary" onclick="this.closest('.rmm-modal').querySelector('.rmm-submenu-item.rmm-active').click()">
|
|
173
|
+
다시 시도
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
`
|
|
177
|
+
contentContainer.classList.remove('rmm-tab-loading')
|
|
178
|
+
|
|
179
|
+
this.dispatch('contentError', {
|
|
180
|
+
detail: { modalId: this.modalIdValue, itemId: item.dataset.itemId, error: error.message }
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear AJAX cache (can be called programmatically)
|
|
187
|
+
*/
|
|
188
|
+
clearCache() {
|
|
189
|
+
this.ajaxCache.clear()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear cache for specific URL
|
|
194
|
+
*/
|
|
195
|
+
clearCacheFor(url) {
|
|
196
|
+
this.ajaxCache.delete(url)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reload current tab content (useful for AJAX mode)
|
|
201
|
+
*/
|
|
202
|
+
reloadCurrentTab() {
|
|
203
|
+
const activeItem = this.element.querySelector('.rmm-submenu-item.rmm-active')
|
|
204
|
+
if (activeItem) {
|
|
205
|
+
if (this.loadModeValue === 'ajax') {
|
|
206
|
+
// Clear cache for this URL and reload
|
|
207
|
+
const url = activeItem.dataset.url
|
|
208
|
+
if (url) {
|
|
209
|
+
this.ajaxCache.delete(url)
|
|
210
|
+
}
|
|
211
|
+
this.loadAjaxContent(activeItem)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
30
215
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export const VERSION = "1.0.
|
|
11
|
+
export const VERSION = "1.0.10"
|
|
12
12
|
|
|
13
13
|
// Import core modules for internal use
|
|
14
14
|
import modalStore, { MODAL_CONSTANTS, SIZE_CONFIG, POSITION_CONFIG, CASCADE_OFFSET, modalSizeStorage, modalUtils } from "rails_modal_manager/modal_store"
|
|
@@ -59,6 +59,25 @@ export function initHistoryStack() {
|
|
|
59
59
|
return historyStackManager.init()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Initialize global click handler for modal open buttons
|
|
64
|
+
* Handles clicks on elements with data-rmm-modal-id attribute
|
|
65
|
+
* Should be called once on page load
|
|
66
|
+
*/
|
|
67
|
+
export function initGlobalClickHandler() {
|
|
68
|
+
document.addEventListener('click', (e) => {
|
|
69
|
+
// Find the closest element with data-rmm-modal-id
|
|
70
|
+
const trigger = e.target.closest('[data-rmm-modal-id]')
|
|
71
|
+
if (!trigger) return
|
|
72
|
+
|
|
73
|
+
const modalId = trigger.dataset.rmmModalId
|
|
74
|
+
if (!modalId) return
|
|
75
|
+
|
|
76
|
+
e.preventDefault()
|
|
77
|
+
openModal(modalId)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Open a modal by ID
|
|
64
83
|
* @param {string} modalId - The modal's ID
|
|
@@ -24,6 +24,8 @@ export const CASCADE_OFFSET = { x: 30, y: 30 };
|
|
|
24
24
|
|
|
25
25
|
// Size configurations (matching React version)
|
|
26
26
|
export const SIZE_CONFIG = {
|
|
27
|
+
fit: { width: 'auto', minWidth: '150px', height: 'auto' },
|
|
28
|
+
xss: { width: '20%', minWidth: '200px', height: 'auto' },
|
|
27
29
|
xs: { width: '30%', minWidth: '280px', height: 'auto' },
|
|
28
30
|
sm: { width: '40%', minWidth: '360px', height: 'auto' },
|
|
29
31
|
md: { width: '50%', minWidth: '480px', height: 'auto' },
|
|
@@ -330,8 +332,10 @@ class ModalStore {
|
|
|
330
332
|
// ============================================
|
|
331
333
|
|
|
332
334
|
minimizeAll() {
|
|
335
|
+
const minimizedAt = Date.now();
|
|
333
336
|
Object.keys(this.activeModals).forEach(modalId => {
|
|
334
337
|
this.activeModals[modalId].isMinimized = true;
|
|
338
|
+
this.activeModals[modalId].minimizedAt = minimizedAt;
|
|
335
339
|
});
|
|
336
340
|
this.updateBodyScroll();
|
|
337
341
|
this.notify();
|
|
@@ -443,10 +447,12 @@ class ModalStore {
|
|
|
443
447
|
const rootId = this.getRootModal(modalId);
|
|
444
448
|
const descendants = this.getAllDescendants(rootId);
|
|
445
449
|
const allModalIds = [rootId, ...descendants];
|
|
450
|
+
const minimizedAt = Date.now();
|
|
446
451
|
|
|
447
452
|
allModalIds.forEach(id => {
|
|
448
453
|
if (this.activeModals[id]) {
|
|
449
454
|
this.activeModals[id].isMinimized = true;
|
|
455
|
+
this.activeModals[id].minimizedAt = minimizedAt;
|
|
450
456
|
}
|
|
451
457
|
});
|
|
452
458
|
|
|
@@ -489,7 +495,7 @@ class ModalStore {
|
|
|
489
495
|
[rootId, ...descendants].forEach(id => processedModals.add(id));
|
|
490
496
|
});
|
|
491
497
|
|
|
492
|
-
|
|
498
|
+
const groups = Array.from(minimizedRoots).map(rootId => {
|
|
493
499
|
const descendants = this.getAllDescendants(rootId);
|
|
494
500
|
const allModalIds = [rootId, ...descendants];
|
|
495
501
|
|
|
@@ -499,6 +505,7 @@ class ModalStore {
|
|
|
499
505
|
|
|
500
506
|
const leafModalId = sortedByOrder[sortedByOrder.length - 1] || rootId;
|
|
501
507
|
const leafConfig = this.activeModals[leafModalId];
|
|
508
|
+
const rootConfig = this.activeModals[rootId];
|
|
502
509
|
|
|
503
510
|
return {
|
|
504
511
|
rootModalId: rootId,
|
|
@@ -507,8 +514,12 @@ class ModalStore {
|
|
|
507
514
|
groupSize: allModalIds.length,
|
|
508
515
|
zIndex: leafConfig?.zIndex || 0,
|
|
509
516
|
modalIds: allModalIds,
|
|
517
|
+
minimizedAt: rootConfig?.minimizedAt || 0,
|
|
510
518
|
};
|
|
511
519
|
});
|
|
520
|
+
|
|
521
|
+
// Sort by minimizedAt timestamp (oldest first)
|
|
522
|
+
return groups.sort((a, b) => a.minimizedAt - b.minimizedAt);
|
|
512
523
|
}
|
|
513
524
|
}
|
|
514
525
|
|
|
@@ -64,12 +64,14 @@
|
|
|
64
64
|
<% if show_sidebar_toggle %>
|
|
65
65
|
<button type="button"
|
|
66
66
|
class="rmm-header-btn rmm-sidebar-toggle"
|
|
67
|
-
data-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
67
|
+
data-rmm-header-target="sidebarToggle"
|
|
68
|
+
data-action="click->rmm-header#toggleSidebar"
|
|
69
|
+
title="사이드바 토글">
|
|
70
|
+
<svg class="rmm-icon-collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
71
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
72
|
+
</svg>
|
|
73
|
+
<svg class="rmm-icon-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
|
|
74
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
73
75
|
</svg>
|
|
74
76
|
</button>
|
|
75
77
|
<% end %>
|
|
@@ -77,6 +79,17 @@
|
|
|
77
79
|
</div>
|
|
78
80
|
|
|
79
81
|
<div class="rmm-header-controls">
|
|
82
|
+
<% if minimizable %>
|
|
83
|
+
<button type="button"
|
|
84
|
+
class="rmm-header-btn"
|
|
85
|
+
data-action="click->rmm-header#minimize"
|
|
86
|
+
title="최소화">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
88
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
<% end %>
|
|
92
|
+
|
|
80
93
|
<% if show_size_controls %>
|
|
81
94
|
<%# Default size button - restores to original size %>
|
|
82
95
|
<button type="button"
|
|
@@ -109,17 +122,6 @@
|
|
|
109
122
|
</button>
|
|
110
123
|
<% end %>
|
|
111
124
|
|
|
112
|
-
<% if minimizable %>
|
|
113
|
-
<button type="button"
|
|
114
|
-
class="rmm-header-btn"
|
|
115
|
-
data-action="click->rmm-header#minimize"
|
|
116
|
-
title="최소화">
|
|
117
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
118
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
119
|
-
</svg>
|
|
120
|
-
</button>
|
|
121
|
-
<% end %>
|
|
122
|
-
|
|
123
125
|
<%# Custom header buttons %>
|
|
124
126
|
<% header_buttons.each do |btn| %>
|
|
125
127
|
<%
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
min_height: Number - Minimum height in px (default: 150)
|
|
28
28
|
max_width: Number - Maximum width in px (default: nil)
|
|
29
29
|
max_height: Number - Maximum height in px (default: nil)
|
|
30
|
+
height: String - Modal height (default: "auto", e.g. "400px", "50vh")
|
|
30
31
|
animation_duration: Number - Animation duration in ms (default: 200)
|
|
31
32
|
show_header: Boolean - Show header (default: true)
|
|
32
33
|
show_footer: Boolean - Show footer (default: false)
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
min_height ||= 150
|
|
68
69
|
max_width ||= nil
|
|
69
70
|
max_height ||= nil
|
|
71
|
+
height ||= "auto"
|
|
70
72
|
animation_duration ||= 200
|
|
71
73
|
show_header = true if local_assigns[:show_header].nil?
|
|
72
74
|
show_footer ||= false
|
|
@@ -108,6 +110,7 @@
|
|
|
108
110
|
rmm_modal_prevent_body_scroll_value: prevent_body_scroll,
|
|
109
111
|
rmm_modal_min_width_value: min_width,
|
|
110
112
|
rmm_modal_min_height_value: min_height,
|
|
113
|
+
rmm_modal_height_value: height,
|
|
111
114
|
rmm_modal_animation_duration_value: animation_duration,
|
|
112
115
|
action: "click->rmm-modal#handleClick"
|
|
113
116
|
}
|
|
@@ -158,9 +161,9 @@
|
|
|
158
161
|
items: sidebar_items,
|
|
159
162
|
collapsed: sidebar_collapsed
|
|
160
163
|
%>
|
|
164
|
+
<!-- Mobile overlay: closes sidebar when tapped (dispatches toggle event) -->
|
|
161
165
|
<div class="rmm-sidebar-overlay"
|
|
162
|
-
|
|
163
|
-
data-action="click->rmm-sidebar#closeOverlay"></div>
|
|
166
|
+
onclick="const sidebar = this.previousElementSibling; if(sidebar && sidebar.classList.contains('rmm-sidebar') && !sidebar.classList.contains('rmm-sidebar-collapsed')) { sidebar.dispatchEvent(new CustomEvent('rmm-sidebar:toggle', { bubbles: false })); }"></div>
|
|
164
167
|
<% end %>
|
|
165
168
|
|
|
166
169
|
<div class="rmm-content-wrapper">
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
Locals:
|
|
5
5
|
modal_id: String - The modal ID
|
|
6
|
-
items: Array - Menu items [{id:, label:, icon_svg:, badge:, active:, disabled:}]
|
|
6
|
+
items: Array - Menu items [{id:, label:, icon_svg:, badge:, active:, disabled:, panel_id:, submenu_id:}]
|
|
7
7
|
collapsed: Boolean - Initially collapsed (default: false)
|
|
8
|
+
|
|
9
|
+
Item options:
|
|
10
|
+
- panel_id: ID of the content panel to show when selected
|
|
11
|
+
- submenu_id: ID of the submenu group to show when selected
|
|
8
12
|
%>
|
|
9
13
|
<%
|
|
10
14
|
modal_id ||= ""
|
|
@@ -29,6 +33,8 @@
|
|
|
29
33
|
class="<%= item_classes.join(' ') %>"
|
|
30
34
|
data-item-id="<%= item[:id] %>"
|
|
31
35
|
data-item-label="<%= item[:label] %>"
|
|
36
|
+
<% if item[:panel_id].present? %>data-panel-id="<%= item[:panel_id] %>"<% end %>
|
|
37
|
+
<% if item[:submenu_id].present? %>data-submenu-id="<%= item[:submenu_id] %>"<% end %>
|
|
32
38
|
data-action="click->rmm-sidebar#selectItem"
|
|
33
39
|
<% if item[:disabled] %>aria-disabled="true"<% end %>>
|
|
34
40
|
<% if item[:icon_svg].present? %>
|
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Rails Modal Manager - Submenu Component
|
|
2
|
+
Rails Modal Manager - Submenu Component (Pill/Tab Style)
|
|
3
3
|
|
|
4
4
|
Locals:
|
|
5
5
|
modal_id: String - The modal ID
|
|
6
|
-
items: Array - Menu items [{id:, label:, icon_svg:, active:, disabled:}]
|
|
6
|
+
items: Array - Menu items [{id:, label:, icon_svg:, active:, disabled:, panel_id:, url:}]
|
|
7
|
+
load_mode: String - 'preload' (default) or 'ajax'
|
|
8
|
+
cache_ajax: Boolean - Cache AJAX responses (default: true)
|
|
9
|
+
|
|
10
|
+
Preload mode:
|
|
11
|
+
- Use panel_id to link to pre-rendered content panels
|
|
12
|
+
- Panels should have class="rmm-tab-panel" and matching id
|
|
13
|
+
|
|
14
|
+
AJAX mode:
|
|
15
|
+
- Use url to specify content endpoint
|
|
16
|
+
- Content container should have class="rmm-tab-content"
|
|
7
17
|
%>
|
|
8
18
|
<%
|
|
9
19
|
modal_id ||= ""
|
|
10
20
|
items ||= []
|
|
21
|
+
load_mode ||= "preload"
|
|
22
|
+
cache_ajax = cache_ajax.nil? ? true : cache_ajax
|
|
11
23
|
%>
|
|
12
24
|
<div class="rmm-submenu"
|
|
13
25
|
data-controller="rmm-submenu"
|
|
14
|
-
data-rmm-submenu-modal-id-value="<%= modal_id %>"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
data-rmm-submenu-modal-id-value="<%= modal_id %>"
|
|
27
|
+
data-rmm-submenu-load-mode-value="<%= load_mode %>"
|
|
28
|
+
data-rmm-submenu-cache-ajax-value="<%= cache_ajax %>">
|
|
29
|
+
<div class="rmm-submenu-inner">
|
|
30
|
+
<% items.each do |item| %>
|
|
31
|
+
<%
|
|
32
|
+
item_classes = ["rmm-submenu-item"]
|
|
33
|
+
item_classes << "rmm-active" if item[:active]
|
|
34
|
+
item_classes << "rmm-disabled" if item[:disabled]
|
|
35
|
+
%>
|
|
36
|
+
<button type="button"
|
|
37
|
+
class="<%= item_classes.join(' ') %>"
|
|
38
|
+
data-item-id="<%= item[:id] %>"
|
|
39
|
+
data-item-label="<%= item[:label] %>"
|
|
40
|
+
<% if item[:panel_id].present? %>data-panel-id="<%= item[:panel_id] %>"<% end %>
|
|
41
|
+
<% if item[:url].present? %>data-url="<%= item[:url] %>"<% end %>
|
|
42
|
+
data-action="click->rmm-submenu#selectItem"
|
|
43
|
+
<% if item[:disabled] %>disabled<% end %>>
|
|
44
|
+
<% if item[:icon_svg].present? %>
|
|
45
|
+
<span class="rmm-submenu-item-icon"><%= item[:icon_svg].html_safe %></span>
|
|
46
|
+
<% end %>
|
|
47
|
+
<span><%= item[:label] %></span>
|
|
48
|
+
</button>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
33
51
|
</div>
|