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 |