Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better mobile device detection for interactive examples buttons #250

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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