| // 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(); |
| }); |
| } |
| } |
| |
| initialThemeSetup(); |
| |
| const onPageLoad = () => { |
| registerHeaderListeners(); |
| setDownloadLinks(); |
| setThemeButtons(); |
| setVersionSpans(); |
| registerPortToggles(); |
| registerCookieNotice(); |
| }; |
| |
| // 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); |
| } |
| })(); |