diyhrt/transfemscience.org/assets/js/script.js

704 lines
24 KiB
JavaScript

/* These need to be here and run before DOMContentLoaded to prevent flash of unstyled content (FOUC) */
/* Only <html> and <head> are available by this point (based on when script called in HTML) */
// For preventing jump of scroll position on click when scrolled down in sidebar
var toc_sidebar_no_scroll_jump = false;
// Set theme if applicable
if (is_local_storage_available() == true && localStorage.getItem('theme')) {
document.documentElement.setAttribute('data-theme', localStorage.getItem('theme'));
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches == true) {
document.documentElement.setAttribute('data-theme', 'dark');
}
// Set persistent header to hidden if applicable
if (is_local_storage_available() == true && localStorage.getItem('persistent-header-hidden')) {
if (localStorage.getItem('persistent-header-hidden') == 'true') {
toggle_persistent_header(false);
}
}
// Show sidebar if applicable
if (is_local_storage_available() == true && localStorage.getItem('show-sidebar')) {
if (localStorage.getItem('show-sidebar') == 'true' && is_mobile() == false) {
toggle_sidebar(false);
}
// If no sidebar show setting and if screen is reasonably wide, then show it by default
} else {
if (window.matchMedia && is_mobile() == false) {
var media_query = window.matchMedia('(min-width: 1250px)');
if (media_query.matches == true) {
toggle_sidebar(false);
}
}
}
window.addEventListener('DOMContentLoaded', function () {
// Set first current sidebar section
if (does_sidebar_exist() == true) {
update_current_sidebar_section();
}
// Set up for Google Translate if applicable
if (is_google_translate_open() == true) {
// Get and set Google Translate bar height
update_google_translate_bar_height();
// Mutation observer to watch for and update upon changes of Google Translate bar show/hide
var observer = new MutationObserver(update_google_translate_bar_height);
observer.observe(document.body, { attributes: true, attributeFilter: ['style'] });
}
// Set up site event listeners (scroll, buttons, color scheme change, keyboard keys, etc.)
set_up_site_event_listeners();
return;
});
// Set up site event listeners
function set_up_site_event_listeners() {
/* Scroll, resize, fullscreen, and color scheme change listeners */
// Page scroll listener
window.addEventListener('scroll', function () {
// Check and set persistent header status
update_header_persistent_status();
// Update sidebar current section
if (does_sidebar_exist() == true) {
update_current_sidebar_section();
}
return;
});
// Mobile change listener
if (window.matchMedia) {
matchMediaAddEventListener('(max-width: 900px)', function () {
// If switching to mobile
if (is_mobile() == true) {
// Close sidebar if it's open
if (is_sidebar_open() == true) {
toggle_sidebar(false);
}
// If switching to desktop
} else {
// Open sidebar if it should be open
if (is_sidebar_open() == false) {
if (is_local_storage_available() == true && localStorage.getItem('show-sidebar') == 'true') {
toggle_sidebar(false);
}
}
}
return;
});
}
// Theme (light/dark mode) listener (follow system)
if (window.matchMedia) {
matchMediaAddEventListener('(prefers-color-scheme: dark)', function (event) {
if (is_local_storage_available() == true && !localStorage.getItem('theme')) {
var theme = event.matches == true ? 'dark' : 'light';
if (theme == 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
return;
});
}
/* Button listeners */
// Theme (light/dark mode) button listener
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById('theme-button').addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
toggle_theme(true);
return;
});
});
// Theme (light/dark mode) button reset listener (context menu)
document.getElementById('theme-button').addEventListener('contextmenu', function (event) {
event.preventDefault();
if (!window.confirm('Clear theme (light/dark mode) setting? (This will reset the theme setting to the default of follow system theme.)')) {
return;
}
if (is_local_storage_available() == true) {
localStorage.removeItem('theme');
}
if (window.matchMedia) {
var media_query = window.matchMedia('(prefers-color-scheme: light)');
if (media_query.matches == true) {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
}
}
// alert('Theme (light/dark mode) setting reset.');
return;
});
// Translate button listener
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById('language-button').addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
// If Google Translate mode isn't already active
if (is_google_translate_open() == false) {
// Get user's preferred language from browser settings
var preferred_language = String(navigator.language);
// Need to get rid of country code but Chinese simplified vs. traditional are exceptions for Google Translate
// https://cloud.google.com/translate/docs/languages
if (preferred_language != 'zh-CN' && preferred_language != 'zh-TW') {
preferred_language = preferred_language.split('-')[0];
}
// Fix Filipino (seems to be an exception with mismatched code here among many languages)
// It's "Filipino" ("fil") in Chrome but "Tagalog" ("tl") in Firefox; Google Translate uses "tl"
if (preferred_language == 'fil') {
preferred_language = 'tl';
}
// Set Google Translate settings
var source_language = 'auto'; // Must be 'auto' if trying to translate from 'en' to 'en' or will error
var target_language = preferred_language;
var interface_language = preferred_language;
// Open Google Translate for the site
window.open('https://translate.google.com/translate?sl=' + source_language + '&tl=' + target_language +
'&hl=' + interface_language + '&u=' + window.location.href + '&client=webapp', '_self');
// If Google Translate mode is active
} else {
var gtranslate_script = document.querySelector('[data-source-url]');
// Other Google Translate data attributes of potential inerest: data-proxy-url, data-proxy-full-url, data-source-language, data-target-language, data-display-language, data-detected-source-language, data-is-source-untranslated, data-source-untranslated-url
var return_url = gtranslate_script.getAttribute('data-source-url');
// Fallback
if (!return_url) {
return_url = 'https://transfemscience.org' + window.location.pathname;
}
// Take user back to site (exit Google Translate)
window.location.href = return_url;
}
return;
});
});
// Search button listener
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById('search-button').addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
// Open search (Google Custom Search Engine) in a new tab
// var search_url = 'https://cse.google.com/cse?cx=368ceeb2b0e46ce35';
// The regex checks for either GitHub or Google Translate format (periods substituted with dashes)
if (String(window.location.hostname).match(/[.-]github[.-]io/)) {
var search_url = String(location.origin) + '/transfemscience' + '/search/' + window.location.search;
} else {
var search_url = String(location.origin) + '/search/' + window.location.search;
}
window.open(search_url, '_self');
return;
});
});
// Contents sidebar open/close button listener
['sidebar-button', 'sidebar-button-standalone', 'toc-button-mobile'].forEach(function (element_id) {
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById(element_id).addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
toggle_sidebar(true);
return;
});
});
// Contents sidebar open/close button reset listener (context menu)
document.getElementById(element_id).addEventListener('contextmenu', function (event) {
event.preventDefault();
if (!window.confirm('Clear sidebar show/hide by default setting?')) {
return;
}
if (is_local_storage_available() == true) {
localStorage.removeItem('show-sidebar');
}
// alert('Sidebar show/hide by default setting reset.');
return;
});
});
/*
// Contents sidebar swipe left to close listener
document.addEventListener('swiped-left', function (event) {
// If sidebar is open and swiped element is not a table (with potential horizontal scroll)
if (is_sidebar_open() == true && !event.target.closest('table')) {
toggle_sidebar(true);
} else {
return;
}
});
// Contents sidebar swipe right to open listener
document.addEventListener('swiped-right', function (event) {
// If sidebar is closed and swiped element is not a table (with potential horizontal scroll)
if (is_sidebar_open() == false && !event.target.closest('table')) {
toggle_sidebar(true);
} else {
return;
}
});
*/
// Contents sidebar item listeners
var links = document.querySelectorAll('.toc-link');
for (var i = 0; i < links.length; i++) {
links[i].onclick = function (event) {
// If mobile, hide contents menu after clicking a ToC link
if (is_mobile() == true) {
if (is_sidebar_open() == true) {
// Prevent slide transition for sidebar hide
document.documentElement.setAttribute('data-sidebar-slide-transition-disable', '');
// Following a short delay (to show tap / link highlight)...
setTimeout(function () {
// Close the sidebar
toggle_sidebar();
// Reset prevent sidebar hide slide transition
setTimeout(function () {
document.documentElement.removeAttribute('data-sidebar-slide-transition-disable');
}, 150);
}, 100);
}
}
// Hide anchor tag from URL in the case of sidebar #top link click
if (event.target.hash == '#top') {
window.scrollTo(0, 0);
history.pushState({}, document.title, window.location.href.split('#')[0]);
return false;
}
// Prevent jump of scroll position on click if scrolled down in sidebar
toc_sidebar_no_scroll_jump = true;
setInterval(function () {
toc_sidebar_no_scroll_jump = false;
}, 200);
return;
}
}
// Mobile menu open/close button listener
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById('menu-button-mobile-menu').addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
toggle_mobile_menu();
return;
});
});
// Persistent header hide/restore button listeners
['hide-persistent-header-button', 'restore-persistent-header-button'].forEach(function (element_id) {
['mousedown', 'keydown'].forEach(function (event_type) {
document.getElementById(element_id).addEventListener(event_type, function (event) {
// If keydown event and not Enter or Space key
if (event_type == 'keydown' && event.code != 'Enter' && event.code != 'Space') { return; }
// If mousedown event
if (event_type == 'mousedown') {
// Prevent setting button active and therefore outline
event.preventDefault();
// If not main mouse button (e.g., context menu)
if (event.button != 0) {
return;
}
}
// Execute button action
toggle_persistent_header(true);
return;
});
});
// Persistent header hide/restore button reset listeners (context menu)
document.getElementById(element_id).addEventListener('contextmenu', function (event) {
event.preventDefault();
if (!window.confirm('Clear persistent top bar show/hide by default setting?')) {
return;
}
if (is_local_storage_available() == true) {
localStorage.removeItem('persistent-header-hidden');
}
if (is_persistent_header_hidden() == true) {
toggle_persistent_header(false);
}
// alert('Persistent top bar show/hide by default setting reset.');
return;
});
});
/* Keyboard listeners */
// Keyboard input listener
document.addEventListener('keydown', function (event) {
// Sidebar keyboard shortcut handler (Ctrl+Shift+L)
if (event.ctrlKey && event.shiftKey && (event.key === 'l' || event.key === 'L')) {
event.preventDefault();
toggle_sidebar(true);
// Persistent header / top bar keyboard shortcut handler (Ctrl+Shift+H)
} else if (event.ctrlKey && event.shiftKey && (event.key === 'h' || event.key === 'H')) {
event.preventDefault();
toggle_persistent_header(true);
}
return;
});
/* Miscellaneous listeners */
// Spoiler image listeners
var spoilers = document.getElementsByClassName('spoiler');
Array.prototype.forEach.call(spoilers, function (spoiler) {
spoiler.addEventListener('click', function () {
if (spoiler.classList.contains('spoiler')) {
spoiler.classList.remove('spoiler');
}
return;
});
return;
});
return;
}
function is_dark_theme() {
var current_theme = document.documentElement.getAttribute('data-theme');
return current_theme == 'dark' ? true : false;
}
function toggle_theme(remember) {
var new_theme = is_dark_theme() == false ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', new_theme);
if (remember == true && is_local_storage_available() == true) {
localStorage.setItem('theme', new_theme);
}
return;
}
function is_header_persistent() {
var is_header_persistent = document.documentElement.hasAttribute('data-header-persistent');
return is_header_persistent;
}
function update_header_persistent_status() {
if (window.scrollY > 20) {
document.documentElement.setAttribute('data-header-persistent', '');
} else {
document.documentElement.removeAttribute('data-header-persistent');
}
return;
}
function is_mobile() {
if (window.matchMedia) {
var media_query = window.matchMedia('(max-width: 900px)');
if (media_query.matches == true) {
return true;
} else {
return false;
}
}
return;
}
function is_mobile_menu_open() {
return document.documentElement.hasAttribute('data-mobile-menu-open');
}
function toggle_mobile_menu() {
if (is_mobile_menu_open() == false) {
document.documentElement.setAttribute('data-mobile-menu-open', '');
} else {
document.documentElement.removeAttribute('data-mobile-menu-open');
}
return;
}
function is_persistent_header_hidden() {
return document.documentElement.hasAttribute('data-persistent-header-hidden');
}
function toggle_persistent_header(remember) {
// Prevent grow/shrink transition for persistent header hide/show
document.documentElement.setAttribute('data-persistent-header-toggle-no-transition', '');
// Toggle persistent header
if (is_persistent_header_hidden() == false) {
document.documentElement.setAttribute('data-persistent-header-hidden', '');
if (remember == true && is_local_storage_available() == true) {
localStorage.setItem('persistent-header-hidden', 'true');
}
} else {
document.documentElement.removeAttribute('data-persistent-header-hidden');
if (remember == true && is_local_storage_available() == true) {
localStorage.setItem('persistent-header-hidden', 'false');
}
}
// Reset prevent grow/shrink transition for persistent header hide/show
setTimeout(function () {
document.documentElement.removeAttribute('data-persistent-header-toggle-no-transition');
}, 200);
return;
}
function does_sidebar_exist() {
var does_sidebar_exist = !document.documentElement.hasAttribute('data-no-contents-sidebar');
return does_sidebar_exist;
}
function is_sidebar_open() {
var is_sidebar_open = document.documentElement.hasAttribute('data-sidebar-open');
return is_sidebar_open;
}
function toggle_sidebar(remember) {
if (is_sidebar_open() == false) {
document.documentElement.setAttribute('data-sidebar-open', '');
if (remember == true && is_local_storage_available() == true) {
localStorage.setItem('show-sidebar', 'true');
}
} else {
document.documentElement.removeAttribute('data-sidebar-open');
if (remember == true && is_local_storage_available() == true) {
localStorage.setItem('show-sidebar', 'false');
}
}
return;
}
function update_current_sidebar_section() {
var section_indice = get_current_sidebar_section_indice();
var sidebar_contents = document.getElementById('sidebar-contents');
var section_links = sidebar_contents.querySelectorAll('a[id^="heading-"]');
for (var i = 0; i < section_links.length; i++) {
if (section_links[i].id != 'heading-' + section_indice) {
if (section_links[i].classList.contains('active-section')) {
section_links[i].classList.remove('active-section');
}
} else {
if (!section_links[i].classList.contains('active-section')) {
section_links[i].classList.add('active-section');
if (toc_sidebar_no_scroll_jump == false) {
section_links[i].scrollIntoView(false);
}
}
}
}
return;
}
function get_current_sidebar_section_indice() {
var article = document.getElementById('article');
var sections = article.querySelectorAll('h1, h2, h3, h4, h5, h6');
var scroll_position = window.scrollY;
var page_height = document.documentElement.scrollHeight;
var window_height = window.innerHeight;
for (var i = sections.length - 1; i >= 0; i--) {
var section_position = sections[i].offsetTop;
// Very bottom (remove later (?))
if (i == sections.length - 1 && page_height - (scroll_position + window_height) <= 30) {
return i;
}
// Bottom
/*if (scroll_position >= page_height - window_height * 1.5) {
// ...
return sections.length; // Not this! (But just for now...)
}*/
// Very top (remove later (?))
if (scroll_position <= 30) {
return 0; // Top of page "section"
}
// Top
if (scroll_position < window_height * 0.5) {
if (scroll_position + window_height * 0.5 > section_position) {
return i;
}
}
// Top
/*if (scroll_position < window_height * 0.5) {
// ...
if (scroll_position >= section_position) {
return i - 1; // Not this! (But just for now...)
}
}*/
// Mid
if (scroll_position + window_height * 0.5 > section_position) {
return i;
}
}
// Scrolled midway (Luna-esque function—works (?) but could be better) (to-do: use/remove)
// } else if (section_position > scroll_position + window_height * (scroll_position / page_height)) {
return;
}
// Is Google Translate mode active?
function is_google_translate_open() {
return String(window.location.hostname).includes('translate.goog');
}
// Is Google Translate bar open? (Currently unused)
function is_google_translate_bar_open() {
if (is_google_translate_open() == false) {
return false;
} else {
var google_translate_bar_height = get_google_translate_bar_height();
return google_translate_bar_height == 0 ? false : true;
}
}
// Get Google Translate bar height
function get_google_translate_bar_height() {
return parseFloat(document.body.style.marginTop);
}
// Set Google Translate bar height
// For CSS styling to update site top positioning to account for Google Translate bar
function update_google_translate_bar_height() {
var google_translate_bar_height = get_google_translate_bar_height();
google_translate_bar_height = String(google_translate_bar_height) + 'px';
document.documentElement.style.setProperty('--google-translate-bar-height', google_translate_bar_height);
return;
}
// Wrapper function for window.matchMedia().addEventListener() for cross-browser support
// https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
function matchMediaAddEventListener(media_query, listener_function) {
try {
// Chrome, Firefox, iOS/Safari 14+, etc.
window.matchMedia(media_query).addEventListener('change', listener_function);
} catch {
try {
// iOS/Safari 13 and below
window.matchMedia(media_query).addListener(listener_function);
} catch {
console.error('window.matchMedia.addEventListener() and window.matchMedia.addListener() not supported.');
}
}
return;
}
// Check if local storage is supported and available (avoid exceptions)
// https://stackoverflow.com/questions/16427636/check-if-localstorage-is-available
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
function is_local_storage_available() {
let storage;
try {
storage = window['localStorage'];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch (error) {
return error instanceof DOMException && (
// Everything except Firefox
error.code === 22 ||
// Firefox
error.code === 1014 ||
// Test name field too, because code might not be present
// Everything except Firefox
error.name === 'QuotaExceededError' ||
// Firefox
error.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// Acknowledge QuotaExceededError only if there's something already stored
(storage && storage.length !== 0);
}
}