Highlight Text While Scrolling

Here’s the code from the video.

document.addEventListener('DOMContentLoaded', () => {
    // Configuration object with excluded terms
    const config = {
        excludedTerms: [
            'Key takeaways',
            // Add more terms here as needed
            // Example:
            // 'Summary',
            // 'Table of Contents',
            // etc.
        ]
    };

    // Rest of the styles remain the same
    const style = document.createElement('style');
    style.textContent = `
        @keyframes highlightMove {
            0% {
                background-size: 0% 100%;
                background-position: 0 0;
            }
            20% {
                background-size: 35% 100%;
                background-position: 0 0;
            }
            60% {
                background-size: 65% 100%;
                background-position: 0 0;
            }
            100% {
                background-size: 100% 100%;
                background-position: 0 0;
            }
        }

        .highlight-line {
            display: inline;
            padding: 3px 0;
            background-image: 
                repeating-linear-gradient(
                    -45deg,
                    rgba(255, 255, 0, 0) 0,
                    rgba(255, 255, 0, 0) 1px,
                    rgba(255, 255, 0, 0.5) 1px,
                    rgba(255, 255, 0, 0.5) 2px
                ),
                linear-gradient(
                    90deg,
                    rgba(255, 255, 0, 0.5) 0%,
                    rgba(255, 255, 0, 0.6) 15%,
                    rgba(255, 255, 0, 0.5) 85%,
                    rgba(255, 255, 0, 0.4) 100%
                );
            background-repeat: no-repeat;
            background-size: 0% 100%;
            background-position: 0 0;
            transition: none;
        }

        .highlight-line.active {
            animation: highlightMove 1.2s cubic-bezier(0.215, 0.610, 0.355, 1.000);
            animation-fill-mode: forwards;
        }

        .highlight-line::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: repeating-linear-gradient(
                -45deg,
                transparent 0,
                transparent 2px,
                rgba(255, 255, 0, 0.1) 2px,
                rgba(255, 255, 0, 0.1) 3px
            );
            mix-blend-mode: multiply;
            pointer-events: none;
        }
    `;
    document.head.appendChild(style);

    const processedPositions = new Map();
    const highlightedElements = new Set();

    function shouldHighlight(element) {
        const text = element.textContent.trim();
        return !config.excludedTerms.some(term => 
            text.toLowerCase() === term.toLowerCase()
        );
    }

    function getPositionSignature(element) {
        const rect = element.getBoundingClientRect();
        return `${rect.top}-${rect.left}-${element.textContent.trim()}`;
    }

    function wrapTextInHighlights(element) {
        // Skip if text is in excluded list
        if (!shouldHighlight(element)) {
            return;
        }

        const positionKey = getPositionSignature(element);
        
        if (processedPositions.has(positionKey)) {
            element.remove();
            return;
        }
        
        processedPositions.set(positionKey, true);
        
        const span = document.createElement('span');
        span.className = 'highlight-line';
        span.style.position = 'relative';
        
        while (element.firstChild) {
            span.appendChild(element.firstChild);
        }
        
        element.appendChild(span);
    }

    // Rest of the code remains the same...
    function isInViewport(element) {
        const rect = element.getBoundingClientRect();
        const buffer = window.innerHeight / 2;
        return (
            rect.top >= -buffer &&
            rect.top <= (window.innerHeight + buffer)
        );
    }

    let animationQueue = [];
    let isAnimating = false;

    async function animateHighlight(element) {
        if (highlightedElements.has(element)) return;
        
        element.classList.add('active');
        highlightedElements.add(element);
        await new Promise(resolve => setTimeout(resolve, 1200));
    }

    async function processQueue() {
        if (isAnimating || animationQueue.length === 0) return;
        
        isAnimating = true;
        
        while (animationQueue.length > 0) {
            const element = animationQueue.shift();
            if (!highlightedElements.has(element)) {
                await animateHighlight(element);
                await new Promise(resolve => setTimeout(resolve, 200));
            }
        }
        
        isAnimating = false;
    }

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting && !highlightedElements.has(entry.target)) {
                animationQueue.push(entry.target);
                processQueue();
            }
        });
    }, {
        root: null,
        rootMargin: '50px 0px',
        threshold: 0.1
    });

    setTimeout(() => {
        const boldElements = document.querySelectorAll('.entry-content p strong');
        boldElements.forEach(wrapTextInHighlights);

        document.querySelectorAll('.highlight-line').forEach(element => {
            observer.observe(element);
        });

        const initialElements = Array.from(document.querySelectorAll('.highlight-line'))
            .filter(element => isInViewport(element) && !highlightedElements.has(element));
        animationQueue.push(...initialElements);
        processQueue();
    }, 100);
});

Final thoughts

That concludes this article. What do you think? Let me know in the comments below (I read and reply to every comment). If you found this helpful, check out my full blog and subscribe to my YouTube channel. Thanks for reading!

Leave a Comment