1- let prefersDark = window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ;
1+ // To prevent a flash of un-themed content, this script must be loaded in the
2+ // <head> and cannot be marked async, defer, or type=module.
23
3- function setTheme ( mode ) {
4- if ( mode !== 'light' && mode !== 'dark' && mode !== 'auto' ) {
5- console . error ( `Got invalid theme mode: ${ mode } . Resetting to auto.` ) ;
6- mode = 'auto' ;
4+ const themes = [ 'auto' , 'light' , 'dark' ] ;
5+ const defaultTheme = themes [ 0 ] ;
6+
7+ const cookieName = 'theme' ;
8+ const cookieMaxAgeMs = 365 * 24 * 60 * 60 * 1000 ; // 1 year
9+ const cookieDomain = getCookieDomain ( window . location . hostname , [
10+ // Share the cookie between all subdomains (code, docs, www, etc.) in
11+ // these base domains. More-specific bases must be listed first.
12+ 'preview.djangoproject.com' ,
13+ 'djangoproject.com' ,
14+ 'djangoproject.localhost' ,
15+ 'djangoproject.local' ,
16+ // Any other hostname (localhost, 127.0.0.1, pr-123.readthedocs.build, etc.)
17+ // will use a host-only cookie.
18+ ] ) ;
19+ const cookieSecure = window . location . protocol === 'https:' ;
20+ const cookieSameSite = 'Lax' ;
21+
22+ const prefersDarkMediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
23+ let prefersDark = prefersDarkMediaQuery . matches ;
24+
25+ function setTheme ( theme ) {
26+ if ( themes . indexOf ( theme ) < 0 ) {
27+ console . error ( `Invalid theme: '${ theme } '. Resetting to '${ defaultTheme } '.` ) ;
28+ theme = defaultTheme ;
729 }
8- document . documentElement . dataset . theme = mode ;
9- // trim host to get base domain name for set in cookie domain name for subdomain access
10- const arrHost = window . location . hostname . split ( '.' ) ;
11- const prefix = arrHost . shift ( ) ;
12- const host = arrHost . join ( '.' ) ;
13- setCookie ( 'theme' , mode , host ) ;
30+ document . documentElement . dataset . theme = theme ;
31+ setThemeCookie ( theme ) ;
1432}
1533
1634function cycleTheme ( ) {
17- let currentTheme = document . documentElement . dataset . theme ;
18- if ( currentTheme !== 'light' && currentTheme !== 'dark' && currentTheme !== 'auto' ) {
19- currentTheme = 'auto' ;
20- }
21-
22- if ( prefersDark ) {
23- // Auto (dark) -> Light -> Dark
24- if ( currentTheme === 'auto' ) {
25- setTheme ( 'light' ) ;
26- } else if ( currentTheme === 'light' ) {
27- setTheme ( 'dark' ) ;
28- } else {
29- setTheme ( 'auto' ) ;
30- }
31- } else {
32- // Auto (light) -> Dark -> Light
33- if ( currentTheme === 'auto' ) {
34- setTheme ( 'dark' ) ;
35- } else if ( currentTheme === 'dark' ) {
36- setTheme ( 'light' ) ;
37- } else {
38- setTheme ( 'auto' ) ;
39- }
40- }
35+ // If prefersDark, cycle Auto (dark) -> Light -> Dark;
36+ // otherwise, cycle Auto (light) -> Dark -> Light.
37+ const currentThemeIndex = Math . max (
38+ 0 ,
39+ themes . indexOf ( document . documentElement . dataset . theme ) ,
40+ ) ;
41+ const direction = prefersDark ? 1 : - 1 ;
42+ const newThemeIndex =
43+ ( currentThemeIndex + direction + themes . length ) % themes . length ;
44+ setTheme ( themes [ newThemeIndex ] ) ;
4145}
4246
4347function initTheme ( ) {
44- // set theme defined in localStorage if there is one, or fallback to auto mode
45- const currentTheme = getCookie ( 'theme' ) ;
46- currentTheme ? setTheme ( currentTheme ) : setTheme ( 'auto' ) ;
48+ // Set theme stored in cookie if there is one, or fallback to auto mode.
49+ const currentTheme = getThemeCookie ( ) || defaultTheme ;
50+ setTheme ( currentTheme ) ;
4751}
4852
49- function setupTheme ( ) {
50- // Attach event handlers for toggling themes
51- let buttons = document . getElementsByClassName ( 'theme-toggle' ) ;
52- for ( let i = 0 ; i < buttons . length ; i ++ ) {
53- buttons [ i ] . addEventListener ( 'click' , cycleTheme ) ;
53+ function setupThemeToggle ( ) {
54+ // Attach event handlers for theme toggle buttons.
55+ for ( const button of document . getElementsByClassName ( 'theme-toggle' ) ) {
56+ button . addEventListener ( 'click' , cycleTheme ) ;
5457 }
5558}
5659
57- function setCookie ( cname , cvalue , domain ) {
58- const d = new Date ( ) ;
59- d . setTime ( d . getTime ( ) + 365 * 24 * 60 * 60 * 1000 ) ; // 1 year
60- let expires = 'expires=' + d . toUTCString ( ) ;
61- // change the SameSite attribute if it's on development or production
62- const sameSiteAttribute =
63- domain === 'localhost'
64- ? 'SameSite=Lax;'
65- : `Domain=${ domain } ; SameSite=None; Secure;` ;
66- document . cookie = `${ cname } =${ cvalue } ; ${ sameSiteAttribute } ${ expires } ; path=/;` ;
60+ function setThemeCookie ( theme ) {
61+ const expires = new Date ( Date . now ( ) + cookieMaxAgeMs ) . toUTCString ( ) ;
62+ const attributes = [
63+ `${ cookieName } =${ encodeURIComponent ( theme ) } ` ,
64+ 'Path=/' ,
65+ `Expires=${ expires } ` ,
66+ `SameSite=${ cookieSameSite } ` ,
67+ cookieDomain ? `Domain=${ cookieDomain } ` : '' ,
68+ cookieSecure ? 'Secure' : '' ,
69+ ] ;
70+ document . cookie = attributes . filter ( Boolean ) . join ( '; ' ) ;
6771}
6872
69- function getCookie ( cname ) {
70- let name = cname + '=' ;
71- let decodedCookie = decodeURIComponent ( document . cookie ) ;
72- let ca = decodedCookie . split ( ';' ) ;
73- for ( let i = 0 ; i < ca . length ; i ++ ) {
74- let c = ca [ i ] ;
75- while ( c . charAt ( 0 ) === ' ' ) {
76- c = c . substring ( 1 ) ;
77- }
78- if ( c . indexOf ( name ) === 0 ) {
79- return c . substring ( name . length , c . length ) ;
73+ function getThemeCookie ( ) {
74+ // This must be synchronous to avoid a flash of un-themed content on load,
75+ // so cannot use the Cookie Store API.
76+ const cookie = document . cookie
77+ . split ( / ; \s * / g)
78+ . find ( ( cookie ) => cookie . startsWith ( `${ cookieName } =` ) ) ;
79+ return cookie ? decodeURIComponent ( cookie . slice ( cookieName . length + 1 ) ) : '' ;
80+ }
81+
82+ function getCookieDomain ( hostname , baseDomains ) {
83+ hostname = hostname . toLowerCase ( ) ;
84+ for ( const baseDomain of baseDomains ) {
85+ if ( hostname === baseDomain || hostname . endsWith ( `.${ baseDomain } ` ) ) {
86+ return baseDomain ;
8087 }
8188 }
8289 return '' ;
@@ -85,12 +92,9 @@ function getCookie(cname) {
8592initTheme ( ) ;
8693
8794document . addEventListener ( 'DOMContentLoaded' , function ( ) {
88- setupTheme ( ) ;
95+ setupThemeToggle ( ) ;
8996} ) ;
9097
91- // reset theme and release image if auto mode activated and os preferences have changed
92- window
93- . matchMedia ( '(prefers-color-scheme: dark)' )
94- . addEventListener ( 'change' , function ( e ) {
95- prefersDark = e . matches ;
96- } ) ;
98+ prefersDarkMediaQuery . addEventListener ( 'change' , ( e ) => {
99+ prefersDark = e . matches ;
100+ } ) ;
0 commit comments