/************************************************* * Academic * https://github.com/gcushen/hugo-academic * * Core JS functions and initialization. **************************************************/ // Filter by search term. let $quickSearch = $('.filter-search').keyup(debounce(function () { searchRegex = new RegExp($quickSearch.val(), 'gi'); $grid_pubs.isotope(); })); // Debounce input to prevent spamming filter requests. function debounce(fn, threshold) { let timeout; threshold = threshold || 100; return function debounced() { clearTimeout(timeout); let args = arguments; let _this = this; function delayed() { fn.apply(_this, args); } timeout = setTimeout(delayed, threshold); }; } // Flatten object by concatenating values. function concatValues(obj) { let value = ''; for (let prop in obj) { value += obj[prop]; } return value; } $('.pub-filters').on('change', function () { let $this = $(this); // Get group key. let filterGroup = $this[0].getAttribute('data-filter-group'); // Set filter for group. pubFilters[filterGroup] = this.value; // Combine filters. filterValues = concatValues(pubFilters); // Activate filters. $grid_pubs.isotope(); // If filtering by publication type, update the URL hash to enable direct linking to results. if (filterGroup == "pubtype") { // Set hash URL to current filter. let url = $(this).val(); if (url.substr(0, 9) == '.pubtype-') { window.location.hash = url.substr(9); } else { window.location.hash = ''; } } }); // Filter publications according to hash in URL. function filter_publications() { let urlHash = window.location.hash.replace('#', ''); let filterValue = '*'; // Check if hash is numeric. if (urlHash != '' && !isNaN(urlHash)) { filterValue = '.pubtype-' + urlHash; } // Set filter. let filterGroup = 'pubtype'; pubFilters[filterGroup] = filterValue; filterValues = concatValues(pubFilters); // Activate filters. $grid_pubs.isotope(); // Set selected option. $('.pubtype-select').val(filterValue); } /* --------------------------------------------------------------------------- * Google Maps or OpenStreetMap via Leaflet. * --------------------------------------------------------------------------- */ function initMap() { if ($('#map').length) { let map_provider = $('#map-provider').val(); let lat = $('#map-lat').val(); let lng = $('#map-lng').val(); let zoom = parseInt($('#map-zoom').val()); let address = $('#map-dir').val(); let api_key = $('#map-api-key').val(); if (map_provider == 1) { let map = new GMaps({ div: '#map', lat: lat, lng: lng, zoom: zoom, zoomControl: true, zoomControlOpt: { style: 'SMALL', position: 'TOP_LEFT' }, panControl: false, streetViewControl: false, mapTypeControl: false, overviewMapControl: false, scrollwheel: true, draggable: true }); map.addMarker({ lat: lat, lng: lng, click: function (e) { let url = 'https://www.google.com/maps/place/' + encodeURIComponent(address) + '/@' + lat + ',' + lng + '/'; window.open(url, '_blank') }, title: address }) } else { let map = new L.map('map').setView([lat, lng], zoom); if (map_provider == 3 && api_key.length) { L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', maxZoom: 18, id: 'mapbox.streets', accessToken: api_key }).addTo(map); } else { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); } let marker = L.marker([lat, lng]).addTo(map); let url = lat + ',' + lng + '#map=' + zoom + '/' + lat + '/' + lng + '&layers=N'; marker.bindPopup(address + '
'); } } } /* --------------------------------------------------------------------------- * GitHub API. * --------------------------------------------------------------------------- */ function printLatestRelease(selector, repo) { $.getJSON('https://api.github.com/repos/' + repo + '/tags').done(function (json) { let release = json[0]; $(selector).append(' ' + release.name); }).fail(function (jqxhr, textStatus, error) { let err = textStatus + ", " + error; console.log("Request Failed: " + err); }); } /* --------------------------------------------------------------------------- * Toggle search dialog. * --------------------------------------------------------------------------- */ function toggleSearchDialog() { if ($('body').hasClass('searching')) { // Clear search query and hide search modal. $('[id=search-query]').blur(); $('body').removeClass('searching compensate-for-scrollbar'); // Remove search query params from URL as user has finished searching. removeQueryParamsFromUrl(); // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars. $('#fancybox-style-noscroll').remove(); } else { // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars. if (!$('#fancybox-style-noscroll').length && document.body.scrollHeight > window.innerHeight) { $('head').append( '' ); $('body').addClass('compensate-for-scrollbar'); } // Show search modal. $('body').addClass('searching'); $('.search-results').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 200); $('#search-query').focus(); } } /* --------------------------------------------------------------------------- * Change Theme Mode (0: Day, 1: Night, 2: Auto). * --------------------------------------------------------------------------- */ function canChangeTheme() { // If the theme changer component is present, then user is allowed to change the theme variation. return $('.js-theme-selector').length; } function getThemeMode() { return parseInt(localStorage.getItem('dark_mode') || 2); } function changeThemeModeClick(newMode) { console.info('Request to set theme.'); if (!canChangeTheme()) { console.info('Cannot set theme - admin disabled theme selector.'); return; } let isDarkTheme; switch (newMode) { case 0: localStorage.setItem('dark_mode', '1'); isDarkTheme = 1; console.info('User changed theme variation to Dark.'); showActiveTheme(0); break; case 1: localStorage.setItem('dark_mode', '2'); if (window.matchMedia('(prefers-color-scheme: dark)').matches) { // The visitor prefers dark themes and switching to the dark variation is allowed by admin. isDarkTheme = 1; } else if (window.matchMedia('(prefers-color-scheme: light)').matches) { // The visitor prefers light themes and switching to the dark variation is allowed by admin. isDarkTheme = 0; } else { isDarkTheme = isSiteThemeDark; // Use the site's default theme variation based on `light` in the theme file. } console.info('User changed theme variation to Auto.'); showActiveTheme(1); break; default: localStorage.setItem('dark_mode', '0'); isDarkTheme = 0; console.info('User changed theme variation to Light.'); showActiveTheme(2); break; } renderThemeVariation(isDarkTheme); } function showActiveTheme(mode){ switch (mode) { case 0: // Dark. $('.js-set-theme-light').removeClass('dropdown-item-active'); $('.js-set-theme-dark').addClass('dropdown-item-active'); $('.js-set-theme-auto').removeClass('dropdown-item-active'); break; case 1: // Auto. $('.js-set-theme-light').removeClass('dropdown-item-active'); $('.js-set-theme-dark').removeClass('dropdown-item-active'); $('.js-set-theme-auto').addClass('dropdown-item-active'); break; default: // Light. $('.js-set-theme-light').addClass('dropdown-item-active'); $('.js-set-theme-dark').removeClass('dropdown-item-active'); $('.js-set-theme-auto').removeClass('dropdown-item-active'); break; } } function getThemeVariation() { if (!canChangeTheme()) { return isSiteThemeDark; // Use the site's default theme variation based on `light` in the theme file. } let currentThemeMode = getThemeMode(); let isDarkTheme; switch (currentThemeMode) { case 0: isDarkTheme = 0; break; case 1: isDarkTheme = 1; break; default: if (window.matchMedia('(prefers-color-scheme: dark)').matches) { // The visitor prefers dark themes and switching to the dark variation is allowed by admin. isDarkTheme = 1; } else if (window.matchMedia('(prefers-color-scheme: light)').matches) { // The visitor prefers light themes and switching to the dark variation is allowed by admin. isDarkTheme = 0; } else { isDarkTheme = isSiteThemeDark; // Use the site's default theme variation based on `light` in the theme file. } break; } return isDarkTheme; } /** * Render theme variation (day or night). * * @param {int} isDarkTheme - TODO: convert to boolean. * @param {boolean} init * @returns {undefined} */ function renderThemeVariation(isDarkTheme, init = false) { // Is code highlighting enabled in site config? const codeHlEnabled = $('link[title=hl-light]').length > 0; const codeHlLight = $('link[title=hl-light]')[0]; const codeHlDark = $('link[title=hl-dark]')[0]; const diagramEnabled = $('script[title=mermaid]').length > 0; // Check if re-render required. if (!init) { // If request to render light when light variation already rendered, return. // If request to render dark when dark variation already rendered, return. if ((isDarkTheme === 0 && !$('body').hasClass('dark')) || (isDarkTheme === 1 && $('body').hasClass('dark'))) { return; } } if (isDarkTheme === 0) { if (!init) { // Only fade in the page when changing the theme variation. $('body').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 500); } $('body').removeClass('dark'); if (codeHlEnabled) { codeHlLight.disabled = false; codeHlDark.disabled = true; } if (diagramEnabled) { if (init) { mermaid.initialize({theme: 'default', securityLevel: 'loose'}); } else { // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks. location.reload(); } } } else if (isDarkTheme === 1) { if (!init) { // Only fade in the page when changing the theme variation. $('body').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 500); } $('body').addClass('dark'); if (codeHlEnabled) { codeHlLight.disabled = true; codeHlDark.disabled = false; } if (diagramEnabled) { if (init) { mermaid.initialize({theme: 'dark', securityLevel: 'loose'}); } else { // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks. location.reload(); } } } } function initThemeVariation() { // If theme changer component present, set its icon according to the theme mode (day, night, or auto). if (canChangeTheme) { let themeMode = getThemeMode(); switch (themeMode) { case 0: showActiveTheme(2); console.info('Initialize theme variation to Light.'); break; case 1: showActiveTheme(0); console.info('Initialize theme variation to Dark.'); break; default: showActiveTheme(1); console.info('Initialize theme variation to Auto.'); break; } } // Render the day or night theme. let isDarkTheme = getThemeVariation(); renderThemeVariation(isDarkTheme, true); } /* --------------------------------------------------------------------------- * Normalize Bootstrap Carousel Slide Heights. * --------------------------------------------------------------------------- */ function normalizeCarouselSlideHeights() { $('.carousel').each(function () { // Get carousel slides. let items = $('.carousel-item', this); // Reset all slide heights. items.css('min-height', 0); // Normalize all slide heights. let maxHeight = Math.max.apply(null, items.map(function () { return $(this).outerHeight() }).get()); items.css('min-height', maxHeight + 'px'); }) } /* --------------------------------------------------------------------------- * Fix Hugo's Goldmark output and Mermaid code blocks. * --------------------------------------------------------------------------- */ /** * Fix Hugo's Goldmark output. */ function fixHugoOutput() { // Fix Goldmark table of contents. // - Must be performed prior to initializing ScrollSpy. $('#TableOfContents').addClass('nav flex-column'); $('#TableOfContents li').addClass('nav-item'); $('#TableOfContents li a').addClass('nav-link'); // Fix Goldmark task lists (remove bullet points). $("input[type='checkbox'][disabled]").parents('ul').addClass('task-list'); } /** * Fix Mermaid.js clash with Highlight.js. * Refactor Mermaid code blocks as divs to prevent Highlight parsing them and enable Mermaid to parse them. */ function fixMermaid() { let mermaids = []; [].push.apply(mermaids, document.getElementsByClassName('language-mermaid')); for (let i = 0; i < mermaids.length; i++) { $(mermaids[i]).unwrap('pre'); // Remove wrapper.
$(mermaids[i]).replaceWith(function () {
// Convert block to and add `mermaid` class so that Mermaid will parse it.
return $("").append($(this).contents()).addClass('mermaid');
});
}
}
/* ---------------------------------------------------------------------------
* On document ready.
* --------------------------------------------------------------------------- */
$(document).ready(function () {
fixHugoOutput();
fixMermaid();
// Initialise code highlighting if enabled for this page.
// Note: this block should be processed after the Mermaid code-->div conversion.
if (code_highlighting) {
hljs.initHighlighting();
}
// Initialize theme variation.
initThemeVariation();
// Change theme mode.
$('.js-set-theme-light').click(function (e) {
e.preventDefault();
changeThemeModeClick(2);
});
$('.js-set-theme-dark').click(function (e) {
e.preventDefault();
changeThemeModeClick(0);
});
$('.js-set-theme-auto').click(function (e) {
e.preventDefault();
changeThemeModeClick(1);
});
// Live update of day/night mode on system preferences update (no refresh required).
// Note: since we listen only for *dark* events, we won't detect other scheme changes such as light to no-preference.
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addListener((e) => {
if (!canChangeTheme()) {
// Changing theme variation is not allowed by admin.
return;
}
const darkModeOn = e.matches;
console.log(`OS dark mode preference changed to ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
let currentThemeVariation = parseInt(localStorage.getItem('dark_mode') || 2);
let isDarkTheme;
if (currentThemeVariation === 2) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
// The visitor prefers dark themes.
isDarkTheme = 1;
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
// The visitor prefers light themes.
isDarkTheme = 0;
} else {
// The visitor does not have a day or night preference, so use the theme's default setting.
isDarkTheme = isSiteThemeDark;
}
renderThemeVariation(isDarkTheme);
}
});
});
/* ---------------------------------------------------------------------------
* On window loaded.
* --------------------------------------------------------------------------- */
$(window).on('load', function () {
// Filter projects.
$('.projects-container').each(function (index, container) {
let $container = $(container);
let $section = $container.closest('section');
let layout;
if ($section.find('.isotope').hasClass('js-layout-row')) {
layout = 'fitRows';
} else {
layout = 'masonry';
}
$container.imagesLoaded(function () {
// Initialize Isotope after all images have loaded.
$container.isotope({
itemSelector: '.isotope-item',
layoutMode: layout,
masonry: {
gutter: 20
},
filter: $section.find('.default-project-filter').text()
});
// Filter items when filter link is clicked.
$section.find('.project-filters a').click(function () {
let selector = $(this).attr('data-filter');
$container.isotope({filter: selector});
$(this).removeClass('active').addClass('active').siblings().removeClass('active all');
return false;
});
// If window hash is set, scroll to hash.
// Placing this within `imagesLoaded` prevents scrolling to the wrong location due to dynamic image loading
// affecting page layout and position of the target anchor ID.
// Note: If there are multiple project widgets on a page, ideally only perform this once after images
// from *all* project widgets have finished loading.
if (window.location.hash) {
scrollToAnchor();
}
});
});
// Enable publication filter for publication index page.
if ($('.pub-filters-select')) {
filter_publications();
// Useful for changing hash manually (e.g. in development):
// window.addEventListener('hashchange', filter_publications, false);
}
// Scroll to top of page.
$('.back-to-top').click(function (event) {
event.preventDefault();
$('html, body').animate({
'scrollTop': 0
}, 800, function () {
window.location.hash = "";
});
});
// Load citation modal on 'Cite' click.
$('.js-cite-modal').click(function (e) {
e.preventDefault();
let filename = $(this).attr('data-filename');
let modal = $('#modal');
modal.find('.modal-body code').load(filename, function (response, status, xhr) {
if (status == 'error') {
let msg = "Error: ";
$('#modal-error').html(msg + xhr.status + " " + xhr.statusText);
} else {
$('.js-download-cite').attr('href', filename);
}
});
modal.modal('show');
});
// Copy citation text on 'Copy' click.
$('.js-copy-cite').click(function (e) {
e.preventDefault();
// Get selection.
let range = document.createRange();
let code_node = document.querySelector('#modal .modal-body');
range.selectNode(code_node);
window.getSelection().addRange(range);
try {
// Execute the copy command.
document.execCommand('copy');
} catch (e) {
console.log('Error: citation copy failed.');
}
// Remove selection.
window.getSelection().removeRange(range);
});
// Initialise Google Maps if necessary.
initMap();
// Print latest version of GitHub projects.
let githubReleaseSelector = '.js-github-release';
if ($(githubReleaseSelector).length > 0)
printLatestRelease(githubReleaseSelector, $(githubReleaseSelector).data('repo'));
// On search icon click toggle search dialog.
$('.js-search').click(function (e) {
e.preventDefault();
toggleSearchDialog();
});
$(document).on('keydown', function (e) {
if (e.which == 27) {
// `Esc` key pressed.
if ($('body').hasClass('searching')) {
toggleSearchDialog();
}
} else if (e.which == 191 && e.shiftKey == false && !$('input,textarea').is(':focus')) {
// `/` key pressed outside of text input.
e.preventDefault();
toggleSearchDialog();
}
});
});
// Normalize Bootstrap carousel slide heights.
$(window).on('load resize orientationchange', normalizeCarouselSlideHeights);
// Automatic main menu dropdowns on mouse over.
$('body').on('mouseenter mouseleave', '.dropdown', function (e) {
var dropdown = $(e.target).closest('.dropdown');
var menu = $('.dropdown-menu', dropdown);
dropdown.addClass('show');
menu.addClass('show');
setTimeout(function () {
dropdown[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
menu[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
}, 300);
// Re-initialize Scrollspy with dynamic navbar height offset.
fixScrollspy();
if (window.location.hash) {
// When accessing homepage from another page and `#top` hash is set, show top of page (no hash).
if (window.location.hash == "#top") {
window.location.hash = ""
} else if (!$('.projects-container').length) {
// If URL contains a hash and there are no dynamically loaded images on the page,
// immediately scroll to target ID taking into account responsive offset.
// Otherwise, wait for `imagesLoaded()` to complete before scrolling to hash to prevent scrolling to wrong
// location.
scrollToAnchor();
}
}
// Call `fixScrollspy` when window is resized.
let resizeTimer;
$(window).resize(function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(fixScrollspy, 200);
});
});
})(jQuery);