-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodal-stack.js
More file actions
152 lines (139 loc) · 6.12 KB
/
Copy pathmodal-stack.js
File metadata and controls
152 lines (139 loc) · 6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/* ============================================================
modal-stack.js (2026-05-13)
------------------------------------------------------------
Site-wide modal back-stack. Whenever a modal opens, it records
the modal that was open *before* it. When a modal closes, the
stack is popped and that previous modal is re-opened — so the
user always returns to whatever they were on, not to the bare
index.
The stack lives in sessionStorage so it survives the URL
navigations involved in the article-from-supplement flow
(clicking "Further Reading" target=_top navigates the parent
window, which would otherwise blow away any in-memory state).
Supported modal types — extend as new modals get plumbed in:
{ type: 'supplement', slug: '...' } → window.SSModal.open(slug)
{ type: 'article', id: N } → window.goArticle(id)
API:
window.SSModalStack.push(entry) record an entry to come back to
window.SSModalStack.pop() remove and return the latest
window.SSModalStack.peek() return latest without removing
window.SSModalStack.snapshot() detect what modal is currently
open in the DOM and return an
entry shape for it (or null)
window.SSModalStack.reopen(entry) call the appropriate opener
for an entry; returns true on
success, false on miss
window.SSModalStack.clear() empty the stack
============================================================ */
(function(){
if (window.SSModalStack) return;
var STACK_KEY = 'ss-modal-stack';
var MAX_DEPTH = 10;
/* (2026-06-06) Start every full page load with an EMPTY stack.
The stack originally lived in sessionStorage so it could survive the
supplement→article roundtrip (index.html?supplement=…#article-N).
That roundtrip is gone — supplement-detail.js links straight to /a/
pages, and the ?supplement= URL param alone restores the supplement
modal (closeArtModal's safety net + supplement-modal.js auto-open).
What sessionStorage persistence actually delivered in practice was
STALE entries: closing any modal several journeys later popped an
old entry and surprise-reopened (or worse, navigated via goArticle) —
the "random modal flashes open/closed" bug. Same-page stacking
(article modal → supplement modal → close → back to article) pushes
after load and is unaffected. */
try { sessionStorage.removeItem(STACK_KEY); } catch(_){}
function readStack(){
try { var s = sessionStorage.getItem(STACK_KEY); return s ? JSON.parse(s) : []; }
catch(_){ return []; }
}
function writeStack(arr){
try { sessionStorage.setItem(STACK_KEY, JSON.stringify(arr)); } catch(_){}
}
function entryKey(e){
if (!e || !e.type) return '';
return e.type + ':' + (e.slug || e.id || e.name || '');
}
function push(entry){
if (!entry || !entry.type) return;
var s = readStack();
/* Don't push a duplicate of the current top — happens if the same
URL navigation fires open twice (auto-open + pushState handlers). */
if (s.length && entryKey(s[s.length-1]) === entryKey(entry)) return;
s.push(entry);
if (s.length > MAX_DEPTH) s = s.slice(-MAX_DEPTH);
writeStack(s);
}
function pop(){
var s = readStack();
if (!s.length) return null;
var top = s.pop();
writeStack(s);
return top;
}
function peek(){
var s = readStack();
return s.length ? s[s.length-1] : null;
}
function clear(){ writeStack([]); }
/* Detect what modal is currently open by inspecting the DOM.
Used to decide what to push before opening a new modal. */
function snapshot(){
/* Article modal (app.js) */
var art = document.getElementById('art-modal');
if (art && art.classList.contains('open')) {
var id = window._currentArticleId || null;
/* Fall back to URL hash if global isn't set yet. */
if (!id) {
var m = (location.hash || '').match(/^#article-(\d+)$/);
if (m) id = parseInt(m[1], 10);
}
if (id) return { type: 'article', id: id };
}
/* Supplement modal (supplement-modal.js iframe) */
var ssm = document.querySelector('.ssm.open');
if (ssm) {
var sp = new URLSearchParams(location.search);
var slug = sp.get('supplement');
/* History state is the second source of truth — supplement-modal.js
stores { ssm: slug } via pushState. */
if (!slug && history.state && history.state.ssm) slug = history.state.ssm;
if (slug) return { type: 'supplement', slug: slug };
}
/* Legacy supp-modal in app.js (different element from .ssm). */
var legacy = document.getElementById('supp-modal');
if (legacy && legacy.classList.contains('open')) {
var nm = window._currentSuppName || null;
if (nm) return { type: 'supp-legacy', name: nm };
}
return null;
}
/* Re-open a modal from a stack entry. Returns true on success. */
function reopen(entry){
if (!entry || !entry.type) return false;
if (entry.type === 'supplement' && window.SSModal && typeof window.SSModal.open === 'function') {
window.SSModal.open(entry.slug);
return true;
}
if (entry.type === 'article' && typeof window.goArticle === 'function') {
var div = document.getElementById('article-' + entry.id);
if (!div) return false;
/* Slimmed metadata stub (hidden .ar-readmore redirect only): goArticle
would NAVIGATE to the /a/ page rather than reopen a modal. Closing a
modal must never trigger a page navigation — skip. Only the dozen
page-less articles with full inline bodies reopen as a modal. */
if (div.children.length <= 1 && div.querySelector('a.ar-readmore')) return false;
window.goArticle(entry.id);
return true;
}
/* supp-legacy is index-only and uses showSuppModal(name); skip for now. */
return false;
}
window.SSModalStack = {
push: push,
pop: pop,
peek: peek,
snapshot: snapshot,
reopen: reopen,
clear: clear
};
})();