| // Copyright 2019 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| /** | |
| * A bit of navigation related code for handling dismissible elements. | |
| */ | |
| window.initFuncs = []; | |
| (() => { | |
| 'use strict'; | |
| function registerHeaderListeners() { | |
| // Desktop menu hover state | |
| const menuItemHovers = document.querySelectorAll('.js-desktop-menu-hover'); | |
| menuItemHovers.forEach(menuItemHover => { | |
| // when user clicks on the dropdown menu item on desktop or mobile, | |
| // force the menu to stay open until the user clicks off of it. | |
| menuItemHover.addEventListener('mouseenter', e => { | |
| const forced = document.querySelector('.forced-open'); | |
| if (forced && forced !== menuItemHover) { | |
| forced.blur(); | |
| forced.classList.remove('forced-open'); | |
| } | |
| // prevents menus that have been tabbed into from staying open | |
| // when you hover over another menu | |
| e.target.classList.remove('forced-closed'); | |
| e.target.classList.add('forced-open'); | |
| }); | |
| const toggleForcedOpen = e => { | |
| const isForced = e.target.classList.contains('forced-open'); | |
| const target = e.currentTarget; | |
| if (isForced) { | |
| target.removeEventListener('blur', e => | |
| target.classList.remove('forced-open') | |
| ); | |
| target.classList.remove('forced-open'); | |
| target.classList.add('forced-closed'); | |
| target.blur(); | |
| target.parentNode.addEventListener('mouseout', () => { | |
| target.classList.remove('forced-closed'); | |
| }); | |
| } else { | |
| target.classList.remove('forced-closed'); | |
| target.classList.add('forced-open'); | |
| target.parentNode.removeEventListener('mouseout', () => { | |
| target.classList.remove('forced-closed'); | |
| }); | |
| } | |
| e.target.focus(); | |
| }; | |
| menuItemHover.addEventListener('click', toggleForcedOpen); | |
| menuItemHover.addEventListener('focus', e => { | |
| e.target.classList.add('forced-closed'); | |
| e.target.classList.remove('forced-open'); | |
| }); | |
| // ensure focus is removed when esc is pressed | |
| const focusOutOnEsc = e => { | |
| if (e.key === 'Escape') { | |
| const textarea = document.getElementById('code'); | |
| if (e.target == textarea) { | |
| e.preventDefault(); | |
| textarea.blur(); | |
| } | |
| else { | |
| const forcedOpenItem = document.querySelector('.forced-open'); | |
| const target = e.currentTarget; | |
| if (forcedOpenItem) { | |
| forcedOpenItem.classList.remove('forced-open'); | |
| forcedOpenItem.blur(); | |
| forcedOpenItem.classList.add('forced-closed'); | |
| e.target.focus(); | |
| } | |
| } | |
| } | |
| }; | |
| document.addEventListener('keydown', focusOutOnEsc); | |
| }); | |
| // Mobile menu subnav menus | |
| const header = document.querySelector('.js-header'); | |
| const headerbuttons = document.querySelectorAll('.js-headerMenuButton'); | |
| headerbuttons.forEach(button => { | |
| button.addEventListener('click', e => { | |
| e.preventDefault(); | |
| const isActive = header.classList.contains('is-active'); | |
| if (isActive) { | |
| handleNavigationDrawerInactive(header); | |
| } else { | |
| handleNavigationDrawerActive(header); | |
| } | |
| button.setAttribute('aria-expanded', isActive); | |
| }); | |
| }); | |
| const scrim = document.querySelector('.js-scrim'); | |
| scrim.addEventListener('click', e => { | |
| e.preventDefault(); | |
| // find any active submenus and close them | |
| const activeSubnavs = document.querySelectorAll( | |
| '.NavigationDrawer-submenuItem.is-active' | |
| ); | |
| activeSubnavs.forEach(subnav => handleNavigationDrawerInactive(subnav)); | |
| handleNavigationDrawerInactive(header); | |
| headerbuttons.forEach(button => { | |
| button.setAttribute( | |
| 'aria-expanded', | |
| header.classList.contains('is-active') | |
| ); | |
| }); | |
| }); | |
| const getNavigationDrawerMenuItems = navigationDrawer => [ | |
| navigationDrawer.querySelector('.NavigationDrawer-header > a'), | |
| ...navigationDrawer.querySelectorAll( | |
| ':scope > .NavigationDrawer-nav > .NavigationDrawer-list > .NavigationDrawer-listItem > a, :scope > .NavigationDrawer-nav > .NavigationDrawer-list > .NavigationDrawer-listItem > .Header-socialIcons > a' | |
| ), | |
| ]; | |
| const getNavigationDrawerIsSubnav = navigationDrawer => | |
| navigationDrawer.classList.contains('NavigationDrawer-submenuItem'); | |
| const handleNavigationDrawerInactive = navigationDrawer => { | |
| const menuItems = getNavigationDrawerMenuItems(navigationDrawer); | |
| navigationDrawer.classList.remove('is-active'); | |
| const parentMenuItem = navigationDrawer | |
| .closest('.NavigationDrawer-listItem') | |
| ?.querySelector(':scope > a'); | |
| parentMenuItem?.focus(); | |
| menuItems.forEach(item => item.setAttribute('tabindex', '-1')); | |
| menuItems[0].removeEventListener( | |
| 'keydown', | |
| handleMenuItemTabLeft.bind(navigationDrawer) | |
| ); | |
| menuItems[menuItems.length - 1].removeEventListener( | |
| 'keydown', | |
| handleMenuItemTabRight.bind(navigationDrawer) | |
| ); | |
| if (navigationDrawer === header) { | |
| headerbuttons[0]?.focus(); | |
| } | |
| }; | |
| const handleNavigationDrawerActive = navigationDrawer => { | |
| const menuItems = getNavigationDrawerMenuItems(navigationDrawer); | |
| navigationDrawer.classList.add('is-active'); | |
| menuItems.forEach(item => item.setAttribute('tabindex', '0')); | |
| menuItems[0].focus(); | |
| menuItems[0].addEventListener( | |
| 'keydown', | |
| handleMenuItemTabLeft.bind(this, navigationDrawer) | |
| ); | |
| menuItems[menuItems.length - 1].addEventListener( | |
| 'keydown', | |
| handleMenuItemTabRight.bind(this, navigationDrawer) | |
| ); | |
| }; | |
| const handleMenuItemTabLeft = (navigationDrawer, e) => { | |
| if (e.key === 'Tab' && e.shiftKey) { | |
| e.preventDefault(); | |
| handleNavigationDrawerInactive(navigationDrawer); | |
| } | |
| }; | |
| const handleMenuItemTabRight = (navigationDrawer, e) => { | |
| if (e.key === 'Tab' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleNavigationDrawerInactive(navigationDrawer); | |
| } | |
| }; | |
| const prepMobileNavigationDrawer = navigationDrawer => { | |
| const isSubnav = getNavigationDrawerIsSubnav(navigationDrawer); | |
| const menuItems = getNavigationDrawerMenuItems(navigationDrawer); | |
| navigationDrawer.addEventListener('keyup', e => { | |
| if (e.key === 'Escape') { | |
| handleNavigationDrawerInactive(navigationDrawer); | |
| } | |
| }); | |
| menuItems.forEach(item => { | |
| const parentLi = item.closest('li'); | |
| if ( | |
| parentLi && | |
| parentLi.classList.contains('js-mobile-subnav-trigger') | |
| ) { | |
| const submenu = parentLi.querySelector( | |
| '.NavigationDrawer-submenuItem' | |
| ); | |
| item.addEventListener('click', () => { | |
| handleNavigationDrawerActive(submenu); | |
| }); | |
| } | |
| }); | |
| if (isSubnav) { | |
| handleNavigationDrawerInactive(navigationDrawer); | |
| navigationDrawer | |
| .querySelector('.NavigationDrawer-header') | |
| .addEventListener('click', e => { | |
| e.preventDefault(); | |
| handleNavigationDrawerInactive(navigationDrawer); | |
| }); | |
| } | |
| }; | |
| document | |
| .querySelectorAll('.NavigationDrawer') | |
| .forEach(drawer => prepMobileNavigationDrawer(drawer)); | |
| handleNavigationDrawerInactive(header); | |
| } | |
| /** | |
| * Attempts to detect user's operating system and sets the download | |
| * links accordingly | |
| */ | |
| async function setDownloadLinks() { | |
| const versionElement = document.querySelector('.js-latestGoVersion'); | |
| if (versionElement) { | |
| const anchorTagWindows = document.querySelector('.js-downloadWin'); | |
| const anchorTagMac = document.querySelector('.js-downloadMac'); | |
| const anchorTagLinux = document.querySelector('.js-downloadLinux'); | |
| const version = await getLatestVersion(); | |
| const macDownloadUrl = `/dl/${version}.darwin-amd64.pkg`; | |
| const windowsDownloadUrl = `/dl/${version}.windows-amd64.msi`; | |
| const linuxDownloadUrl = `/dl/${version}.linux-amd64.tar.gz`; | |
| anchorTagWindows.href = windowsDownloadUrl; | |
| anchorTagMac.href = macDownloadUrl; | |
| anchorTagLinux.href = linuxDownloadUrl; | |
| /* | |
| * Note: we do not change .js-downloadBtn anymore | |
| * because it is impossible to tell reliably which architecture | |
| * the user's browser is running on. | |
| */ | |
| } | |
| } | |
| function registerPortToggles() { | |
| for (const el of document.querySelectorAll('.js-togglePorts')) { | |
| el.addEventListener('click', () => { | |
| el.setAttribute('aria-expanded', el.getAttribute('aria-expanded') === 'true' ? 'false' : 'true') | |
| }) | |
| } | |
| } | |
| /** | |
| * Retrieves list of Go versions & returns the latest | |
| */ | |
| async function getLatestVersion() { | |
| let version = 'go1.17'; // fallback version if fetch fails | |
| try { | |
| const versionData = await (await fetch('/dl/?mode=json')).json(); | |
| if (!versionData.length) { | |
| return version; | |
| } | |
| versionData.sort((v1, v2) => { | |
| return v2.version - v1.version; | |
| }); | |
| version = versionData[0].version; | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| return version; | |
| } | |
| /** | |
| * initialThemeSetup sets data-theme attribute based on preferred color | |
| */ | |
| function initialThemeSetup() { | |
| const themeCookie = document.cookie.match( | |
| /prefers-color-scheme=(light|dark|auto)/ | |
| ); | |
| const theme = themeCookie && themeCookie.length > 0 && themeCookie[1]; | |
| if (theme) { | |
| document.querySelector('html').setAttribute('data-theme', theme); | |
| } | |
| } | |
| /** | |
| * setThemeButtons sets click listeners for toggling theme buttons | |
| */ | |
| function setThemeButtons() { | |
| for (const el of document.querySelectorAll('.js-toggleTheme')) { | |
| el.addEventListener('click', () => { | |
| toggleTheme(); | |
| }); | |
| } | |
| } | |
| /** | |
| * setVersionSpan sets the latest version in any span that has this selector. | |
| */ | |
| async function setVersionSpans() { | |
| const spans = document.querySelectorAll('.GoVersionSpan'); | |
| if (!spans) return; | |
| const version = await getLatestVersion(); | |
| Array.from(spans).forEach(span => { | |
| span.textContent = `Download (${version.replace('go', '')})` | |
| }); | |
| } | |
| /** | |
| * toggleTheme switches the preferred color scheme between auto, light, and dark. | |
| */ | |
| function toggleTheme() { | |
| let nextTheme = 'dark'; | |
| const theme = document.documentElement.getAttribute('data-theme'); | |
| if (theme === 'dark') { | |
| nextTheme = 'light'; | |
| } else if (theme === 'light') { | |
| nextTheme = 'auto'; | |
| } | |
| let domain = ''; | |
| if (location.hostname === 'go.dev') { | |
| // Include subdomains to apply the setting to pkg.go.dev. | |
| domain = 'domain=.go.dev;'; | |
| } | |
| document.documentElement.setAttribute('data-theme', nextTheme); | |
| document.cookie = `prefers-color-scheme=${nextTheme};${domain}path=/;max-age=31536000;`; | |
| } | |
| function registerCookieNotice() { | |
| const themeCookie = document.cookie.match(/cookie-consent=true/); | |
| if (!themeCookie) { | |
| const notice = document.querySelector('.js-cookieNotice'); | |
| const button = notice.querySelector('button'); | |
| notice.classList.add('Cookie-notice--visible'); | |
| button.addEventListener('click', () => { | |
| let domain = ''; | |
| if (location.hostname === 'go.dev') { | |
| // Apply the cookie to *.go.dev. | |
| domain = 'domain=.go.dev;'; | |
| } | |
| document.cookie = `cookie-consent=true;${domain}path=/;max-age=31536000`; | |
| notice.remove(); | |
| }); | |
| } | |
| } | |
| // registerSurveyBanner shows the Go Developer Survey banner. On desktop, | |
| // clicking the minimize button or the survey link collapses the floating card | |
| // into a compact sticky pill ('minimized'). On mobile, a top banner is shown. | |
| // When the cookie notice is active, the desktop card sits above it. The state | |
| // is recorded in localStorage under the survey ID, so no cookie is involved. | |
| function registerSurveyBanner() { | |
| const banner = document.querySelector('.js-surveyBanner'); | |
| if (!banner) { | |
| return; | |
| } | |
| const key = banner.dataset.surveyId; | |
| if (!key) { | |
| banner.remove(); | |
| return; | |
| } | |
| const getState = () => { | |
| try { | |
| return localStorage.getItem(key); | |
| } catch (e) { | |
| return null; | |
| } | |
| }; | |
| const setState = (state) => { | |
| try { | |
| localStorage.setItem(key, state); | |
| } catch (e) { | |
| // Storage is unavailable; the banner will show again on the next page. | |
| } | |
| }; | |
| const cookieNotice = document.querySelector('.js-cookieNotice'); | |
| if (cookieNotice && !document.cookie.match(/cookie-consent=true/)) { | |
| banner.classList.add('SurveyBanner--aboveCookie'); | |
| cookieNotice.querySelector('button')?.addEventListener('click', () => { | |
| banner.classList.remove('SurveyBanner--aboveCookie'); | |
| }); | |
| } | |
| const minimize = () => { | |
| setState('minimized'); | |
| banner.classList.remove('SurveyBanner--visible'); | |
| banner.classList.add('SurveyBanner--minimized'); | |
| }; | |
| banner | |
| .querySelector('.js-surveyBannerLink') | |
| ?.addEventListener('click', minimize); | |
| banner | |
| .querySelector('.js-surveyBannerClose') | |
| ?.addEventListener('click', minimize); | |
| const state = getState(); | |
| banner.classList.toggle('SurveyBanner--minimized', state === 'minimized'); | |
| banner.classList.toggle('SurveyBanner--visible', state !== 'minimized'); | |
| } | |
| initialThemeSetup(); | |
| const onPageLoad = () => { | |
| registerHeaderListeners(); | |
| setDownloadLinks(); | |
| setThemeButtons(); | |
| setVersionSpans(); | |
| registerPortToggles(); | |
| registerCookieNotice(); | |
| registerSurveyBanner(); | |
| }; | |
| // DOM might be already loaded when we try to setup the callback, hence the check. | |
| if (document.readyState !== 'loading') { | |
| onPageLoad(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', onPageLoad); | |
| } | |
| })(); |