rails_modal_manager 1.0.59 → 1.0.61
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 +10 -0
- data/app/assets/stylesheets/rails_modal_manager.css +60 -0
- data/app/helpers/rails_modal_manager/modal_helper.rb +32 -0
- data/app/javascript/rails_modal_manager/controllers/rmm_modal_controller.js +63 -7
- data/app/javascript/rails_modal_manager/history_stack_manager.js +28 -0
- data/app/javascript/rails_modal_manager/index.js +129 -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: '09dbfd6b3213ab34ae1874fa5e785061ae3e896415949b76fc54d8504df04420'
|
|
4
|
+
data.tar.gz: f64b4b8b42503cbea4dcdfcf7bf439353456f055863803af2628e39b71859b91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de7b11bb9f2ea74aa9afb1c61dec4069e544ec98132aefc9e19f513b9baa26d246cb43d4f7de214ee9646eb6e27e81e76c176721bee0a2e0e765d0668980c8fd
|
|
7
|
+
data.tar.gz: 62a26a444c72824cc5964d14004bdf879505b983bd2016a769d2d53c5998b05107ec17ef4974a28e80f32f29b139024e8338411ce737cb673f8661debe9fa224
|
data/README.md
CHANGED
|
@@ -12,6 +12,13 @@ Rails 애플리케이션을 위한 고급 모달 매니저입니다.
|
|
|
12
12
|
> - **헤더 색상** — 본문의 `#3b82f6` 파란색 → v1.0.57 부터 `#3e6187` 블루 차콜
|
|
13
13
|
> - **푸터 버튼** — `onclick` 은 v1.0.55 에서 deprecated. `type: "submit"` + `form:` 패턴 사용
|
|
14
14
|
> - **z-index** — v1.0.58 에서 계층 재배치 (overlay 1400 < modal 1500~8000 < taskbar 9000 < confirm 10000)
|
|
15
|
+
>
|
|
16
|
+
> ⚠️ **이벤트 이름 (2026-08-07 교정)** — 아래 본문과 구 OPTIONS.md 에 `rmm:closed` / `rmm:minimize` /
|
|
17
|
+
> `rmm:maximize` 로 적혀 있던 것은 **오기입니다. 그 이름으로는 발화되지 않습니다.**
|
|
18
|
+
> 실제 이름은 `rmm-modal:closed` / `rmm-modal:minimize` / `rmm-modal:maximize` 이며
|
|
19
|
+
> (Stimulus `this.dispatch()` 가 컨트롤러 identifier `rmm-modal` 을 접두어로 붙임),
|
|
20
|
+
> `rmm:open` · `rmm:close` 만 `index.js` 가 직접 dispatch 해 접두어가 없습니다.
|
|
21
|
+
> 정확한 목록·권장 패턴은 **[OPTIONS.md → Events](OPTIONS.md#events)** 참조.
|
|
15
22
|
|
|
16
23
|
---
|
|
17
24
|
|
|
@@ -646,6 +653,9 @@ const descendants = modalStore.getAllDescendants('parent-modal')
|
|
|
646
653
|
--rmm-bg: #ffffff;
|
|
647
654
|
--rmm-border: #e2e8f0;
|
|
648
655
|
--rmm-overlay-bg: rgba(0, 0, 0, 0.5);
|
|
656
|
+
/* v1.0.60 — 오버레이 닫기 어포던스. 끄려면 closable 을 default 로 덮으면 된다 */
|
|
657
|
+
--rmm-overlay-cursor: default; /* 오버레이로는 닫을 수 없음 */
|
|
658
|
+
--rmm-overlay-cursor-closable: pointer; /* 클릭 또는 더블클릭으로 닫힘 */
|
|
649
659
|
|
|
650
660
|
/* Header */
|
|
651
661
|
--rmm-header-height: 48px;
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
/* Overlay */
|
|
40
40
|
--rmm-overlay-bg: rgba(0, 0, 0, 0.5);
|
|
41
41
|
--rmm-overlay-blur: 0px;
|
|
42
|
+
/* v1.0.60 — 오버레이 닫기 가능 여부를 커서로 알린다.
|
|
43
|
+
끄려면 프로젝트에서 --rmm-overlay-cursor-closable: default; 로 덮으면 된다. */
|
|
44
|
+
--rmm-overlay-cursor: default; /* 오버레이로는 닫을 수 없음 */
|
|
45
|
+
--rmm-overlay-cursor-closable: pointer; /* 클릭 또는 더블클릭으로 닫힘 */
|
|
42
46
|
|
|
43
47
|
/* Header — v1.0.57 블루 차콜 (사용자 지정 팔레트) */
|
|
44
48
|
--rmm-header-bg: #3e6187;
|
|
@@ -228,6 +232,29 @@
|
|
|
228
232
|
visibility var(--rmm-animation-duration) var(--rmm-animation-timing);
|
|
229
233
|
backdrop-filter: blur(1px);
|
|
230
234
|
-webkit-backdrop-filter: blur(1px);
|
|
235
|
+
/* v1.0.60 — 기본은 "닫을 수 없음". 닫히는 경우만 아래 속성 선택자가 덮는다. */
|
|
236
|
+
cursor: var(--rmm-overlay-cursor);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ⭐v1.0.60 — 오버레이 닫기 어포던스
|
|
240
|
+
오버레이 클릭으로 닫히는지 여부는 지금까지 화면에 아무 단서가 없어서, 사용자는 눌러 보기 전까지
|
|
241
|
+
알 수 없었다(닫히지 않으면 "고장난 것"으로 읽힌다). 정책은 이미 오버레이 요소의 data 속성에
|
|
242
|
+
실려 있으므로, 그 값을 그대로 커서로 표현한다.
|
|
243
|
+
|
|
244
|
+
⚠ 색상이 아니라 커서를 쓰는 이유:
|
|
245
|
+
· 모달 중첩 시 오버레이가 여러 장 겹치는데, 색은 섞여서 판별이 안 되지만 커서는 항상 최상단 것만 보인다
|
|
246
|
+
· 색만으로 정보를 전달하면 색각 이상 사용자가 구분할 수 없다 (WCAG 1.4.1)
|
|
247
|
+
· 학습이 필요 없다 — pointer 는 웹의 보편 규약이다
|
|
248
|
+
|
|
249
|
+
⚠ 선언 순서 주의: dblclick 규칙이 반드시 뒤에 와야 한다.
|
|
250
|
+
rmm_overlay_controller#handleClick 은 `if (!closeOnClick || closeOnDblclick) return` 이라
|
|
251
|
+
더블클릭 모드에서는 close_on_click 값과 무관하게 단일 클릭이 무시된다. 반대로 handleDblclick 은
|
|
252
|
+
closeOnDblclick 만 확인하므로, close_on_click=false + dblclick=true 조합도 "닫을 수 있는" 상태다. */
|
|
253
|
+
.rmm-overlay[data-rmm-overlay-close-on-click-value="true"] {
|
|
254
|
+
cursor: var(--rmm-overlay-cursor-closable);
|
|
255
|
+
}
|
|
256
|
+
.rmm-overlay[data-rmm-overlay-close-on-dblclick-value="true"] {
|
|
257
|
+
cursor: var(--rmm-overlay-cursor-closable);
|
|
231
258
|
}
|
|
232
259
|
|
|
233
260
|
.rmm-overlay.rmm-active {
|
|
@@ -2391,3 +2418,36 @@
|
|
|
2391
2418
|
visibility: hidden !important;
|
|
2392
2419
|
}
|
|
2393
2420
|
}
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
/* ============================================
|
|
2424
|
+
v1.0.61 — Shell (placeholder) modal
|
|
2425
|
+
openModalShell() 로 띄운 틀과, 틀을 제자리 교체할 때의 «애니메이션 없이 즉시» 표시
|
|
2426
|
+
============================================ */
|
|
2427
|
+
.rmm-modal.rmm-instant,
|
|
2428
|
+
.rmm-overlay.rmm-instant {
|
|
2429
|
+
transition: none !important;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
.rmm-shell-skeleton {
|
|
2433
|
+
padding: 20px;
|
|
2434
|
+
display: flex;
|
|
2435
|
+
flex-direction: column;
|
|
2436
|
+
gap: 12px;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
.rmm-shell-line {
|
|
2440
|
+
height: 14px;
|
|
2441
|
+
border-radius: 6px;
|
|
2442
|
+
background: linear-gradient(90deg, #eef2f6 25%, #f8fafc 37%, #eef2f6 63%);
|
|
2443
|
+
background-size: 400% 100%;
|
|
2444
|
+
animation: rmm-shell-shimmer 1.4s ease infinite;
|
|
2445
|
+
}
|
|
2446
|
+
.rmm-shell-line-sm { width: 35%; }
|
|
2447
|
+
.rmm-shell-line-md { width: 60%; }
|
|
2448
|
+
.rmm-shell-line-lg { width: 90%; }
|
|
2449
|
+
|
|
2450
|
+
@keyframes rmm-shell-shimmer {
|
|
2451
|
+
0% { background-position: 100% 50%; }
|
|
2452
|
+
100% { background-position: 0 50%; }
|
|
2453
|
+
}
|
|
@@ -42,6 +42,38 @@ module RailsModalManager
|
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Render the shell (placeholder) modal template — v1.0.61+
|
|
46
|
+
# Render once in the layout. openModalShell() clones it to show a modal frame immediately
|
|
47
|
+
# while the real modal is fetched; replaceModalShell() swaps it in place (same history entry).
|
|
48
|
+
# The block is the loading body (skeleton); a built-in skeleton is used when omitted.
|
|
49
|
+
#
|
|
50
|
+
# @example In layout
|
|
51
|
+
# <%= rmm_modal_shell_template %>
|
|
52
|
+
# <%= rmm_modal_shell_template { render "shared/modal_skeleton" } %>
|
|
53
|
+
#
|
|
54
|
+
def rmm_modal_shell_template(options = {}, &block)
|
|
55
|
+
content = block_given? ? capture(&block) : rmm_default_shell_skeleton
|
|
56
|
+
modal = render(
|
|
57
|
+
partial: "rails_modal_manager/modal",
|
|
58
|
+
locals: {
|
|
59
|
+
title: "__RMM_SHELL_TITLE__",
|
|
60
|
+
size: "md",
|
|
61
|
+
draggable: false,
|
|
62
|
+
resizable: false,
|
|
63
|
+
minimizable: false,
|
|
64
|
+
close_on_overlay: true,
|
|
65
|
+
enable_history_stack: true
|
|
66
|
+
}.merge(options).merge(modal_id: "__RMM_SHELL__", block_content: content)
|
|
67
|
+
)
|
|
68
|
+
content_tag(:template, modal, id: "rmm-modal-shell-template")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def rmm_default_shell_skeleton
|
|
72
|
+
content_tag(:div, class: "rmm-shell-skeleton", "aria-hidden": "true") do
|
|
73
|
+
safe_join(%w[lg md lg sm md lg].map { |w| content_tag(:div, "", class: "rmm-shell-line rmm-shell-line-#{w}") })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
45
77
|
# Render the global taskbar component
|
|
46
78
|
# Should be included once in the layout, typically at the end of body
|
|
47
79
|
#
|
|
@@ -148,8 +148,17 @@ export default class extends Controller {
|
|
|
148
148
|
return // Already open
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// v1.0.61+: 자리표시(shell) 모달 인계 — replaceModalShell() 이 data-rmm-adopt-shell 로 지정.
|
|
152
|
+
// 틀이 이미 차지한 브라우저 history 칸과 포커스 복귀 대상을 넘겨받고, 틀은 애니메이션 없이 걷어낸다.
|
|
153
|
+
const adoptShellId = this.element.dataset.rmmAdoptShell
|
|
154
|
+
let shell = null
|
|
155
|
+
if (adoptShellId) {
|
|
156
|
+
delete this.element.dataset.rmmAdoptShell
|
|
157
|
+
shell = this._takeOverShell(adoptShellId, modalId)
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
// Save previous focus
|
|
152
|
-
this.previousFocus = document.activeElement
|
|
161
|
+
this.previousFocus = shell ? shell.previousFocus : document.activeElement
|
|
153
162
|
|
|
154
163
|
// Check for saved size (use persistentId or modalId)
|
|
155
164
|
let initialSize = this.sizeValue
|
|
@@ -196,8 +205,9 @@ export default class extends Controller {
|
|
|
196
205
|
historyStackManager.addHisData('modal', modalId, () => this.close('history'))
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
// Show modal with animation
|
|
200
|
-
this.showModal()
|
|
208
|
+
// Show modal with animation (틀 인계 시에는 제자리 교체 — 열림 애니메이션 없이 즉시)
|
|
209
|
+
this.showModal({ instant: !!shell })
|
|
210
|
+
if (shell) shell.remove()
|
|
201
211
|
|
|
202
212
|
// Add event listeners
|
|
203
213
|
this.addEventListeners()
|
|
@@ -372,7 +382,7 @@ export default class extends Controller {
|
|
|
372
382
|
// Animation
|
|
373
383
|
// ============================================
|
|
374
384
|
|
|
375
|
-
showModal() {
|
|
385
|
+
showModal({ instant = false } = {}) {
|
|
376
386
|
const modal = this.element
|
|
377
387
|
const overlay = this.getOverlay()
|
|
378
388
|
|
|
@@ -382,8 +392,7 @@ export default class extends Controller {
|
|
|
382
392
|
// Apply depth class for visual hierarchy (parent/child/grandchild)
|
|
383
393
|
this.applyDepthClass()
|
|
384
394
|
|
|
385
|
-
|
|
386
|
-
requestAnimationFrame(() => {
|
|
395
|
+
const activate = () => {
|
|
387
396
|
modal.classList.add('rmm-active')
|
|
388
397
|
if (overlay) overlay.classList.add('rmm-active')
|
|
389
398
|
|
|
@@ -391,7 +400,54 @@ export default class extends Controller {
|
|
|
391
400
|
if (this.focusTrapValue) {
|
|
392
401
|
this.setupFocusTrap()
|
|
393
402
|
}
|
|
394
|
-
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (instant) {
|
|
406
|
+
// v1.0.61+: 틀(shell) 교체 — 같은 태스크 안에서 틀 제거와 함께 반영돼 중간 프레임이 그려지지 않는다
|
|
407
|
+
modal.classList.add('rmm-instant')
|
|
408
|
+
if (overlay) overlay.classList.add('rmm-instant')
|
|
409
|
+
activate()
|
|
410
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
411
|
+
modal.classList.remove('rmm-instant')
|
|
412
|
+
if (overlay) overlay.classList.remove('rmm-instant')
|
|
413
|
+
}))
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Start animation
|
|
418
|
+
requestAnimationFrame(activate)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* v1.0.61+: 자리표시(shell) 모달의 자리를 넘겨받는다.
|
|
423
|
+
* - history: 틀이 push 한 칸을 그대로 두고 id·닫기 함수만 이 모달로 이관(replaceHisData)
|
|
424
|
+
* - store: 틀을 조용히 제거(z-index 는 이 모달이 이어서 받는다)
|
|
425
|
+
* 틀이 이미 닫혔으면(뒤로가기·ESC 등) null — 호출 측(replaceModalShell)이 먼저 걸러낸다.
|
|
426
|
+
* @returns {{ previousFocus: Element|null, remove: Function }|null}
|
|
427
|
+
*/
|
|
428
|
+
_takeOverShell(shellId, modalId) {
|
|
429
|
+
if (!modalStore.getModalConfig(shellId)) return null
|
|
430
|
+
const shellEl = document.getElementById(shellId)
|
|
431
|
+
const shellCtrl = shellEl && this.application.getControllerForElementAndIdentifier(shellEl, 'rmm-modal')
|
|
432
|
+
const previousFocus = shellCtrl ? shellCtrl.previousFocus : null
|
|
433
|
+
|
|
434
|
+
if (this.enableHistoryStackValue) {
|
|
435
|
+
historyStackManager.replaceHisData('modal', shellId, modalId, () => this.close('history'))
|
|
436
|
+
} else {
|
|
437
|
+
// 이 모달은 history 를 쓰지 않는다 — 틀의 칸은 일반 닫기처럼 되돌린다
|
|
438
|
+
historyStackManager.removeHisData('modal', shellId)
|
|
439
|
+
}
|
|
440
|
+
modalStore.unregisterCloseCallback(shellId)
|
|
441
|
+
modalStore.closeModal(shellId)
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
previousFocus,
|
|
445
|
+
remove: () => {
|
|
446
|
+
const overlayEl = document.getElementById(`${shellId}-overlay`)
|
|
447
|
+
if (overlayEl) overlayEl.remove()
|
|
448
|
+
if (shellEl) shellEl.remove()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
395
451
|
}
|
|
396
452
|
|
|
397
453
|
/**
|
|
@@ -199,6 +199,33 @@ function silentRemoveHisData(type, id) {
|
|
|
199
199
|
return true;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Hand an existing history entry over to another target without touching browser history (v1.0.61+).
|
|
204
|
+
* Used when a placeholder («shell») modal is swapped for the real modal: the pushState made when the
|
|
205
|
+
* shell opened stays as-is, only the entry's id and close function move to the real modal, so the
|
|
206
|
+
* back button closes the real modal and no orphan history state is left behind.
|
|
207
|
+
* @param {string} type - Target type ('modal', ...)
|
|
208
|
+
* @param {string} oldId - Current owner id
|
|
209
|
+
* @param {string} newId - New owner id
|
|
210
|
+
* @param {Function} close - New close function (optional)
|
|
211
|
+
* @returns {boolean} Whether the entry was found and handed over
|
|
212
|
+
*/
|
|
213
|
+
function replaceHisData(type, oldId, newId, close) {
|
|
214
|
+
if (typeof window === 'undefined') return false;
|
|
215
|
+
if (!isEnabledForDevice(type)) return false;
|
|
216
|
+
|
|
217
|
+
const hisData = loadHisData();
|
|
218
|
+
const item = hisData.find((entry) => entry.type === type && entry.id === oldId);
|
|
219
|
+
if (!item) return false;
|
|
220
|
+
|
|
221
|
+
if (item.closeKey) closeFunctions.delete(item.closeKey);
|
|
222
|
+
item.id = newId;
|
|
223
|
+
item.closeKey = generateUUID();
|
|
224
|
+
if (close) closeFunctions.set(item.closeKey, close);
|
|
225
|
+
saveHisData(hisData);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
202
229
|
/**
|
|
203
230
|
* Remove multiple items from history stack at once
|
|
204
231
|
* - Remove all specified items from stack
|
|
@@ -392,6 +419,7 @@ const historyStackManager = {
|
|
|
392
419
|
removeHisData,
|
|
393
420
|
removeMultipleHisData,
|
|
394
421
|
silentRemoveHisData,
|
|
422
|
+
replaceHisData,
|
|
395
423
|
popHisData,
|
|
396
424
|
moveToTop,
|
|
397
425
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export const VERSION = "1.0.
|
|
11
|
+
export const VERSION = "1.0.61"
|
|
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"
|
|
@@ -192,6 +192,134 @@ export function closeModal(modalId, source = 'programmatic') {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// ============================================
|
|
196
|
+
// Shell (placeholder) modal — v1.0.61+
|
|
197
|
+
// ============================================
|
|
198
|
+
// 서버 응답을 기다리는 동안 «모달 틀»을 먼저 띄우고, 실제 모달이 오면 그 자리를 제자리 교체한다.
|
|
199
|
+
// 틀 마크업은 레이아웃에 한 번 렌더한 rmm_modal_shell_template(<template>)을 복제한다 — gem 의
|
|
200
|
+
// 모달 파셜 그대로이므로 헤더·오버레이·크기 규칙이 실제 모달과 같다.
|
|
201
|
+
|
|
202
|
+
const SHELL_TEMPLATE_ID = 'rmm-modal-shell-template'
|
|
203
|
+
const SHELL_ID_TOKEN = '__RMM_SHELL__'
|
|
204
|
+
const SHELL_TITLE_TOKEN = '__RMM_SHELL_TITLE__'
|
|
205
|
+
|
|
206
|
+
function escapeHtml(value) {
|
|
207
|
+
return String(value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether shell modals are available (the layout rendered rmm_modal_shell_template)
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
export function modalShellAvailable() {
|
|
215
|
+
return !!document.getElementById(SHELL_TEMPLATE_ID)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Open a placeholder («shell») modal immediately while the real modal is being fetched.
|
|
220
|
+
* Pass the same size/height/persistentId as the real modal so the swap does not jump.
|
|
221
|
+
* @param {string} shellId - Unique id for the shell (e.g. `${modalId}-shell`)
|
|
222
|
+
* @param {Object} options
|
|
223
|
+
* @param {string} options.title - Header title while loading
|
|
224
|
+
* @param {string} options.size - 'fit'|'xss'|'xs'|'sm'|'md'|'lg'|'xl'|'full'
|
|
225
|
+
* @param {string} options.height - e.g. '90vh'
|
|
226
|
+
* @param {string} options.persistentId - Same persistent_id as the real modal (saved size is shared)
|
|
227
|
+
* @param {boolean} options.mobileDefaultMaximized
|
|
228
|
+
* @param {boolean} options.closeOnOverlay
|
|
229
|
+
* @returns {string|null} shellId, or null when the template is missing (caller falls back)
|
|
230
|
+
*/
|
|
231
|
+
export function openModalShell(shellId, options = {}) {
|
|
232
|
+
const template = document.getElementById(SHELL_TEMPLATE_ID)
|
|
233
|
+
if (!template || !shellId) return null
|
|
234
|
+
// 같은 id 의 틀이 아직 열려 있으면(같은 대상 재클릭) 그대로 쓴다. 닫히는 중·잔재면 즉시 걷어낸다 —
|
|
235
|
+
// 닫힘 애니메이션을 기다리는 사이 새 틀을 같은 id 로 넣으면 옛 틀의 정리가 새 틀을 지울 수 있다.
|
|
236
|
+
if (isModalShellOpen(shellId)) return shellId
|
|
237
|
+
removeShellDom(shellId)
|
|
238
|
+
|
|
239
|
+
const html = template.innerHTML
|
|
240
|
+
.split(SHELL_ID_TOKEN).join(shellId)
|
|
241
|
+
.split(SHELL_TITLE_TOKEN).join(escapeHtml(options.title || ''))
|
|
242
|
+
const temp = document.createElement('div')
|
|
243
|
+
temp.innerHTML = html
|
|
244
|
+
const modal = temp.querySelector(`#${CSS.escape(shellId)}`)
|
|
245
|
+
if (!modal) return null
|
|
246
|
+
|
|
247
|
+
const set = (name, value) => { if (value !== undefined && value !== null) modal.setAttribute(`data-rmm-modal-${name}-value`, String(value)) }
|
|
248
|
+
set('size', options.size)
|
|
249
|
+
set('height', options.height)
|
|
250
|
+
set('persistent-id', options.persistentId)
|
|
251
|
+
set('mobile-default-maximized', options.mobileDefaultMaximized)
|
|
252
|
+
set('close-on-overlay', options.closeOnOverlay)
|
|
253
|
+
const overlay = temp.querySelector(`#${CSS.escape(shellId)}-overlay`)
|
|
254
|
+
if (overlay && options.closeOnOverlay !== undefined) {
|
|
255
|
+
overlay.setAttribute('data-rmm-overlay-close-on-click-value', String(options.closeOnOverlay))
|
|
256
|
+
}
|
|
257
|
+
// connect() 가 openValue=true 를 보고 곧바로 연다
|
|
258
|
+
modal.setAttribute('data-rmm-modal-open-value', 'true')
|
|
259
|
+
modal.classList.add('rmm-shell')
|
|
260
|
+
|
|
261
|
+
const container = document.getElementById('child-modals-container') || document.body
|
|
262
|
+
while (temp.firstChild) container.appendChild(temp.firstChild)
|
|
263
|
+
return shellId
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Whether the shell is still open (not closed by back button / ESC / overlay while loading)
|
|
268
|
+
* @param {string} shellId
|
|
269
|
+
* @returns {boolean}
|
|
270
|
+
*/
|
|
271
|
+
export function isModalShellOpen(shellId) {
|
|
272
|
+
return !!modalStore.getModalConfig(shellId)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Swap the shell for the real modal in place. The real modal must already be in the DOM (closed).
|
|
277
|
+
* The real modal inherits the shell's history entry, focus-return target and stacking slot, and
|
|
278
|
+
* appears without the opening animation.
|
|
279
|
+
* @param {string} shellId
|
|
280
|
+
* @param {string} modalId - The real modal's id
|
|
281
|
+
* @returns {boolean} false when the shell was already closed — the caller should discard the real modal
|
|
282
|
+
*/
|
|
283
|
+
export function replaceModalShell(shellId, modalId) {
|
|
284
|
+
const modalElement = document.getElementById(modalId)
|
|
285
|
+
if (!modalElement) return false
|
|
286
|
+
if (!isModalShellOpen(shellId)) {
|
|
287
|
+
discardModalShell(shellId)
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
modalElement.dataset.rmmAdoptShell = shellId
|
|
291
|
+
openModal(modalId)
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Close (if open) and remove a shell — e.g. when fetching the real modal failed.
|
|
297
|
+
* @param {string} shellId
|
|
298
|
+
*/
|
|
299
|
+
function removeShellDom(shellId) {
|
|
300
|
+
const overlay = document.getElementById(`${shellId}-overlay`)
|
|
301
|
+
const shell = document.getElementById(shellId)
|
|
302
|
+
if (overlay) overlay.remove()
|
|
303
|
+
if (shell) shell.remove()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function discardModalShell(shellId) {
|
|
307
|
+
const shell = document.getElementById(shellId)
|
|
308
|
+
if (!shell) return
|
|
309
|
+
// 정리 대상은 지금 시점의 요소로 고정(나중에 같은 id 로 들어온 새 틀을 건드리지 않게)
|
|
310
|
+
const overlay = document.getElementById(`${shellId}-overlay`)
|
|
311
|
+
const removeDom = () => {
|
|
312
|
+
if (overlay && overlay.parentNode) overlay.remove()
|
|
313
|
+
if (shell.parentNode) shell.remove()
|
|
314
|
+
}
|
|
315
|
+
if (isModalShellOpen(shellId)) {
|
|
316
|
+
shell.addEventListener('rmm-modal:closed', removeDom, { once: true })
|
|
317
|
+
closeModal(shellId)
|
|
318
|
+
} else {
|
|
319
|
+
removeDom()
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
195
323
|
/**
|
|
196
324
|
* Close the topmost modal
|
|
197
325
|
* @returns {string|null} The closed modal's ID or null if no modal was open
|