rails_modal_manager 1.0.39 → 1.0.41
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/app/javascript/rails_modal_manager/controllers/rmm_header_controller.js +100 -1
- data/app/javascript/rails_modal_manager/controllers/rmm_taskbar_controller.js +99 -3
- data/app/javascript/rails_modal_manager/modal_store.js +40 -1
- 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: 5884364bb07444f45c08e4733c5623883a775592ba853a1275f5f2bd84bb5c03
|
|
4
|
+
data.tar.gz: 06a78be76359c81f55ec9e904488a1f3d961fdababf4bb8b31c24d0d19a8b7e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 985fca2add00de7fca1c9c5d05d66be9b8a6dbc208526de6811dad2c4b27977be9aa332dce1287d561d0fe04c3ec365768abbeeac89f582f07af8250aed843e2
|
|
7
|
+
data.tar.gz: ab10ad1ad3a86cd5a6ad64af536d015cbfeccc030523e03c980c74cb042cab25bd70bd936cd7247980097a18140d2c0969f18c9afb6c6df8fc66301c959c4232
|
|
@@ -11,7 +11,7 @@ export default class extends Controller {
|
|
|
11
11
|
defaultSize: { type: String, default: "md" },
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
static targets = ["maximizeBtn", "sidebarToggle"]
|
|
14
|
+
static targets = ["maximizeBtn", "sidebarToggle", "title"]
|
|
15
15
|
|
|
16
16
|
connect() {
|
|
17
17
|
// Listen for sidebar toggled event to update icon
|
|
@@ -20,6 +20,17 @@ export default class extends Controller {
|
|
|
20
20
|
if (modal) {
|
|
21
21
|
modal.addEventListener('rmm-sidebar:toggled', this.handleSidebarToggled)
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
// Title 더블클릭 이벤트 리스너 추가
|
|
25
|
+
this.setupTitleEdit()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setupTitleEdit() {
|
|
29
|
+
const titleEl = this.element.querySelector('.rmm-header-title')
|
|
30
|
+
if (titleEl) {
|
|
31
|
+
titleEl.addEventListener('dblclick', (e) => this.editTitle(e))
|
|
32
|
+
titleEl.style.cursor = 'default'
|
|
33
|
+
}
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
disconnect() {
|
|
@@ -173,4 +184,92 @@ export default class extends Controller {
|
|
|
173
184
|
if (hiddenIcon) hiddenIcon.style.display = 'none'
|
|
174
185
|
}
|
|
175
186
|
}
|
|
187
|
+
|
|
188
|
+
// ============================================
|
|
189
|
+
// Title Edit (Double-click to edit)
|
|
190
|
+
// ============================================
|
|
191
|
+
|
|
192
|
+
editTitle(e) {
|
|
193
|
+
e.stopPropagation()
|
|
194
|
+
e.preventDefault()
|
|
195
|
+
|
|
196
|
+
const titleEl = e.currentTarget
|
|
197
|
+
if (!titleEl) return
|
|
198
|
+
|
|
199
|
+
// 이미 수정 중인 경우 무시
|
|
200
|
+
if (titleEl.querySelector('input')) return
|
|
201
|
+
|
|
202
|
+
const currentTitle = titleEl.textContent.trim()
|
|
203
|
+
|
|
204
|
+
// input 생성
|
|
205
|
+
const input = document.createElement('input')
|
|
206
|
+
input.type = 'text'
|
|
207
|
+
input.value = currentTitle
|
|
208
|
+
input.className = 'rmm-header-title-input'
|
|
209
|
+
input.style.cssText = `
|
|
210
|
+
width: 100%;
|
|
211
|
+
min-width: 100px;
|
|
212
|
+
max-width: 300px;
|
|
213
|
+
padding: 2px 6px;
|
|
214
|
+
border: 1px solid rgba(255,255,255,0.5);
|
|
215
|
+
border-radius: 4px;
|
|
216
|
+
font-size: inherit;
|
|
217
|
+
font-weight: inherit;
|
|
218
|
+
font-family: inherit;
|
|
219
|
+
background: rgba(0,0,0,0.2);
|
|
220
|
+
color: inherit;
|
|
221
|
+
outline: none;
|
|
222
|
+
`
|
|
223
|
+
|
|
224
|
+
// title 내용을 input으로 교체
|
|
225
|
+
titleEl.textContent = ''
|
|
226
|
+
titleEl.appendChild(input)
|
|
227
|
+
input.focus()
|
|
228
|
+
input.select()
|
|
229
|
+
|
|
230
|
+
// 저장 함수
|
|
231
|
+
const saveTitle = () => {
|
|
232
|
+
const newTitle = input.value.trim()
|
|
233
|
+
const finalTitle = newTitle || currentTitle
|
|
234
|
+
|
|
235
|
+
// title 업데이트
|
|
236
|
+
titleEl.textContent = finalTitle
|
|
237
|
+
|
|
238
|
+
// modalStore의 title도 업데이트
|
|
239
|
+
if (newTitle && newTitle !== currentTitle) {
|
|
240
|
+
this.updateModalTitle(finalTitle)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// blur 시 저장
|
|
245
|
+
input.addEventListener('blur', saveTitle, { once: true })
|
|
246
|
+
|
|
247
|
+
// Enter 시 저장, Escape 시 취소
|
|
248
|
+
input.addEventListener('keydown', (ke) => {
|
|
249
|
+
if (ke.key === 'Enter') {
|
|
250
|
+
ke.preventDefault()
|
|
251
|
+
input.blur()
|
|
252
|
+
} else if (ke.key === 'Escape') {
|
|
253
|
+
ke.preventDefault()
|
|
254
|
+
input.removeEventListener('blur', saveTitle)
|
|
255
|
+
titleEl.textContent = currentTitle
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
updateModalTitle(newTitle) {
|
|
261
|
+
const modal = this.getModalElement()
|
|
262
|
+
if (!modal) return
|
|
263
|
+
|
|
264
|
+
const modalId = modal.id || this.modalIdValue
|
|
265
|
+
if (!modalId) return
|
|
266
|
+
|
|
267
|
+
// modalStore에서 title 업데이트
|
|
268
|
+
import("rails_modal_manager/modal_store").then(module => {
|
|
269
|
+
const modalStore = module.default
|
|
270
|
+
if (modalStore && modalStore.updateModal) {
|
|
271
|
+
modalStore.updateModal(modalId, { title: newTitle })
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
176
275
|
}
|
|
@@ -69,9 +69,21 @@ export default class extends Controller {
|
|
|
69
69
|
const closeAllBtn = groups.length >= 2 ? this.renderCloseAllButton() : ''
|
|
70
70
|
const items = groups.map((group, index) => this.renderTaskbarItem(group, index)).join('')
|
|
71
71
|
this.itemsTarget.innerHTML = closeAllBtn + items
|
|
72
|
+
|
|
73
|
+
// 복구된 상태의 아이템에 더블클릭 이벤트 직접 추가
|
|
74
|
+
this.attachTitleEditListeners()
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
attachTitleEditListeners() {
|
|
79
|
+
const disabledItems = this.element.querySelectorAll('.rmm-taskbar-item-disabled')
|
|
80
|
+
disabledItems.forEach(item => {
|
|
81
|
+
// 기존 리스너 제거를 위해 새 함수 참조 저장
|
|
82
|
+
item._dblclickHandler = (e) => this.editTitle(e, item)
|
|
83
|
+
item.addEventListener('dblclick', item._dblclickHandler)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
renderCloseAllButton() {
|
|
76
88
|
return `
|
|
77
89
|
<button type="button" class="rmm-taskbar-close-all" data-action="click->rmm-taskbar#closeAll" title="최소화된 모달 모두 닫기">
|
|
@@ -89,6 +101,7 @@ export default class extends Controller {
|
|
|
89
101
|
const disabledClass = isRestored ? ' rmm-taskbar-item-disabled' : ''
|
|
90
102
|
|
|
91
103
|
// 드래그 이벤트 + 클릭 이벤트 (복구된 상태가 아닐 때만 클릭 이벤트 추가)
|
|
104
|
+
// 복구된 상태에서는 더블클릭으로 title 수정 가능 (attachTitleEditListeners에서 처리)
|
|
92
105
|
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
106
|
const clickEvent = isRestored ? '' : ' click->rmm-taskbar#restoreItem'
|
|
94
107
|
const dataAction = `data-action="${dragEvents}${clickEvent}"`
|
|
@@ -240,9 +253,19 @@ export default class extends Controller {
|
|
|
240
253
|
e.stopPropagation()
|
|
241
254
|
const modalId = e.currentTarget.dataset.modalId
|
|
242
255
|
if (modalId) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
// 해당 그룹이 복구된 상태인지 확인
|
|
257
|
+
const groups = modalStore.getMinimizedModalGroups()
|
|
258
|
+
const group = groups.find(g => g.rootModalId === modalId)
|
|
259
|
+
|
|
260
|
+
if (group && group.isRestored) {
|
|
261
|
+
// 복구된 상태: taskbar에서만 제거, 모달은 유지
|
|
262
|
+
modalStore.clearMinimizedStateGroup(modalId)
|
|
263
|
+
} else {
|
|
264
|
+
// 최소화 상태: 확인 후 모달 닫기
|
|
265
|
+
const confirmed = window.confirm('이 모달을 닫으시겠습니까?')
|
|
266
|
+
if (confirmed) {
|
|
267
|
+
modalStore.closeModalWithDescendants(modalId)
|
|
268
|
+
}
|
|
246
269
|
}
|
|
247
270
|
}
|
|
248
271
|
}
|
|
@@ -260,6 +283,79 @@ export default class extends Controller {
|
|
|
260
283
|
}
|
|
261
284
|
}
|
|
262
285
|
|
|
286
|
+
// ============================================
|
|
287
|
+
// Title Edit (Double-click on disabled/restored item)
|
|
288
|
+
// ============================================
|
|
289
|
+
|
|
290
|
+
editTitle(e, itemElement = null) {
|
|
291
|
+
e.stopPropagation()
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
|
|
294
|
+
const item = itemElement || e.currentTarget
|
|
295
|
+
const titleSpan = item.querySelector('.rmm-taskbar-item-title')
|
|
296
|
+
const rootModalId = item.dataset.rootId
|
|
297
|
+
|
|
298
|
+
if (!titleSpan || !rootModalId) return
|
|
299
|
+
|
|
300
|
+
// 이미 수정 중인 경우 무시
|
|
301
|
+
if (titleSpan.querySelector('input')) return
|
|
302
|
+
|
|
303
|
+
const currentTitle = titleSpan.textContent
|
|
304
|
+
const originalWidth = titleSpan.offsetWidth
|
|
305
|
+
|
|
306
|
+
// input 생성
|
|
307
|
+
const input = document.createElement('input')
|
|
308
|
+
input.type = 'text'
|
|
309
|
+
input.value = currentTitle
|
|
310
|
+
input.className = 'rmm-taskbar-title-input'
|
|
311
|
+
input.style.cssText = `
|
|
312
|
+
width: ${Math.max(originalWidth, 80)}px;
|
|
313
|
+
min-width: 80px;
|
|
314
|
+
max-width: 200px;
|
|
315
|
+
padding: 2px 4px;
|
|
316
|
+
border: 1px solid #60a5fa;
|
|
317
|
+
border-radius: 3px;
|
|
318
|
+
font-size: inherit;
|
|
319
|
+
font-family: inherit;
|
|
320
|
+
background: #1e293b;
|
|
321
|
+
color: #f1f5f9;
|
|
322
|
+
outline: none;
|
|
323
|
+
`
|
|
324
|
+
|
|
325
|
+
// title 내용을 input으로 교체
|
|
326
|
+
titleSpan.textContent = ''
|
|
327
|
+
titleSpan.appendChild(input)
|
|
328
|
+
input.focus()
|
|
329
|
+
input.select()
|
|
330
|
+
|
|
331
|
+
// 저장 함수
|
|
332
|
+
const saveTitle = () => {
|
|
333
|
+
const newTitle = input.value.trim()
|
|
334
|
+
if (newTitle && newTitle !== currentTitle) {
|
|
335
|
+
// taskbar title만 업데이트 (모달 title은 유지)
|
|
336
|
+
modalStore.updateTaskbarTitle(rootModalId, newTitle)
|
|
337
|
+
}
|
|
338
|
+
// input을 text로 다시 변경
|
|
339
|
+
titleSpan.textContent = newTitle || currentTitle
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// blur 시 저장
|
|
343
|
+
input.addEventListener('blur', saveTitle, { once: true })
|
|
344
|
+
|
|
345
|
+
// Enter 시 저장, Escape 시 취소
|
|
346
|
+
input.addEventListener('keydown', (ke) => {
|
|
347
|
+
if (ke.key === 'Enter') {
|
|
348
|
+
ke.preventDefault()
|
|
349
|
+
input.blur()
|
|
350
|
+
} else if (ke.key === 'Escape') {
|
|
351
|
+
ke.preventDefault()
|
|
352
|
+
// blur 이벤트 제거하고 원래 title로 복원
|
|
353
|
+
input.removeEventListener('blur', saveTitle)
|
|
354
|
+
titleSpan.textContent = currentTitle
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
263
359
|
escapeHtml(text) {
|
|
264
360
|
const div = document.createElement('div')
|
|
265
361
|
div.textContent = text
|
|
@@ -141,6 +141,19 @@ class ModalStore {
|
|
|
141
141
|
this.notify();
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Update taskbar-only title for a modal group
|
|
146
|
+
* This does NOT change the actual modal title, only the taskbar display
|
|
147
|
+
* @param {string} rootModalId - Root modal ID of the group
|
|
148
|
+
* @param {string} newTitle - New title to display in taskbar
|
|
149
|
+
*/
|
|
150
|
+
updateTaskbarTitle(rootModalId, newTitle) {
|
|
151
|
+
if (!this.activeModals[rootModalId]) return;
|
|
152
|
+
|
|
153
|
+
this.activeModals[rootModalId].taskbarTitle = newTitle;
|
|
154
|
+
this.notify();
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
bringToFront(modalId) {
|
|
145
158
|
if (!this.activeModals[modalId]) return;
|
|
146
159
|
if (this.modalOrder[this.modalOrder.length - 1] === modalId) return;
|
|
@@ -575,6 +588,29 @@ class ModalStore {
|
|
|
575
588
|
this.notify();
|
|
576
589
|
}
|
|
577
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Clear minimized state for a modal group (remove from taskbar but keep modal open)
|
|
593
|
+
* Used when closing taskbar item for a restored (visible) modal
|
|
594
|
+
* @param {string} modalId - Any modal ID in the group
|
|
595
|
+
*/
|
|
596
|
+
clearMinimizedStateGroup(modalId) {
|
|
597
|
+
const rootId = this.getRootModal(modalId);
|
|
598
|
+
const descendants = this.getAllDescendants(rootId);
|
|
599
|
+
const allModalIds = [rootId, ...descendants];
|
|
600
|
+
|
|
601
|
+
// isMinimized와 isRestored 모두 false로 설정 (taskbar에서 제거, 모달은 유지)
|
|
602
|
+
allModalIds.forEach(id => {
|
|
603
|
+
if (this.activeModals[id]) {
|
|
604
|
+
this.activeModals[id].isMinimized = false;
|
|
605
|
+
this.activeModals[id].isRestored = false;
|
|
606
|
+
delete this.activeModals[id].minimizedAt;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
this.updateBodyScroll();
|
|
611
|
+
this.notify();
|
|
612
|
+
}
|
|
613
|
+
|
|
578
614
|
getMinimizedModalGroups() {
|
|
579
615
|
const minimizedRoots = new Set();
|
|
580
616
|
const processedModals = new Set();
|
|
@@ -603,10 +639,13 @@ class ModalStore {
|
|
|
603
639
|
const leafConfig = this.activeModals[leafModalId];
|
|
604
640
|
const rootConfig = this.activeModals[rootId];
|
|
605
641
|
|
|
642
|
+
// taskbarTitle이 있으면 우선 사용 (모달 title과 별도로 관리)
|
|
643
|
+
const displayTitle = rootConfig?.taskbarTitle || leafConfig?.title || leafModalId;
|
|
644
|
+
|
|
606
645
|
return {
|
|
607
646
|
rootModalId: rootId,
|
|
608
647
|
leafModalId,
|
|
609
|
-
title:
|
|
648
|
+
title: displayTitle,
|
|
610
649
|
groupSize: allModalIds.length,
|
|
611
650
|
zIndex: leafConfig?.zIndex || 0,
|
|
612
651
|
modalIds: allModalIds,
|