rails_modal_manager 1.0.32 → 1.0.33

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 137650bba1f5ded9e215a34fba7402a47e1f888641416a478acbeaee9fcc806b
4
- data.tar.gz: a047f2a4c6a11f65d539e3df7aefb2e02bbdb8bf2a0194a67b52a66214e22d8d
3
+ metadata.gz: be6c4d54beffce5af66fd740e4c8d1164bab5ad19ca6ceacfb889da7ff5a908f
4
+ data.tar.gz: 559d900a84c1ebefde8711de82a91e7eac7521d05439e71af314e5dfd14d362c
5
5
  SHA512:
6
- metadata.gz: 5786f1ea88a886ddd26132669b480c7b2d3ed2b3a456934d0d65b818c7faf9c83e76a5822fd662b74a6dbebebbecebbe51ab3edd04375b73fb7c14b16884d635
7
- data.tar.gz: df5a7843b45cd6e126825d10b1600fc628a76db7d4f21ed596938e3c1d2367a051fde591d499bfac88cc88459696459f4b86950f24a155129d68d46c2abb5da1
6
+ metadata.gz: 12084926d5370e7e3bc4e4bf047696b90f3f3c962d4bb185e3eddbb40ecce88e695a1a1413b2068083db229953f40e8acda5b0f4b5eaad6dffe73f4ecf94da1c
7
+ data.tar.gz: 01b06a874a6bf993d2e27d3ec24604a9c10a1da90bde62d8200ee8aad53dbdeb6a1cd90e27af859bcef29acc3ad3e0ca057741afb5b93c21aaf0f555cc810e32
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Rails 애플리케이션을 위한 고급 모달 매니저입니다.
4
4
  `@reshacs/react-modal-manager`에서 포팅되었습니다.
5
5
 
6
- **Version:** 1.0.24
6
+ **Version:** 1.0.33
7
7
 
8
8
  ---
9
9
 
@@ -198,6 +198,7 @@ closeModal('my-modal')
198
198
  | `confirm-close-message` | String | "정말 닫으시겠습니까?" | 확인 메시지 |
199
199
  | `mobile-default-maximized` | Boolean | false | 모바일에서 기본 최대화 |
200
200
  | `parent-modal-id` | String | - | 부모 모달 ID (종속 관계) |
201
+ | `persistent-id` | String | - | 크기 상태 공유용 ID (같은 타입 모달 간 크기 공유) |
201
202
  | `min-width` | Number | 200 | 최소 너비 (px) |
202
203
  | `min-height` | Number | 150 | 최소 높이 (px) |
203
204
  | `max-width` | Number | - | 최대 너비 (px) |
