// Wrap in an IFFE. (() => { /////////////////////////////////////////// // Remove the no-js class from the html tag that is meant for the noscript mode. /////////////////////////////////////////// document.getElementsByTagName('html')[0].classList.remove('no-js'); /////////////////////////////////////////// // Log for fellow developers. /////////////////////////////////////////// console.info(`%c Welcome to ${document.location.hostname}`, "padding:20px; font: 38px Impact, sans-serif; color: #ddd; text-shadow: 0 1px 1px #bbb,0 2px 0 #999, 0 3px 0 #888, 0 4px 0 #777, 0 5px 0 #666, 0 6px 0 #555, 0 7px 0 #444, 0 8px 0 #333, 0 9px 7px #302314;"); console.info("If you find something cool and would like to learn more, please contact me using the contact page. Will love to hear from a fellow developer"); /////////////////////////////////////////// // Convert date to hours from now /////////////////////////////////////////// function getAgoTime(value) { if (!value) { return ""; } const d = new Date(value.trim()); const now = new Date(); const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000)); const minutes = Math.round(Math.abs(seconds / 60)); const hours = Math.round(Math.abs(minutes / 60)); const days = Math.round(Math.abs(hours / 24)); const months = Math.round(Math.abs(days / 30.416)); const years = Math.round(Math.abs(days / 365)); if (seconds <= 45) { return 'a few seconds ago'; } else if (seconds <= 90) { return 'a minute ago'; } else if (minutes <= 45) { return minutes + ' minutes ago'; } else if (minutes <= 90) { return 'an hour ago'; } else if (hours <= 22) { return hours + ' hours ago'; } else if (hours <= 36) { return 'a day ago'; } else if (days <= 25) { return days + ' days ago'; } else if (days <= 45) { return 'a month ago'; } else if (days <= 345) { return months + ' months ago'; } else if (days <= 545) { return 'a year ago'; } else { // (days > 545) return years + ' years ago'; } } document.addEventListener("DOMContentLoaded", () => { Array.from(document.getElementsByTagName('time')).forEach((x) => { if (x.className === 'now') { x.innerText = new Date().getFullYear(); } else { x.innerText = getAgoTime(x.getAttribute('datetime')); } // TODO: Attach event listener to update the string here. }); }); /////////////////////////////////////////// // Contact Us form /////////////////////////////////////////// const contact = document.getElementById("contact-us-form"); if (contact) { contact.addEventListener("submit", (e) => { e.preventDefault(); fetch(contact.getAttribute("action") || window.location.pathname, { method: 'POST', body: new FormData(contact) }).then(response => { // Remove contact and add Thank You. const div = document.createElement('div'); div.className = "contact-response"; if (response.ok) { div.innerHTML = '{{- default "Thank you" $.Site.Params.contact.response -}}'; } else { div.innerHTML = '{{- default "There was an error sending this message. Please try again later" $.Site.Params.contact.responseError -}}' } contact.parentNode && contact.parentNode.replaceChild(div, contact); }) }) } /////////////////////////////////////////// // Header /////////////////////////////////////////// // Inspired by https://www.sysleaf.com/js-toggle-header-on-scroll/ const consideredTop = 200; const height = document.documentElement.clientHeight; let lastKnownScrollY = 0; let currentScrollY = 0; let ticking = false; let eleHeader = null; let eleCheckbox = null; let eleScroll = null; let eleSearch = null; let hitCount = 0; const classes = { pinned: 'header-pin', unpinned: 'header-unpin', }; function updateScrollPosition() { let winScroll = document.body.scrollTop || document.documentElement.scrollTop; let height = document.documentElement.scrollHeight - document.documentElement.clientHeight; let scrolled = (winScroll / height) * 100; document.getElementById("scroll-indicator").style.width = scrolled + "%"; } function onScroll() { currentScrollY = window.pageYOffset; requestAnimationFrame(updateScrollPosition); requestTick(); } function requestTick() { if (eleCheckbox.checked || eleSearch === document.activeElement) { return; } if (!ticking) { requestAnimationFrame(update); } ticking = true; } function update() { ticking = false; if (eleCheckbox.checked) { return; } // Scroll to top hiding. if (currentScrollY > 2 * height) { if (!eleScroll.classList.contains('visible')) { eleScroll.classList.add('visible'); } } else { if (eleScroll.classList.contains('visible')) { eleScroll.classList.remove('visible'); } } // Header hiding // Ignore first 2 hits for safari reload in the center of the page // It is good enough with 2. First is the Js load, // Second is safari's scroll to position. if (hitCount > 2) { if (currentScrollY + height >= document.documentElement.scrollHeight || currentScrollY < consideredTop) { pin(); } else if (currentScrollY < lastKnownScrollY) { pin(); } else if (currentScrollY > lastKnownScrollY) { unpin(); } } lastKnownScrollY = currentScrollY; hitCount++; } function pin() { if (eleHeader.classList.contains(classes.unpinned)) { eleHeader.classList.remove(classes.unpinned); eleHeader.classList.add(classes.pinned); eleScroll.classList.remove('unpin'); } } function unpin() { if (eleHeader.classList.contains(classes.pinned) || !eleHeader.classList.contains(classes.unpinned)) { eleHeader.classList.remove(classes.pinned); eleHeader.classList.add(classes.unpinned); eleScroll.classList.add('unpin'); } } document.addEventListener("DOMContentLoaded", () => { eleCheckbox = document.getElementsByClassName('hamburger')[0]; eleSearch = document.querySelector('#searchbox input'); eleHeader = document.getElementById('header'); eleScroll = document.querySelector('.scroll-up'); document.addEventListener('scroll', onScroll, false); }); /////////////////////////////////////////// // Color Picker /////////////////////////////////////////// let color = window.localStorage.color || '{{- $.Site.Params.color -}}'; function setColor(hex) { color = hex; window.localStorage.color = color; document.documentElement.style.setProperty("--theme-color", hex); } document.querySelectorAll('.theme-choice').forEach(s => { s.addEventListener('click', e => { const color = e.target.getAttribute('data-color'); setColor("#" + color); }); }); const colorSelector = document.querySelector('.custom-color'); if (colorSelector) { colorSelector.addEventListener('click', evt => { evt.preventDefault(); const x = document.createElement("input"); x.setAttribute("type", "color"); x.value = color; x.click(); x.addEventListener('input', () => { setColor(x.value); }) }); } const checkbox = document.querySelector('#color-switch'); if (checkbox) { checkbox.checked = document.documentElement.classList.contains("dark"); const handler = () => { if (checkbox.checked) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } window.localStorage.dark = checkbox.checked ? "true" : "false"; }; checkbox.addEventListener("change", handler); } /////////////////////////////////////////// // Search /////////////////////////////////////////// // {{- with .Site.Home.OutputFormats.Get "JSON" }} class Search { constructor() { // Ignore errors loading search. this.prepare().catch(() => { }); } get resultTemplate() { return `
Result

