1/** 2 * Theme toggle: the light/dark control in the header bar and the mobile sheet. 3 * 4 * The scheme is already correct before this file runs â the default comes from 5 * the `prefers-color-scheme` media query in the stylesheet, and a stored choice 6 * was stamped onto the html element by the inline script in inc/dark-mode.php, 7 * ahead of the first paint. What is left for this file is the part that cannot 8 * be expressed in CSS: the click, the accessible state, and the browser chrome 9 * colour. 10 * 11 * WHY A MATCHING CHOICE IS DELETED RATHER THAN STORED. Choosing the scheme the 12 * system is already on removes the override instead of writing it. Otherwise 13 * every reader who once pressed the button would be pinned to that scheme for 14 * good, and a phone that turns dark at sunset would stop being followed â the 15 * button would silently have become "never change again". Deleting it puts the 16 * page back on the system, which is the state it started in. 17 */ 18( function () { 19 'use strict'; 20 21 var KEY = window.jmThemeStorageKey || 'jm-theme'; 22 var root = document.documentElement; 23 var query = window.matchMedia ? window.matchMedia( '( prefers-color-scheme: dark )' ) : null; 24 var buttons = [].slice.call( document.querySelectorAll( '[data-jm-theme-toggle]' ) ); 25 26 if ( ! buttons.length ) { 27 return; 28 } 29 30 function systemScheme() { 31 return query && query.matches ? 'dark' : 'light'; 32 } 33 34 function storedScheme() { 35 try { 36 var value = window.localStorage.getItem( KEY ); 37 return 'dark' === value || 'light' === value ? value : null; 38 } catch ( error ) { 39 // Safari in private mode throws on access rather than returning null. 40 return null; 41 } 42 } 43 44 function activeScheme() { 45 return storedScheme() || systemScheme(); 46 } 47 48 /** 49 * Keeps the Android status bar and the desktop title bar in step. 50 * 51 * Both tags are printed by inc/pwa-manifest.php with a `media` that already 52 * matches the system. Overwriting it is how an explicit choice reaches the 53 * browser chrome, which has no other way to hear about it. 54 */ 55 function paintChrome( isDark ) { 56 var metas = document.querySelectorAll( 'meta[name="theme-color"][data-jm-theme-color]' ); 57 58 [].forEach.call( metas, function ( meta ) { 59 var wantsDark = 'dark' === meta.getAttribute( 'data-jm-theme-color' ); 60 meta.setAttribute( 'media', wantsDark === isDark ? 'all' : 'not all' ); 61 } ); 62 } 63 64 function sync() { 65 var isDark = 'dark' === activeScheme(); 66 67 buttons.forEach( function ( button ) { 68 button.setAttribute( 'aria-pressed', isDark ? 'true' : 'false' ); 69 } ); 70 71 paintChrome( isDark ); 72 } 73 74 /** 75 * The theme lives on tokens that a good many rules animate with the shared 76 * `--baseTransition`, so a switch would otherwise crawl across the page one 77 * component at a time. The class suppresses transitions for one frame; the 78 * double rAF is what guarantees the new colours have been painted before it 79 * comes off again. 80 */ 81 function withoutTransitions( change ) { 82 root.classList.add( 'jm-theme-switching' ); 83 change(); 84 window.requestAnimationFrame( function () { 85 window.requestAnimationFrame( function () { 86 root.classList.remove( 'jm-theme-switching' ); 87 } ); 88 } ); 89 } 90 91 function apply( scheme ) { 92 withoutTransitions( function () { 93 if ( scheme === systemScheme() ) { 94 try {
95 window.localStorage.removeItem( KEY ); 96 } catch ( error ) {} 97 98 root.removeAttribute( 'data-theme' ); 99 } else { 100 try { 101 window.localStorage.setItem( KEY, scheme ); 102 } catch ( error ) {} 103 104 root.setAttribute( 'data-theme', scheme ); 105 } 106 } ); 107 108 sync(); 109 } 110 111 buttons.forEach( function ( button ) { 112 button.addEventListener( 'click', function () { 113 apply( 'dark' === activeScheme() ? 'light' : 'dark' ); 114 } ); 115 } ); 116 117 // The system changed under an unpinned page: CSS has already followed, the 118 // button state and the chrome colour have not. 119 if ( query ) { 120 if ( query.addEventListener ) { 121 query.addEventListener( 'change', sync ); 122 } else if ( query.addListener ) { 123 query.addListener( sync ); 124 } 125 } 126 127 // Another tab switched: follow it, so the reader does not have two of our 128 // pages open in two different schemes. 129 window.addEventListener( 'storage', function ( event ) { 130 if ( event.key !== KEY ) { 131 return; 132 } 133 134 withoutTransitions( function () { 135 var scheme = storedScheme(); 136 137 if ( scheme ) { 138 root.setAttribute( 'data-theme', scheme ); 139 } else { 140 root.removeAttribute( 'data-theme' ); 141 } 142 } ); 143 144 sync(); 145 } ); 146 147 sync(); 148}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.