PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / assets / js / settings-hash-state.js
wp-2fa / includes / assets / js Last commit date
customize-styles.js 1 day ago exclude-self-prompt.js 1 day ago intlTelInput-vanilla.js 1 day ago policies-hash-state.js 1 day ago save-policies-new.js 1 day ago save-settings-new.js 1 day ago settings-design-logic.js 1 day ago settings-hash-state.js 1 day ago test-email-new.js 1 day ago wizard-new.js 1 day ago
settings-hash-state.js
276 lines
1 /**
2 * Settings Hash State — preserves navigation state in the URL hash so users
3 * can refresh / share a direct link and land on the same section, sub-tab,
4 * and open provider accordion(s).
5 *
6 * Hash format: #section=email-settings&tab=templates&provider=0,2
7 *
8 * - section : the settings-page ID without the "-wrap" suffix
9 * - tab : the radio-button ID without the "tab-" prefix (sub-tabs)
10 * - provider : comma-separated indices of open .provider-item elements
11 *
12 * @package wp-2fa
13 * @since 2.8.0
14 */
15
16 /* ── early hide: inject styles immediately (before paint) when hash is
17 * present so the page doesn't flash its default state while JS restores
18 * the correct view ─────────────────────────────────────────────────── */
19 ( function () {
20 var hash = window.location.hash.replace( /^#/, '' );
21 if ( ! hash || hash.indexOf( 'section=' ) === -1 ) {
22 return;
23 }
24 var s = document.createElement( 'style' );
25 s.id = 'wp2fa-hash-loading';
26 s.textContent =
27 '.wp-2fa-settings-new .main-settings-new { opacity: 0; }' +
28 '.wp2fa-loading { display: flex !important; justify-content: center;' +
29 ' align-items: center; padding: 60px 0; }' +
30 '.wp2fa-loading::after { content: ""; width: 36px; height: 36px;' +
31 ' border: 3px solid #ddd; border-top-color: #0073aa;' +
32 ' border-radius: 50%; animation: wp2fa-spin .6s linear infinite; }' +
33 '@keyframes wp2fa-spin { to { transform: rotate(360deg); } }';
34 document.head.appendChild( s );
35 } )();
36
37 ( function () {
38 'use strict';
39
40 /* ── helpers ──────────────────────────────────────────── */
41
42 function parseHash() {
43 var raw = window.location.hash.replace( /^#/, '' );
44 if ( ! raw ) {
45 return {};
46 }
47 var state = {};
48 raw.split( '&' ).forEach( function ( pair ) {
49 var parts = pair.split( '=' );
50 if ( parts.length === 2 ) {
51 state[ decodeURIComponent( parts[ 0 ] ) ] = decodeURIComponent( parts[ 1 ] );
52 }
53 } );
54 return state;
55 }
56
57 function writeHash( state ) {
58 var parts = [];
59 Object.keys( state ).forEach( function ( key ) {
60 if ( state[ key ] !== undefined && state[ key ] !== null && state[ key ] !== '' ) {
61 parts.push( encodeURIComponent( key ) + '=' + encodeURIComponent( state[ key ] ) );
62 }
63 } );
64 var hash = parts.length ? '#' + parts.join( '&' ) : window.location.pathname + window.location.search;
65 history.replaceState( null, '', hash );
66 }
67
68 /* ── loading overlay ─────────────────────────────────── */
69
70 function showLoading() {
71 var wrapper = document.querySelector( '.wp-2fa-settings-new' );
72 if ( ! wrapper ) {
73 return;
74 }
75 var el = document.createElement( 'div' );
76 el.className = 'wp2fa-loading';
77 wrapper.insertBefore( el, wrapper.querySelector( '.main-settings-new' ) );
78 }
79
80 function hideLoading() {
81 var loader = document.querySelector( '.wp2fa-loading' );
82 if ( loader ) {
83 loader.remove();
84 }
85 var style = document.getElementById( 'wp2fa-hash-loading' );
86 if ( style ) {
87 style.remove();
88 }
89 var main = document.querySelector( '.wp-2fa-settings-new .main-settings-new' );
90 if ( main ) {
91 main.style.opacity = '';
92 }
93 }
94
95 /* ── navigate back to settings overview ───────────────── */
96
97 function navigateBack() {
98 var container = document.querySelector( '.general-settings-main-wrapper' );
99 if ( ! container ) {
100 return;
101 }
102 // Show the main settings list.
103 container.style.display = '';
104
105 // Hide every settings-page panel.
106 document.querySelectorAll( '.settings-page' ).forEach( function ( page ) {
107 page.style.display = 'none';
108 } );
109
110 // Close any open provider accordions.
111 document.querySelectorAll( '.provider-item.open' ).forEach( function ( item ) {
112 item.classList.remove( 'open' );
113 } );
114
115 // Clear the hash.
116 writeHash( {} );
117 }
118
119 /* ── read current DOM state ──────────────────────────── */
120
121 function readDOMState() {
122 var state = {};
123
124 // 1. Which settings-page is visible?
125 var pages = document.querySelectorAll( '.settings-page' );
126 pages.forEach( function ( page ) {
127 if ( page.style.display === 'block' && page.id ) {
128 state.section = page.id.replace( /-wrap$/, '' );
129 }
130 } );
131
132 // 2. Which sub-tab radio is checked inside the visible section?
133 if ( state.section ) {
134 var wrap = document.getElementById( state.section + '-wrap' );
135 if ( wrap ) {
136 var checked = wrap.querySelector( '.tab-radio:checked' );
137 if ( checked && checked.id ) {
138 state.tab = checked.id.replace( /^tab-/, '' );
139 }
140 }
141 }
142
143 // 3. Which provider-items are open (globally — they only appear in one section)?
144 var allProviders = document.querySelectorAll( '.provider-item' );
145 var openIndices = [];
146 allProviders.forEach( function ( item, idx ) {
147 if ( item.classList.contains( 'open' ) ) {
148 openIndices.push( idx );
149 }
150 } );
151 if ( openIndices.length ) {
152 state.provider = openIndices.join( ',' );
153 }
154
155 return state;
156 }
157
158 /* ── restore state from hash ─────────────────────────── */
159
160 function restoreState() {
161 var state = parseHash();
162 var container = document.querySelector( '.general-settings-main-wrapper' );
163
164 if ( ! Object.keys( state ).length || ! container ) {
165 hideLoading();
166 return;
167 }
168
169 // 1. Section
170 if ( state.section ) {
171 var target = document.getElementById( state.section + '-wrap' );
172 if ( target ) {
173 // Hide every other settings-page first.
174 document.querySelectorAll( '.settings-page' ).forEach( function ( p ) {
175 p.style.display = 'none';
176 } );
177 target.style.display = 'block';
178 container.style.display = 'none';
179 }
180 }
181
182 // 2. Sub-tab — programmatically check the correct radio.
183 if ( state.tab ) {
184 var radio = document.getElementById( 'tab-' + state.tab );
185 if ( radio ) {
186 radio.checked = true;
187 }
188 }
189
190 // 3. Provider accordions.
191 if ( state.provider ) {
192 var indices = state.provider.split( ',' ).map( Number );
193 var allProviders = document.querySelectorAll( '.provider-item' );
194 indices.forEach( function ( idx ) {
195 if ( allProviders[ idx ] ) {
196 allProviders[ idx ].classList.add( 'open' );
197 }
198 } );
199 }
200
201 hideLoading();
202 }
203
204 /* ── update hash from DOM ────────────────────────────── */
205
206 function syncHash() {
207 writeHash( readDOMState() );
208 }
209
210 /* ── attach listeners ────────────────────────────────── */
211
212 function installListeners() {
213 var container = document.querySelector( '.general-settings-main-wrapper' );
214 if ( ! container ) {
215 return;
216 }
217
218 // Sidebar list-item clicks → update hash after the inline script runs.
219 container.querySelectorAll( '.settings-group li' ).forEach( function ( li ) {
220 li.addEventListener( 'click', function () {
221 setTimeout( syncHash, 0 );
222 } );
223 } );
224
225 // Back-button clicks → navigate back to the main overview.
226 // The inline script in general-settings.php only binds these when the
227 // sidebar item is physically clicked — so when we restore from a hash,
228 // those listeners are never created. We register our own on every
229 // .back-click element and handle the full navigation ourselves.
230 document.querySelectorAll( '.back-click' ).forEach( function ( btn ) {
231 btn.addEventListener( 'click', function ( e ) {
232 e.preventDefault();
233 navigateBack();
234 } );
235 } );
236
237 // Sub-tab radio changes.
238 document.querySelectorAll( '.tab-radio' ).forEach( function ( radio ) {
239 radio.addEventListener( 'change', function () {
240 syncHash();
241 } );
242 } );
243
244 // Sub-tab label clicks (for radios driven by <label for="">).
245 document.querySelectorAll( '.tab-label' ).forEach( function ( label ) {
246 label.addEventListener( 'click', function () {
247 setTimeout( syncHash, 0 );
248 } );
249 } );
250
251 // Provider accordion toggles (event delegation).
252 document.addEventListener( 'click', function ( e ) {
253 if ( e.target.closest( '.provider-summary' ) ) {
254 setTimeout( syncHash, 0 );
255 }
256 } );
257 }
258
259 /* ── bootstrap ───────────────────────────────────────── */
260
261 document.addEventListener( 'DOMContentLoaded', function () {
262 var needsRestore = window.location.hash.indexOf( 'section=' ) !== -1;
263
264 if ( needsRestore ) {
265 showLoading();
266 }
267
268 // Run after the inline scripts in general-settings.php have bound
269 // their own DOMContentLoaded listeners.
270 setTimeout( function () {
271 restoreState();
272 installListeners();
273 }, 50 );
274 } );
275 } )();
276