@@ -438,6 +439,14 @@ header_buttons: [
438
439
 
439
440
  Taskbar는 자동으로 최소화된 모달들을 렌더링합니다.
440
441
 
442
+ **Taskbar 기능 (v1.0.33+):**
443
+
444
+ - **클릭하여 복구**: 태스크바 아이템 클릭 시 모달 복구 (X버튼 제외)
445
+ - **드래그 앤 드롭**: 아이템을 드래그하여 순서 변경
446
+ - **키보드 단축키**: Alt+1~9로 빠른 복구/최소화 토글
447
+ - **모두 닫기**: 2개 이상 최소화 시 "모두 닫기" 버튼 표시
448
+ - **확인 다이얼로그**: X버튼 클릭 시 닫기 확인
449
+
441
450
  ### Resize Handle
442
451
 
443
452
  모달 리사이즈를 위한 핸들입니다.
@@ -789,6 +798,28 @@ MIT License
789
798
 
790
799
  ## 변경 이력
791
800
 
801
+ ### v1.0.33
802
+
803
+ - **Taskbar 기능 대폭 개선**:
804
+ - 클릭하여 복구: 태스크바 아이템 클릭 시 모달 복구 (X버튼 제외)
805
+ - 드래그 앤 드롭: 최소화된 모달 순서 변경 지원
806
+ - 키보드 단축키: Alt+1~9로 빠른 복구/최소화 토글
807
+ - 모두 닫기 버튼: 2개 이상 최소화 시 표시 (확인 다이얼로그 포함)
808
+ - 개별 닫기 확인: X버튼 클릭 시 확인 다이얼로그 표시
809
+ - **복구된 모달 상태 표시**: 최소화에서 복구된 모달은 태스크바에서 비활성화 상태로 표시
810
+ - **같은 ID 모달 재오픈 버그 수정**: 최소화된 상태에서 같은 ID로 모달 열기 시 정상적으로 복구
811
+
812
+ ### v1.0.32
813
+
814
+ - **`persistent_id` 옵션 추가**: 같은 타입의 모달들이 크기 상태를 공유할 수 있도록 지원
815
+ - 업체 모달, 사용자 프로필 모달 등에서 활용 가능
816
+
817
+ ### v1.0.31
818
+
819
+ - **모달 크기 기억 기능 추가**: 모달이 마지막 크기(최대화/기본)를 localStorage에 저장하여 다음에 열 때 동일한 크기로 열림
820
+ - **모바일 사이드바 동작 개선**: 최대화된 모달에서 아이콘 상태는 inline(본문 공간 차지), 확장 상태는 오버레이 방식으로 변경
821
+ - **사이드바 3상태 토글**: expanded(확장), icons(아이콘), hidden(숨김) 상태 지원
822
+
792
823
  ### v1.0.24
793
824
 
794
825
  - **`submenu_groups` 옵션 추가**: 사이드바의 각 메뉴에 서로 다른 서브메뉴 그룹을 연결 가능
@@ -1157,6 +1157,20 @@
1157
1157
  background: var(--rmm-taskbar-item-hover);
1158
1158
  }
1159
1159
 
1160
+ .rmm-taskbar-item-disabled {
1161
+ opacity: 0.5;
1162
+ pointer-events: none;
1163
+ }
1164
+
1165
+ .rmm-taskbar-item-disabled .rmm-taskbar-item-close {
1166
+ pointer-events: auto;
1167
+ opacity: 0.7;
1168
+ }
1169
+
1170
+ .rmm-taskbar-item-disabled .rmm-taskbar-item-close:hover {
1171
+ opacity: 1;
1172
+ }
1173
+
1160
1174
  .rmm-taskbar-item-title {
1161
1175
  max-width: 150px;
1162
1176
  overflow: hidden;
@@ -1204,6 +1218,120 @@
1204
1218
  height: 14px;
1205
1219
  }
1206
1220
 
