Skip to content
Merged
9 changes: 0 additions & 9 deletions jupyterlite_sphinx/jupyterlite_sphinx.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
172 changes: 135 additions & 37 deletions jupyterlite_sphinx/jupyterlite_sphinx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading