From 476486d04091f35fd5d7aaee401b9057cc3661bf Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 10 Jan 2025 07:27:42 +0530 Subject: [PATCH 1/6] Add throttled ConfigLoader to cache try_examples config --- jupyterlite_sphinx/jupyterlite_sphinx.js | 137 ++++++++++++++++------- 1 file changed, 97 insertions(+), 40 deletions(-) diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index 4ae8c96..d1da0f7 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -149,55 +149,112 @@ var tryExamplesGlobalMinHeight = 0; */ var tryExamplesConfigLoaded = false; -window.loadTryExamplesConfig = async (configFilePath) => { - if (tryExamplesConfigLoaded) { - return; - } - 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; - } - throw new Error(`Error fetching ${configFilePath}`); +// A config loader with imprved error handling + request deduplication +const ConfigLoader = (() => { + // setting a private state for managing requests and errors + let configLoadPromise = null; + let lastErrorTimestamp = 0; + const ERROR_THROTTLE_MS = 5000; // error messages at most every 5 seconds + const failedRequestsCache = new Set(); + + const shouldShowError = () => { + const now = Date.now(); + if (now - lastErrorTimestamp > ERROR_THROTTLE_MS) { + lastErrorTimestamp = now; + return true; } + return false; + }; - const data = await response.json(); - if (!data) { + const logError = (message) => { + if (shouldShowError()) { + console.log(message); + } + }; + + const loadConfig = async (configFilePath) => { + if (tryExamplesConfigLoaded) { return; } - // Set minimum iframe height based on value in config file - if (data.global_min_height) { - tryExamplesGlobalMinHeight = parseInt(data.global_min_height); + if (failedRequestsCache.has(configFilePath)) { + return; } - // 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"); + // 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; + } + + 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) { + failedRequestsCache.add(configFilePath); + logError("Optional try_examples config file not found."); + return; + } + throw new Error(`Error fetching ${configFilePath}`); + } + + const data = await response.json(); + if (!data) { + return; } - break; + + // 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; + } + } + } catch (error) { + console.error(error); + } finally { + tryExamplesConfigLoaded = true; + configLoadPromise = null; } - } - } catch (error) { - console.error(error); - } - tryExamplesConfigLoaded = true; -}; + })(); + + return configLoadPromise; + }; + + return { + loadConfig, + resetState: () => { + tryExamplesConfigLoaded = false; + configLoadPromise = null; + failedRequestsCache.clear(); + lastErrorTimestamp = 0; + }, + }; +})(); + +window.loadTryExamplesConfig = ConfigLoader.loadConfig; window.toggleTryExamplesButtons = () => { /* Toggle visibility of TryExamples buttons. For use in console for debug From af7f8cdb39270bcfc548fa0b88473296859a00b2 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Fri, 10 Jan 2025 07:30:02 +0530 Subject: [PATCH 2/6] Fix typo --- jupyterlite_sphinx/jupyterlite_sphinx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index d1da0f7..0b43007 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -149,7 +149,7 @@ var tryExamplesGlobalMinHeight = 0; */ var tryExamplesConfigLoaded = false; -// A config loader with imprved error handling + request deduplication +// A config loader with improved error handling + request deduplication const ConfigLoader = (() => { // setting a private state for managing requests and errors let configLoadPromise = null; From f1bb4eaae483788012050b28760d7c3f8834fc58 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:34:59 +0530 Subject: [PATCH 3/6] Add more mobile device detection for hiding TryExamples buttons --- jupyterlite_sphinx/jupyterlite_sphinx.js | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index 0b43007..bff58cb 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -149,6 +149,42 @@ var tryExamplesGlobalMinHeight = 0; */ var tryExamplesConfigLoaded = false; +// 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.", + ); + } + + return isLikelyMobile; +}; + // A config loader with improved error handling + request deduplication const ConfigLoader = (() => { // setting a private state for managing requests and errors @@ -173,6 +209,15 @@ const ConfigLoader = (() => { }; 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"); + } + tryExamplesConfigLoaded = true; // mock it + return; + } + if (tryExamplesConfigLoaded) { return; } @@ -254,6 +299,27 @@ const ConfigLoader = (() => { }; })(); +// 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"); + } + } + }, 250); +}); + window.loadTryExamplesConfig = ConfigLoader.loadConfig; window.toggleTryExamplesButtons = () => { From 30c440150b6ab845c6092659437db049655b6d64 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:42:38 +0530 Subject: [PATCH 4/6] Remove redundant CSS --- jupyterlite_sphinx/jupyterlite_sphinx.css | 9 --------- 1 file changed, 9 deletions(-) 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; - } -} From fcfd71c098e69b1d67a98cbfa8bee763008315a3 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:09:27 +0530 Subject: [PATCH 5/6] Remove throttling and simplify caching --- jupyterlite_sphinx/jupyterlite_sphinx.js | 31 +++--------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index 0b43007..1d5315b 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -149,38 +149,15 @@ var tryExamplesGlobalMinHeight = 0; */ var tryExamplesConfigLoaded = false; -// A config loader with improved error handling + request deduplication +// A config loader with request deduplication + permanent caching const ConfigLoader = (() => { - // setting a private state for managing requests and errors let configLoadPromise = null; - let lastErrorTimestamp = 0; - const ERROR_THROTTLE_MS = 5000; // error messages at most every 5 seconds - const failedRequestsCache = new Set(); - - const shouldShowError = () => { - const now = Date.now(); - if (now - lastErrorTimestamp > ERROR_THROTTLE_MS) { - lastErrorTimestamp = now; - return true; - } - return false; - }; - - const logError = (message) => { - if (shouldShowError()) { - console.log(message); - } - }; const loadConfig = async (configFilePath) => { if (tryExamplesConfigLoaded) { return; } - if (failedRequestsCache.has(configFilePath)) { - return; - } - // 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 @@ -200,8 +177,7 @@ const ConfigLoader = (() => { const response = await fetch(configFileUrl); if (!response.ok) { if (response.status === 404) { - failedRequestsCache.add(configFilePath); - logError("Optional try_examples config file not found."); + console.log("Optional try_examples config file not found."); return; } throw new Error(`Error fetching ${configFilePath}`); @@ -245,11 +221,10 @@ const ConfigLoader = (() => { return { loadConfig, + // for testing/debugging only, could be removed resetState: () => { tryExamplesConfigLoaded = false; configLoadPromise = null; - failedRequestsCache.clear(); - lastErrorTimestamp = 0; }, }; })(); From f4944bada11f36295740bf9725f0804068fec39c Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:12:38 +0530 Subject: [PATCH 6/6] Add comment, remove `configLoadPromise = null` --- jupyterlite_sphinx/jupyterlite_sphinx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlite_sphinx/jupyterlite_sphinx.js b/jupyterlite_sphinx/jupyterlite_sphinx.js index 1d5315b..2304c86 100644 --- a/jupyterlite_sphinx/jupyterlite_sphinx.js +++ b/jupyterlite_sphinx/jupyterlite_sphinx.js @@ -166,6 +166,7 @@ const ConfigLoader = (() => { return configLoadPromise; } + // 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 @@ -212,7 +213,6 @@ const ConfigLoader = (() => { console.error(error); } finally { tryExamplesConfigLoaded = true; - configLoadPromise = null; } })();