1221
+ /* Taskbar - Close All Button */
1222
+ .rmm-taskbar-close-all {
1223
+ display: flex;
1224
+ align-items: center;
1225
+ justify-content: center;
1226
+ width: 32px;
1227
+ height: 32px;
1228
+ border: none;
1229
+ background: rgba(239, 68, 68, 0.2);
1230
+ border-radius: 6px;
1231
+ cursor: pointer;
1232
+ color: #ef4444;
1233
+ opacity: 0.9;
1234
+ transition: opacity 0.15s, background-color 0.15s;
1235
+ flex-shrink: 0;
1236
+ margin-right: 4px;
1237
+ }
1238
+
1239
+ .rmm-taskbar-close-all:hover {
1240
+ opacity: 1;
1241
+ background: rgba(239, 68, 68, 0.4);
1242
+ }
1243
+
1244
+ .rmm-taskbar-close-all svg {
1245
+ width: 16px;
1246
+ height: 16px;
1247
+ }
1248
+
1249
+ /* Taskbar - Shortcut Badge */
1250
+ .rmm-taskbar-shortcut {
1251
+ padding: 2px 5px;
1252
+ font-size: 10px;
1253
+ font-weight: 500;
1254
+ background: rgba(255, 255, 255, 0.15);
1255
+ color: rgba(255, 255, 255, 0.7);
1256
+ border-radius: 4px;
1257
+ font-family: monospace;
1258
+ }
1259
+
1260
+ .rmm-taskbar-item:hover .rmm-taskbar-shortcut {
1261
+ background: rgba(255, 255, 255, 0.25);
1262
+ color: rgba(255, 255, 255, 0.9);
1263
+ }
1264
+
1265
+ .rmm-taskbar-item-disabled .rmm-taskbar-shortcut {
1266
+ opacity: 0.5;
1267
+ }
1268
+
1269
+ /* Taskbar - Drag Handle */
1270
+ .rmm-taskbar-item-drag-handle {
1271
+ display: flex;
1272
+ align-items: center;
1273
+ justify-content: center;
1274
+ width: 16px;
1275
+ height: 16px;
1276
+ cursor: grab;
1277
+ opacity: 0.5;
1278
+ transition: opacity 0.15s;
1279
+ flex-shrink: 0;
1280
+ }
1281
+
1282
+ .rmm-taskbar-item-drag-handle:active {
1283
+ cursor: grabbing;
1284
+ }
1285
+
1286
+ .rmm-taskbar-item:hover .rmm-taskbar-item-drag-handle {
1287
+ opacity: 0.8;
1288
+ }
1289
+
1290
+ .rmm-taskbar-item-drag-handle svg {
1291
+ width: 12px;
1292
+ height: 12px;
1293
+ }
1294
+
1295
+ /* Taskbar - Dragging States */
1296
+ .rmm-taskbar-item-dragging {
1297
+ opacity: 0.8;
1298
+ }
1299
+
1300
+ .rmm-taskbar-item-dragging-active {
1301
+ opacity: 0.4;
1302
+ }
1303
+
1304
+ /* Taskbar - Drop Indicators */
1305
+ .rmm-taskbar-item-drop-before {
1306
+ position: relative;
1307
+ }
1308
+
1309
+ .rmm-taskbar-item-drop-before::before {
1310
+ content: '';
1311
+ position: absolute;
1312
+ left: -6px;
1313
+ top: 4px;
1314
+ bottom: 4px;
1315
+ width: 3px;
1316
+ background: var(--rmm-btn-primary-bg, #3b82f6);
1317
+ border-radius: 2px;
1318
+ }
1319
+
1320
+ .rmm-taskbar-item-drop-after {
1321
+ position: relative;
1322
+ }
1323
+
1324
+ .rmm-taskbar-item-drop-after::after {
1325
+ content: '';
1326
+ position: absolute;
1327
+ right: -6px;
1328
+ top: 4px;
1329
+ bottom: 4px;
1330
+ width: 3px;
1331
+ background: var(--rmm-btn-primary-bg, #3b82f6);
1332
+ border-radius: 2px;
1333
+ }
1334
+
1207
1335
  /* ============================================
1208
1336
  Responsive
1209
1337
  ============================================ */
@@ -495,8 +495,10 @@ export default class extends Controller {
495
495
  this.applyStyles()
496
496
 
497
497
  // Handle minimize (modal and overlay)
498
+ // 복구된 모달(isRestored)은 isMinimized가 true지만 화면에 표시되어야 함
498
499
  const overlay = this.getOverlay()
499
- if (config.isMinimized) {
500
+ const shouldMinimize = config.isMinimized && !config.isRestored
501
+ if (shouldMinimize) {
500
502
  this.element.classList.add('rmm-minimized')
501
503
  if (overlay) overlay.classList.add('rmm-minimized')
502
504
  } else {
@@ -5,12 +5,17 @@ import historyStackManager from "rails_modal_manager/history_stack_manager"
5
5
  /**
6
6
  * Rails Modal Manager - Taskbar Controller
7
7
  * Shows minimized modals at the bottom of the screen
8
+ * Supports keyboard shortcuts (Alt+1~9) for quick restore/minimize
9
+ * Supports drag and drop to reorder minimized modals
8
10
  */
9
11
  export default class extends Controller {
10
12
  static targets = ["container", "items"]
11
13
 
12
14
  connect() {
13
15
  this.unsubscribe = modalStore.subscribe(() => this.render())
16
+ this.handleKeyDown = this.handleKeyDown.bind(this)
17
+ document.addEventListener('keydown', this.handleKeyDown)
18
+ this.draggedIndex = null
14
19
  this.render()
15
20
  }
16
21
 
@@ -18,6 +23,32 @@ export default class extends Controller {
18
23
  if (this.unsubscribe) {
19
24
  this.unsubscribe()
20
25
  }
26
+ document.removeEventListener('keydown', this.handleKeyDown)
27
+ }
28
+
29
+ handleKeyDown(e) {
30
+ // Alt+1~9 단축키 처리
31
+ if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
32
+ const keyNum = parseInt(e.key, 10)
33
+ if (keyNum >= 1 && keyNum <= 9) {
34
+ const groups = modalStore.getMinimizedModalGroups()
35
+ const index = keyNum - 1
36
+
37
+ if (index < groups.length) {
38
+ e.preventDefault()
39
+ e.stopPropagation()
40
+
41
+ const group = groups[index]
42
+ if (group.isRestored) {
43
+ // 복구된 상태면 다시 최소화
44
+ modalStore.minimizeModalGroup(group.leafModalId)
45
+ } else {
46
+ // 최소화 상태면 복구
47
+ this.restoreByModalId(group.leafModalId)
48
+ }
49
+ }
50
+ }
51
+ }
21
52
  }
22
53
 
23
54
  render() {
@@ -34,16 +65,37 @@ export default class extends Controller {
34
65
  this.element.classList.add('rmm-taskbar-visible')
35
66
 
36
67
  if (this.hasItemsTarget) {
37
- this.itemsTarget.innerHTML = groups.map(group => this.renderTaskbarItem(group)).join('')
68
+ // 최소화된 모달이 2개 이상이면 "모두 닫기" 버튼 추가
69
+ const closeAllBtn = groups.length >= 2 ? this.renderCloseAllButton() : ''
70
+ const items = groups.map((group, index) => this.renderTaskbarItem(group, index)).join('')
71
+ this.itemsTarget.innerHTML = closeAllBtn + items
38
72
  }
39
73
  }
40
74
 
41
- renderTaskbarItem(group) {
75
+ renderCloseAllButton() {
76
+ return `
77
+ <button type="button" class="rmm-taskbar-close-all" data-action="click->rmm-taskbar#closeAll" title="최소화된 모달 모두 닫기">
78
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
79
+ <line x1="18" y1="6" x2="6" y2="18"></line>
80
+ <line x1="6" y1="6" x2="18" y2="18"></line>
81
+ </svg>
82
+ </button>
83
+ `
84
+ }
85
+
86
+ renderTaskbarItem(group, index) {
42
87
  const groupBadge = group.groupSize > 1 ? `<span class="rmm-taskbar-badge">+${group.groupSize - 1}</span>` : ''
88
+ const isRestored = group.isRestored
89
+ const disabledClass = isRestored ? ' rmm-taskbar-item-disabled' : ''
90
+
91
+ // 드래그 이벤트 + 클릭 이벤트 (복구된 상태가 아닐 때만 클릭 이벤트 추가)
92
+ const dragEvents = 'dragstart->rmm-taskbar#dragStart dragend->rmm-taskbar#dragEnd dragover->rmm-taskbar#dragOver drop->rmm-taskbar#drop dragenter->rmm-taskbar#dragEnter dragleave->rmm-taskbar#dragLeave'
93
+ const clickEvent = isRestored ? '' : ' click->rmm-taskbar#restoreItem'
94
+ const dataAction = `data-action="${dragEvents}${clickEvent}"`
43
95
 
44
96
  return `
45
- <div class="rmm-taskbar-item" data-modal-id="${group.leafModalId}" data-root-id="${group.rootModalId}">
46
- <span class="rmm-taskbar-item-title" data-action="click->rmm-taskbar#restore" data-modal-id="${group.leafModalId}" style="cursor: pointer;">${this.escapeHtml(group.title)}</span>
97
+ <div class="rmm-taskbar-item${disabledClass}" data-modal-id="${group.leafModalId}" data-root-id="${group.rootModalId}" draggable="true" data-index="${index}" ${dataAction}>
98
+ <span class="rmm-taskbar-item-title">${this.escapeHtml(group.title)}</span>
47
99
  ${groupBadge}
48
100
  <button type="button" class="rmm-taskbar-item-close" data-action="click->rmm-taskbar#closeGroup" data-modal-id="${group.rootModalId}" title="닫기">
49
101
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -55,36 +107,152 @@ export default class extends Controller {
55
107
  `
56
108
  }
57
109
 
110
+ // ============================================
111
+ // Drag and Drop Handlers
112
+ // ============================================
113
+
114
+ dragStart(e) {
115
+ const item = e.currentTarget
116
+ this.draggedIndex = parseInt(item.dataset.index, 10)
117
+ item.classList.add('rmm-taskbar-item-dragging')
118
+
119
+ // 드래그 이미지 설정 (반투명)
120
+ e.dataTransfer.effectAllowed = 'move'
121
+ e.dataTransfer.setData('text/plain', this.draggedIndex.toString())
122
+
123
+ // 약간의 지연 후 드래깅 클래스 추가 (드래그 이미지에 영향 안주게)
124
+ setTimeout(() => {
125
+ item.classList.add('rmm-taskbar-item-dragging-active')
126
+ }, 0)
127
+ }
128
+
129
+ dragEnd(e) {
130
+ const item = e.currentTarget
131
+ item.classList.remove('rmm-taskbar-item-dragging', 'rmm-taskbar-item-dragging-active')
132
+ this.draggedIndex = null
133
+
134
+ // 모든 드롭 타겟 스타일 제거
135
+ this.element.querySelectorAll('.rmm-taskbar-item').forEach(el => {
136
+ el.classList.remove('rmm-taskbar-item-drop-before', 'rmm-taskbar-item-drop-after')
137
+ })
138
+ }
139
+
140
+ dragOver(e) {
141
+ e.preventDefault()
142
+ e.dataTransfer.dropEffect = 'move'
143
+ }
144
+
145
+ dragEnter(e) {
146
+ e.preventDefault()
147
+ const item = e.currentTarget
148
+ const targetIndex = parseInt(item.dataset.index, 10)
149
+
150
+ if (this.draggedIndex === null || targetIndex === this.draggedIndex) return
151
+
152
+ // 드롭 위치 표시
153
+ this.element.querySelectorAll('.rmm-taskbar-item').forEach(el => {
154
+ el.classList.remove('rmm-taskbar-item-drop-before', 'rmm-taskbar-item-drop-after')
155
+ })
156
+
157
+ if (targetIndex < this.draggedIndex) {
158
+ item.classList.add('rmm-taskbar-item-drop-before')
159
+ } else {
160
+ item.classList.add('rmm-taskbar-item-drop-after')
161
+ }
162
+ }
163
+
164
+ dragLeave(e) {
165
+ const item = e.currentTarget
166
+ // relatedTarget이 자식 요소인 경우 무시
167
+ if (item.contains(e.relatedTarget)) return
168
+
169
+ item.classList.remove('rmm-taskbar-item-drop-before', 'rmm-taskbar-item-drop-after')
170
+ }
171
+
172
+ drop(e) {
173
+ e.preventDefault()
174
+ const item = e.currentTarget
175
+ const targetIndex = parseInt(item.dataset.index, 10)
176
+
177
+ // 모든 드롭 타겟 스타일 제거
178
+ this.element.querySelectorAll('.rmm-taskbar-item').forEach(el => {
179
+ el.classList.remove('rmm-taskbar-item-drop-before', 'rmm-taskbar-item-drop-after')
180
+ })
181
+
182
+ if (this.draggedIndex === null || targetIndex === this.draggedIndex) return
183
+
184
+ // 순서 변경
185
+ modalStore.reorderMinimizedModalGroups(this.draggedIndex, targetIndex)
186
+ this.draggedIndex = null
187
+ }
188
+
189
+ // ============================================
190
+ // Click Handlers
191
+ // ============================================
192
+
193
+ restoreItem(e) {
194
+ // X버튼 클릭은 제외
195
+ if (e.target.closest('.rmm-taskbar-item-close')) {
196
+ return
197
+ }
198
+ e.stopPropagation()
199
+ const item = e.currentTarget
200
+ const modalId = item.dataset.modalId
201
+ if (modalId) {
202
+ this.restoreByModalId(modalId)
203
+ }
204
+ }
205
+
58
206
  restore(e) {
59
207
  e.stopPropagation()
60
208
  const modalId = e.currentTarget.dataset.modalId || e.target.closest('[data-modal-id]')?.dataset.modalId
61
209
  if (modalId) {
62
- // Re-add to history stack
63
- const rootId = modalStore.getRootModal(modalId)
64
- const descendants = modalStore.getAllDescendants(rootId)
65
- const allModalIds = [rootId, ...descendants]
66
-
67
- // Restore all modals in group
68
- modalStore.restoreModalGroup(modalId)
69
-
70
- // Re-add history entries for each modal
71
- allModalIds.forEach(id => {
72
- const modalElement = document.getElementById(id)
73
- if (modalElement) {
74
- const controller = this.application.getControllerForElementAndIdentifier(modalElement, 'rmm-modal')
75
- if (controller && controller.enableHistoryStackValue && !historyStackManager.hasHisData('modal', id)) {
76
- historyStackManager.addHisData('modal', id, () => controller.close('history'))
77
- }
78
- }
79
- })
210
+ this.restoreByModalId(modalId)
80
211
  }
81
212
  }
82
213
 
214
+ restoreByModalId(modalId) {
215
+ // Re-add to history stack
216
+ const rootId = modalStore.getRootModal(modalId)
217
+ const descendants = modalStore.getAllDescendants(rootId)
218
+ const allModalIds = [rootId, ...descendants]
219
+
220
+ // Restore all modals in group
221
+ modalStore.restoreModalGroup(modalId)
222
+
223
+ // Re-add history entries for each modal
224
+ allModalIds.forEach(id => {
225
+ const modalElement = document.getElementById(id)
226
+ if (modalElement) {
227
+ const controller = this.application.getControllerForElementAndIdentifier(modalElement, 'rmm-modal')
228
+ if (controller && controller.enableHistoryStackValue && !historyStackManager.hasHisData('modal', id)) {
229
+ historyStackManager.addHisData('modal', id, () => controller.close('history'))
230
+ }
231
+ }
232
+ })
233
+ }
234
+
83
235
  closeGroup(e) {
84
236
  e.stopPropagation()
85
237
  const modalId = e.currentTarget.dataset.modalId
86
238
  if (modalId) {
87
- modalStore.closeModalWithDescendants(modalId)
239
+ const confirmed = window.confirm('이 모달을 닫으시겠습니까?')
240
+ if (confirmed) {
241
+ modalStore.closeModalWithDescendants(modalId)
242
+ }
243
+ }
244
+ }
245
+
246
+ closeAll(e) {
247
+ e.stopPropagation()
248
+
249
+ const groups = modalStore.getMinimizedModalGroups()
250
+ if (groups.length < 2) return
251
+
252
+ // 확인 다이얼로그 표시
253
+ const confirmed = window.confirm(`최소화된 모달 ${groups.length}개를 모두 닫으시겠습니까?`)
254
+ if (confirmed) {
255
+ modalStore.closeAllMinimizedModals()
88
256
  }
89
257
  }
90
258
 
@@ -8,7 +8,7 @@
8
8
  * @license MIT
9
9
  */
10
10
 
11
- export const VERSION = "1.0.10"
11
+ export const VERSION = "1.0.33"
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"
@@ -87,11 +87,16 @@ export function openModal(modalId) {
87
87
  const modalElement = document.getElementById(modalId)
88
88
  if (!modalElement) return
89
89
 
90
- // Check if the modal is already open and minimized
90
+ // Check if the modal is already open
91
91
  const config = modalStore.getModalConfig(modalId)
92
- if (config && config.isMinimized) {
93
- // Restore the minimized modal
94
- modalStore.restoreModalGroup(modalId)
92
+ if (config) {
93
+ if (config.isMinimized && !config.isRestored) {
94
+ // 최소화 상태(비활성화 아님)면 복구
95
+ modalStore.restoreModalGroup(modalId)
96
+ } else {
97
+ // 이미 열려있거나 복구된 상태면 앞으로 가져오기
98
+ modalStore.bringToFront(modalId)
99
+ }
95
100
  return
96
101
  }
97
102
 
@@ -86,6 +86,15 @@ class ModalStore {
86
86
 
87
87
  openModal(modalId, config) {
88
88
  if (this.activeModals[modalId]) {
89
+ // 이미 열려있는 모달 처리
90
+ const existingConfig = this.activeModals[modalId];
91
+ if (existingConfig.isMinimized && !existingConfig.isRestored) {
92
+ // 최소화 상태(비활성화 아님)면 복구
93
+ this.restoreModalGroup(modalId);
94
+ } else {
95
+ // 이미 열려있거나 복구된 상태면 앞으로 가져오기
96
+ this.bringToFront(modalId);
97
+ }
89
98
  return;
90
99
  }
91
100
 
@@ -368,13 +377,68 @@ class ModalStore {
368
377
  this.notify();
369
378
  }
370
379
 
380
+ closeAllMinimizedModals() {
381
+ const groups = this.getMinimizedModalGroups();
382
+ // 모든 최소화된 그룹을 닫음
383
+ groups.forEach(group => {
384
+ this.closeModalWithDescendants(group.rootModalId);
385
+ });
386
+ }
387
+
388
+ // 단축키용: 특정 인덱스의 최소화된 모달 그룹 토글 (복구/최소화)
389
+ toggleMinimizedModalByIndex(index) {
390
+ const groups = this.getMinimizedModalGroups();
391
+ if (index < 0 || index >= groups.length) return false;
392
+
393
+ const group = groups[index];
394
+ if (group.isRestored) {
395
+ // 복구된 상태면 다시 최소화
396
+ this.minimizeModalGroup(group.leafModalId);
397
+ } else {
398
+ // 최소화 상태면 복구
399
+ this.restoreModalGroup(group.leafModalId);
400
+ }
401
+ return true;
402
+ }
403
+
404
+ // 최소화된 모달 그룹 순서 변경 (드래그앤드롭용)
405
+ reorderMinimizedModalGroups(fromIndex, toIndex) {
406
+ const groups = this.getMinimizedModalGroups();
407
+ if (fromIndex < 0 || fromIndex >= groups.length) return false;
408
+ if (toIndex < 0 || toIndex >= groups.length) return false;
409
+ if (fromIndex === toIndex) return false;
410
+
411
+ // 순서를 변경하기 위해 minimizedAt 타임스탬프 재할당
412
+ // 기존 순서에서 fromIndex 항목을 toIndex 위치로 이동
413
+ const reordered = [...groups];
414
+ const [moved] = reordered.splice(fromIndex, 1);
415
+ reordered.splice(toIndex, 0, moved);
416
+
417
+ // 새로운 순서에 맞게 minimizedAt 타임스탬프 재할당
418
+ const baseTime = Date.now();
419
+ reordered.forEach((group, index) => {
420
+ const allModalIds = group.modalIds;
421
+ const newTimestamp = baseTime + index;
422
+
423
+ allModalIds.forEach(id => {
424
+ if (this.activeModals[id]) {
425
+ this.activeModals[id].minimizedAt = newTimestamp;
426
+ }
427
+ });
428
+ });
429
+
430
+ this.notify();
431
+ return true;
432
+ }
433
+
371
434
  // ============================================
372
435
  // Body Scroll Management
373
436
  // ============================================
374
437
 
375
438
  hasVisibleOverlayModals() {
376
439
  return Object.values(this.activeModals).some(
377
- config => !config.isMinimized && !config.hideOverlay
440
+ // 복구된 모달(isRestored)은 화면에 보이므로 visible로 처리
441
+ config => (!config.isMinimized || config.isRestored) && !config.hideOverlay
378
442
  );
379
443
  }
380
444
 
@@ -447,14 +511,29 @@ class ModalStore {
447
511
  const rootId = this.getRootModal(modalId);
448
512
  const descendants = this.getAllDescendants(rootId);
449
513
  const allModalIds = [rootId, ...descendants];
450
- const minimizedAt = Date.now();
451
514
 
452
- allModalIds.forEach(id => {
453
- if (this.activeModals[id]) {
454
- this.activeModals[id].isMinimized = true;
455
- this.activeModals[id].minimizedAt = minimizedAt;
456
- }
457
- });
515
+ // 이미 최소화 상태인지 확인 (복구 다시 최소화하는 경우)
516
+ const rootConfig = this.activeModals[rootId];
517
+ const wasRestored = rootConfig?.isRestored;
518
+
519
+ if (wasRestored) {
520
+ // 복구 상태였다면 isRestored만 false로 변경 (위치 유지)
521
+ allModalIds.forEach(id => {
522
+ if (this.activeModals[id]) {
523
+ this.activeModals[id].isRestored = false;
524
+ }
525
+ });
526
+ } else {
527
+ // 새로운 최소화인 경우
528
+ const minimizedAt = Date.now();
529
+ allModalIds.forEach(id => {
530
+ if (this.activeModals[id]) {
531
+ this.activeModals[id].isMinimized = true;
532
+ this.activeModals[id].isRestored = false;
533
+ this.activeModals[id].minimizedAt = minimizedAt;
534
+ }
535
+ });
536
+ }
458
537
 
459
538
  this.updateBodyScroll();
460
539
  this.notify();
@@ -466,9 +545,10 @@ class ModalStore {
466
545
  const descendants = this.getAllDescendants(rootId);
467
546
  const allModalIds = [rootId, ...descendants];
468
547
 
548
+ // isMinimized는 유지하고 isRestored를 true로 설정 (태스크바에 비활성화 상태로 유지)
469
549
  allModalIds.forEach(id => {
470
550
  if (this.activeModals[id]) {
471
- this.activeModals[id].isMinimized = false;
551
+ this.activeModals[id].isRestored = true;
472
552
  }
473
553
  });
474
554
 
@@ -515,6 +595,7 @@ class ModalStore {
515
595
  zIndex: leafConfig?.zIndex || 0,
516
596
  modalIds: allModalIds,
517
597
  minimizedAt: rootConfig?.minimizedAt || 0,
598
+ isRestored: rootConfig?.isRestored || false, // 복구된 상태인지 (비활성화 표시용)
518
599
  };
519
600
  });
520
601
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsModalManager
4
- VERSION = "1.0.32"
4
+ VERSION = "1.0.33"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_modal_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.32
4
+ version: 1.0.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - reshacs