diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.css b/jupyterlite_sphinx/jupyterlite_sphinx.css index b62dd75..41db7f9 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.css +++ b/jupyterlite_sphinx/jupyterlite_sphinx.css @@ -67,12 +67,3 @@ transform: rotate(1turn); } } - -/* we do not want the button to show on smaller screens (phones), as clicking - * can download a lot of data. 480px is a commonly used breakpoint to identify if a device is a smartphone. */ - -@media (max-width: 480px), (max-height: 480px) { - div.try_examples_button_container { - display: none; - } -} diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index 4ae8c96..aa537e4 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -149,55 +149,153 @@ var tryExamplesGlobalMinHeight = 0; */ var tryExamplesConfigLoaded = false; -window.loadTryExamplesConfig = async (configFilePath) => { - if (tryExamplesConfigLoaded) { - return; +// This function is used to check if the current device is a mobile device. +// We assume the authenticity of the user agent string is enough to +// determine that, and we also check the window size as a fallback. +window.isMobileDevice = () => { + const mobilePatterns = [ + /Android/i, + /webOS/i, + /iPhone/i, + /iPad/i, + /iPod/i, + /BlackBerry/i, + /IEMobile/i, + /Windows Phone/i, + /Opera Mini/i, + /SamsungBrowser/i, + /UC.*Browser|UCWEB/i, + /MiuiBrowser/i, + /Mobile/i, + /Tablet/i, + ]; + + const isMobileByUA = mobilePatterns.some((pattern) => + pattern.test(navigator.userAgent), + ); + const isMobileBySize = window.innerWidth <= 480 || window.innerHeight <= 480; + const isLikelyMobile = isMobileByUA || isMobileBySize; + + if (isLikelyMobile) { + console.log( + "Mobile device detected, disabling interactive example buttons to conserve bandwidth.", + ); } - try { - // Add a timestamp as query parameter to ensure a cached version of the - // file is not used. - const timestamp = new Date().getTime(); - const configFileUrl = `${configFilePath}?cb=${timestamp}`; - const currentPageUrl = window.location.pathname; - - const response = await fetch(configFileUrl); - if (!response.ok) { - if (response.status === 404) { - // Try examples ignore file is not present. - console.log("Optional try_examples config file not found."); - return; + + return isLikelyMobile; +}; + +// A config loader with request deduplication + permanent caching +const ConfigLoader = (() => { + let configLoadPromise = null; + + const loadConfig = async (configFilePath) => { + if (window.isMobileDevice()) { + const buttons = document.getElementsByClassName("try_examples_button"); + for (let i = 0; i < buttons.length; i++) { + buttons[i].classList.add("hidden"); } - throw new Error(`Error fetching ${configFilePath}`); + tryExamplesConfigLoaded = true; // mock it + return; } - const data = await response.json(); - if (!data) { + if (tryExamplesConfigLoaded) { return; } - // Set minimum iframe height based on value in config file - if (data.global_min_height) { - tryExamplesGlobalMinHeight = parseInt(data.global_min_height); + // Return the existing promise if the request is in progress, as we + // don't want to make multiple requests for the same file. This + // can happen if there are several try_examples directives on the + // same page. + if (configLoadPromise) { + return configLoadPromise; } - // Disable interactive examples if file matches one of the ignore patterns - // by hiding try_examples_buttons. - Patterns = data.ignore_patterns; - for (let pattern of Patterns) { - let regex = new RegExp(pattern); - if (regex.test(currentPageUrl)) { - var buttons = document.getElementsByClassName("try_examples_button"); - for (var i = 0; i < buttons.length; i++) { - buttons[i].classList.add("hidden"); + // Create and cache the promise for the config request + configLoadPromise = (async () => { + try { + // Add a timestamp as query parameter to ensure a cached version of the + // file is not used. + const timestamp = new Date().getTime(); + const configFileUrl = `${configFilePath}?cb=${timestamp}`; + const currentPageUrl = window.location.pathname; + + const response = await fetch(configFileUrl); + if (!response.ok) { + if (response.status === 404) { + console.log("Optional try_examples config file not found."); + return; + } + throw new Error(`Error fetching ${configFilePath}`); + } + + const data = await response.json(); + if (!data) { + return; + } + + // Set minimum iframe height based on value in config file + if (data.global_min_height) { + tryExamplesGlobalMinHeight = parseInt(data.global_min_height); + } + + // Disable interactive examples if file matches one of the ignore patterns + // by hiding try_examples_buttons. + Patterns = data.ignore_patterns; + for (let pattern of Patterns) { + let regex = new RegExp(pattern); + if (regex.test(currentPageUrl)) { + var buttons = document.getElementsByClassName( + "try_examples_button", + ); + for (var i = 0; i < buttons.length; i++) { + buttons[i].classList.add("hidden"); + } + break; + } } - break; + } catch (error) { + console.error(error); + } finally { + tryExamplesConfigLoaded = true; + } + })(); + + return configLoadPromise; + }; + + return { + loadConfig, + // for testing/debugging only, could be removed + resetState: () => { + tryExamplesConfigLoaded = false; + configLoadPromise = null; + }, + }; +})(); + +// Add a resize handler that will update the buttons' visibility on +// orientation changes +let resizeTimeout; +window.addEventListener("resize", () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + if (!tryExamplesConfigLoaded) return; // since we won't interfere if the config isn't loaded + + const buttons = document.getElementsByClassName("try_examples_button"); + const shouldHide = window.isMobileDevice(); + + for (let i = 0; i < buttons.length; i++) { + if (shouldHide) { + buttons[i].classList.add("hidden"); + } else { + buttons[i].classList.remove("hidden"); } } - } catch (error) { - console.error(error); - } - tryExamplesConfigLoaded = true; -}; + }, 250); +}); + +window.loadTryExamplesConfig = ConfigLoader.loadConfig; window.toggleTryExamplesButtons = () => { /* Toggle visibility of TryExamples buttons. For use in console for debug