{{- partialCached "util/icon" (dict "key" "calendar" "size" 12) "calendar-12" -}} {{- partialCached "util/icon" (dict "key" "hourglass-1" "size" 12) "hourglass-1-12" -}} {{- partialCached "util/icon" (dict "key" "caret-square-o-right" "size" 12) "caret-square-o-right-12" -}}
`; } async prepare() { const response = await fetch('/index.json'); const data = await response.json(); if (data && data.length > 0) { this.data = data; document.querySelector('#searchbox').classList.add('visible'); this.input = document.querySelector('#searchbox input'); this.input.addEventListener('focus', this.triggerResults.bind(this)); this.input.addEventListener('input', this.triggerResults.bind(this)); this.input.addEventListener('keyup', this.handleKeyPress.bind(this)); } } triggerResults() { // We are delaying calculations but that's not a big performance issues. // If you type too fast on a slow browser, we won't get an animation frame. window.requestAnimationFrame(this.showSearchResults.bind(this)); } createResultDivs() { let data = ''; for (var i = 0; i < 5; ++i) { data += this.resultTemplate; } document.querySelector('#searchbox .results').innerHTML = data; const divs = Array.from(document.querySelectorAll("#searchbox .results>div")); divs.forEach(x => { x.addEventListener('mousedown', this.handleClick, { passive: true }); x.addEventListener('mouseover', () => this.selected = x, { passive: true }); }); return divs; } handleClick(e) { window.location = e.currentTarget.dataset['href']; } set selected(element) { if (this._selected !== element) { this._selected && this._selected.classList.remove('selected'); element.classList.add('selected'); this._selected = element; } } get selected() { return this._selected; } handleKeyPress(key) { const x = this.resultDivs.indexOf(this.selected); switch (key.code) { case 'ArrowDown': if (this.resultDivs.length > x + 1 && this.resultDivs[x + 1].style.display !== 'none') { this.selected = this.resultDivs[x + 1]; } break; case 'ArrowUp': if (x > 0) { this.selected = this.resultDivs[x - 1]; } break; case 'Escape': this.input.blur(); break; case 'Enter': window.location = this.selected.dataset['href']; break; } } get indexedData() { return this.index = this.index || this.data.map(x => { let index = {}; for (let y in x) { index[y] = JSON.stringify(x[y]).toLowerCase() } x._index = index; return x; }); } search(key) { key = key.toLowerCase(); const fields = { 'title': 1, 'description': 0.3, 'series': 0.5, 'category': 0.5, 'tags': 0.5, 'contents': 0.1, 'meta': 0.1 } let data = this.indexedData; let results = []; if (this.lastKnownResults) { if (key.indexOf(this.lastKnownResults.key) === 0) { data = this.lastKnownResults.results; } } // Search algorithm results = data.map(x => ({ data: x, score: Object.keys(fields) .map(y => x._index[y].indexOf(key) !== -1 ? fields[y] : 0) .reduce((a, b) => a + b) })) .filter(x => x.score > 0) .sort((a, b) => b.score - a.score !== 0 ? b.score - a.score : new Date(b.data.date) - new Date(a.data.date) ) .map(x => x.data); this.lastKnownResults = { key, results }; return results; } showSearchResults() { this.resultDivs = this.resultDivs || this.createResultDivs(); let results = []; if (this.input.value.length === 0) { results = this.data.sort((b, a) => new Date(a.date).getTime() - new Date(b.date).getTime()); } else { results = this.search(this.input.value); } this.resultDivs.forEach((div, index) => { const result = results[index]; if (index === 0) { this.selected = div; } if (!result) { div.style.display = "none"; return; } const ago = getAgoTime(result.date); div.style.display = "block"; div.dataset['href'] = result.permalink; div.querySelector('img').alt = result.title; div.querySelector('img').src = result.image; div.querySelector('h2').innerText = result.title; div.querySelector('time').innerText = ago.substr(0, ago.indexOf(" ago")); div.querySelector('time').datetime = result.date; div.querySelector('.description').innerText = result.description; div.querySelector('.readingTime').innerText = result.readingTime + 'm'; div.querySelector('.category').innerText = result.category; }); } } new Search(); // {{ end }} /////////////////////////////////////////// // Service Worker /////////////////////////////////////////// /*{{ if not hugo.IsServer }}*/ if ('serviceWorker' in navigator && window.location.pathname !== '/offline') { navigator.serviceWorker.register('/sw.min.js', { scope: '/' }); } /*{{ end }}*/ //////////////////////////////////////////////////////// // Link Pre-fetching to improve perceptible load times. //////////////////////////////////////////////////////// Array.from(document.querySelectorAll('a')).forEach((link) => { let prefetchComplete = false; const prefetch = () => { if (prefetchComplete) { return; } prefetchComplete = true; if (link.href.includes('mailto')) { return; } if (link.href.includes('http') && new URL(link.href).host !== new URL(window.location.href).host) { return; } if (link.href === window.location.href) { return; } const loader = document.createElement("link"); loader.rel = "prefetch"; loader.as = "document"; loader.type = "text/html"; loader.href = link.href; document.head.appendChild(loader); }; link.addEventListener("mouseenter", prefetch, { passive: true }); link.addEventListener("touchstart", prefetch, { passive: true }); }); /////////////////////////////////////////// // Intersection Observer for animations /////////////////////////////////////////// // Scroll animations for iPad and bigger if (window.innerWidth >= 768) { const observer = new IntersectionObserver((entries) => { entries.forEach(x => { if (x.isIntersecting && !x.target.classList.contains('visible')) { x.target.classList.add('visible'); } if (x.target.classList.contains('visible')) { observer.unobserve(x.target); } }); }); ['.left-image', '.meta.default .item', '.item-icon-left .item', '.meta.default', '.max-2', '.max-2 .item', '.filter', '.filter .item', '.blog', '.blog .item', 'footer .items', '.contact', '.more', '.comments', '.comments form', '.item-icon-left', '.full-width', '.full-width .item'].forEach(s => { document.querySelectorAll(s).forEach(x => { const rect = x.getBoundingClientRect(); // All elements above the scroll are visible by default. // We only animate and scroll down. if (rect.y < 0) { x.classList.add('visible'); } else { observer.observe(x); } }); }); } else { // For mobile the hover animations const observer = new IntersectionObserver((entries) => { entries.forEach(x => { if (x.isIntersecting && !x.target.classList.contains('hover')) { x.target.classList.add('hover'); } else if (!x.isIntersecting && x.target.classList.contains('hover')) { x.target.classList.remove('hover') } }); }, { rootMargin: "-20% 0px -70% 0px" }); ['p', 'li', '.meta .content', '.meta .item-cover', '.item-icon', '.main-icon', '.banner-holder', '.img-container', '.tex', '.i-tex', '.post aside svg', '.meta.carousel .item'].forEach(s => { document.querySelectorAll(s).forEach(x => { observer.observe(x); }); }); } })();