Sliding BLOG LIST Indicator GUN
If you browsed my blog posts on desktop, you noticed the sliding pistol indicator on the left. The following code enables that functionality. It is essentially a DIV with absolute positioning which through Javascript takes on the location of the node you are hovering over, with a slight time-out to prevent jitter. Since my blog is dynamically generated I had to use multiple observers. First one checks if the container element is added, the second one checks if the list is added, and another one checks for the blog titles inside that container, which subsequently become the hover targets for the pistol to move towards vertically. The code also handles offsets for dynamic image adjustment stored in variables, and accounts for CSS zoom as of the last update. It also only reveals the indicator when it has assumed its position near the first list entry. Currently this code is used for list entries inside my blog container. It can be easily adapted, however, to monitor other classes and slide towards those, if you want to use it elsewhere on the website.
HTML: <div id="gunindicator" class="gunindicator"></div>
JAVASCRIPT: document.addEventListener("DOMContentLoaded", function() { const subString = '/devlog'; const getURL = window.location.href; const indicator = document.querySelector(".gunindicator"); const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); if (getURL.includes(subString) && isTouch == false) { const leftOffset = 90; const topOffset = 12; let indicatorSet = false; let initialContainer; let hoverTimeout; function updateIndicator(link) { const zoomFactor = parseFloat(document.body.style.zoom) || 1; const rect = link.getBoundingClientRect(); const offsetTop = rect.top + window.scrollY - topOffset; const offsetLeft = rect.left - leftOffset; const linkHeight = rect.height; // Adjust the indicator's position and height indicator.style.left = `${offsetLeft / zoomFactor}px`; indicator.style.top = `${offsetTop / zoomFactor}px`; indicator.style.height = `${linkHeight / zoomFactor}px`; } function showIndicator() { indicator.style.opacity = '1'; indicator.style.visibility = 'visible'; indicator.style.transform = "rotate(0deg)"; } function addHoverEffect(link) { link.addEventListener("mouseover", function() { indicator.style.transform = "rotate(-45deg)"; clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => { updateIndicator(link); indicator.style.transform = "rotate(0deg)"; }, 350); // prevent jittering by delay before focus on link target }); } function attachHoverToAllLinks(container) { const links = container.querySelectorAll("li"); links.forEach(link => { addHoverEffect(link); if (!indicatorSet) { updateIndicator(link); showIndicator(); indicatorSet = true; } }); } function setupObserverForList(container) { const listObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches("li")) { console.log("li added: ", node); // Debugging line addHoverEffect(node); } }); } } }); listObserver.observe(container, { childList: true, subtree: true }); } function setupObserver(container) { const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches("ol")) { attachHoverToAllLinks(node); setupObserverForList(node); } }); } } }); observer.observe(container, { childList: true, subtree: true }); } function handleBlogPostsContainer(container) { setupObserver(container); const firstList = container.querySelector("ol"); if (firstList) { attachHoverToAllLinks(firstList); // Attach hover to any initial `li` elements setupObserverForList(firstList); // Watch for `li` elements in the `ol` } } function monitorForBlogPostsContainer() { const bodyObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches("#blogPostsContainer")) { handleBlogPostsContainer(node); } }); } } }); bodyObserver.observe(document.body, { childList: true, subtree: true }); initialContainer = document.querySelector("#blogPostsContainer"); if (initialContainer) { handleBlogPostsContainer(initialContainer); } } monitorForBlogPostsContainer(); } else { indicator.remove(); } });
CSS: .gunindicator { visibility: hidden; opacity: 0; position: absolute; width: 65px !important; height: 65px !important; background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/8f775caa-0441-4725-ac44-b007c4bed482/pistolIcon3.png') no-repeat center center; background-size: contain; transition: all 1s ease; z-index: 99999; }
If your elements are not dynamically added and are present when the DOM is loaded, you won’t need to make use of observers and the code will be very short.
COMMENTS:
Leave a comment: