javascript

exercises

exercises.js
/**
 * Advanced DOM Traversal - Exercises
 * Practice advanced DOM navigation techniques
 *
 * Note: These exercises are for browser environments.
 * Use the solutions as reference for browser implementations.
 */

// =============================================================================
// EXERCISE 1: TreeWalker Text Finder
// Create a utility to find and manipulate text nodes
// =============================================================================

/*
 * TODO: Create TextFinder class that:
 * - Uses TreeWalker to find text nodes
 * - Supports case-insensitive search
 * - Can highlight found text
 * - Can replace text content
 * - Provides navigation between matches
 */

class TextFinder {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class TextFinder {
    constructor(root = document.body) {
        this.root = root;
        this.matches = [];
        this.currentIndex = -1;
        this.highlightClass = 'text-finder-highlight';
    }
    
    find(searchText, options = {}) {
        const { caseSensitive = false, wholeWord = false } = options;
        
        this.clearHighlights();
        this.matches = [];
        this.currentIndex = -1;
        
        const walker = document.createTreeWalker(
            this.root,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: (node) => {
                    const text = caseSensitive 
                        ? node.textContent 
                        : node.textContent.toLowerCase();
                    const search = caseSensitive 
                        ? searchText 
                        : searchText.toLowerCase();
                    
                    if (wholeWord) {
                        const regex = new RegExp(`\\b${search}\\b`);
                        return regex.test(text)
                            ? NodeFilter.FILTER_ACCEPT
                            : NodeFilter.FILTER_REJECT;
                    }
                    
                    return text.includes(search)
                        ? NodeFilter.FILTER_ACCEPT
                        : NodeFilter.FILTER_REJECT;
                }
            }
        );
        
        while (walker.nextNode()) {
            this.matches.push(walker.currentNode);
        }
        
        return this.matches.length;
    }
    
    highlightAll() {
        this.matches.forEach((node, index) => {
            this.highlightNode(node, index);
        });
    }
    
    highlightNode(textNode, index) {
        const parent = textNode.parentElement;
        const text = textNode.textContent;
        
        const span = document.createElement('span');
        span.className = this.highlightClass;
        span.dataset.matchIndex = index;
        span.textContent = text;
        
        parent.replaceChild(span, textNode);
        this.matches[index] = span;
    }
    
    clearHighlights() {
        document.querySelectorAll('.' + this.highlightClass).forEach(span => {
            const textNode = document.createTextNode(span.textContent);
            span.parentNode.replaceChild(textNode, span);
        });
    }
    
    next() {
        if (this.matches.length === 0) return null;
        
        this.currentIndex = (this.currentIndex + 1) % this.matches.length;
        return this.scrollToMatch(this.currentIndex);
    }
    
    previous() {
        if (this.matches.length === 0) return null;
        
        this.currentIndex = this.currentIndex <= 0 
            ? this.matches.length - 1 
            : this.currentIndex - 1;
        return this.scrollToMatch(this.currentIndex);
    }
    
    scrollToMatch(index) {
        const match = this.matches[index];
        if (match) {
            match.scrollIntoView({ behavior: 'smooth', block: 'center' });
            return match;
        }
        return null;
    }
    
    replace(replacement) {
        if (this.currentIndex >= 0 && this.matches[this.currentIndex]) {
            const match = this.matches[this.currentIndex];
            match.textContent = replacement;
            this.matches.splice(this.currentIndex, 1);
            this.currentIndex--;
        }
    }
    
    replaceAll(replacement) {
        this.matches.forEach(match => {
            if (match.nodeType === Node.TEXT_NODE) {
                match.textContent = replacement;
            } else {
                match.textContent = replacement;
            }
        });
        this.matches = [];
        this.currentIndex = -1;
    }
}
*/

// =============================================================================
// EXERCISE 2: DOM Path Generator
// Generate unique selectors for elements
// =============================================================================

/*
 * TODO: Create generatePath(element) function that:
 * - Creates unique CSS selector path to element
 * - Uses IDs when available
 * - Falls back to nth-child for ambiguous elements
 * - Returns shortest possible path
 */

function generatePath(element) {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
function generatePath(element) {
    if (!element || element === document.documentElement) {
        return 'html';
    }
    
    // If element has ID, use it
    if (element.id) {
        return '#' + element.id;
    }
    
    const path = [];
    let current = element;
    
    while (current && current !== document.documentElement) {
        let selector = current.tagName.toLowerCase();
        
        // Check if ID exists
        if (current.id) {
            path.unshift('#' + current.id);
            break;
        }
        
        // Add nth-child if needed for uniqueness
        const parent = current.parentElement;
        if (parent) {
            const siblings = Array.from(parent.children)
                .filter(child => child.tagName === current.tagName);
            
            if (siblings.length > 1) {
                const index = siblings.indexOf(current) + 1;
                selector += ':nth-of-type(' + index + ')';
            }
        }
        
        path.unshift(selector);
        current = current.parentElement;
    }
    
    return path.join(' > ');
}
*/

// =============================================================================
// EXERCISE 3: Range Selection Helper
// Create a helper for working with text selections
// =============================================================================

/*
 * TODO: Create SelectionHelper class that:
 * - Gets selected text and range
 * - Wraps selection in a specified element
 * - Expands selection to word/sentence/paragraph
 * - Saves and restores selections
 */

class SelectionHelper {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class SelectionHelper {
    constructor() {
        this.savedSelections = [];
    }
    
    getSelection() {
        return window.getSelection();
    }
    
    getSelectedText() {
        return this.getSelection().toString();
    }
    
    getSelectedRange() {
        const selection = this.getSelection();
        if (selection.rangeCount > 0) {
            return selection.getRangeAt(0);
        }
        return null;
    }
    
    wrapSelection(tagName, attributes = {}) {
        const range = this.getSelectedRange();
        if (!range || range.collapsed) return null;
        
        const wrapper = document.createElement(tagName);
        Object.entries(attributes).forEach(([key, value]) => {
            wrapper.setAttribute(key, value);
        });
        
        try {
            range.surroundContents(wrapper);
            return wrapper;
        } catch (e) {
            // Handle partial selection across elements
            const contents = range.extractContents();
            wrapper.appendChild(contents);
            range.insertNode(wrapper);
            return wrapper;
        }
    }
    
    expandToWord() {
        const selection = this.getSelection();
        if (selection.rangeCount === 0) return;
        
        // Use modify method if available
        if (selection.modify) {
            selection.modify('extend', 'backward', 'word');
            selection.modify('extend', 'forward', 'word');
        }
    }
    
    expandToSentence() {
        const range = this.getSelectedRange();
        if (!range) return;
        
        const text = range.startContainer.textContent;
        let start = range.startOffset;
        let end = range.endOffset;
        
        // Find sentence start
        while (start > 0 && !/[.!?]/.test(text[start - 1])) {
            start--;
        }
        
        // Find sentence end
        while (end < text.length && !/[.!?]/.test(text[end])) {
            end++;
        }
        if (end < text.length) end++; // Include punctuation
        
        range.setStart(range.startContainer, start);
        range.setEnd(range.startContainer, end);
    }
    
    saveSelection() {
        const range = this.getSelectedRange();
        if (range) {
            this.savedSelections.push({
                startContainer: range.startContainer,
                startOffset: range.startOffset,
                endContainer: range.endContainer,
                endOffset: range.endOffset
            });
        }
    }
    
    restoreSelection(index = -1) {
        const saved = index === -1 
            ? this.savedSelections.pop() 
            : this.savedSelections[index];
        
        if (!saved) return false;
        
        const range = document.createRange();
        range.setStart(saved.startContainer, saved.startOffset);
        range.setEnd(saved.endContainer, saved.endOffset);
        
        const selection = this.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
        
        return true;
    }
    
    clearSelection() {
        this.getSelection().removeAllRanges();
    }
}
*/

// =============================================================================
// EXERCISE 4: Element Finder
// Create advanced element finding utilities
// =============================================================================

/*
 * TODO: Create ElementFinder class with methods:
 * - byVisibility(visible: boolean) - find visible/hidden elements
 * - byZIndex(minZ, maxZ) - find by z-index range
 * - byOverlap(element) - find elements overlapping with given element
 * - byTextContent(text, exact) - find by text content
 * - byComputedStyle(property, value) - find by computed style
 */

class ElementFinder {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class ElementFinder {
    constructor(root = document.body) {
        this.root = root;
    }
    
    getAllElements() {
        return Array.from(this.root.querySelectorAll('*'));
    }
    
    byVisibility(visible = true) {
        return this.getAllElements().filter(el => {
            const style = getComputedStyle(el);
            const isVisible = 
                style.display !== 'none' &&
                style.visibility !== 'hidden' &&
                style.opacity !== '0' &&
                el.offsetParent !== null;
            
            return visible ? isVisible : !isVisible;
        });
    }
    
    byZIndex(minZ = -Infinity, maxZ = Infinity) {
        return this.getAllElements().filter(el => {
            const style = getComputedStyle(el);
            const zIndex = parseInt(style.zIndex);
            
            if (isNaN(zIndex)) return false;
            return zIndex >= minZ && zIndex <= maxZ;
        });
    }
    
    byOverlap(targetElement) {
        const targetRect = targetElement.getBoundingClientRect();
        
        return this.getAllElements().filter(el => {
            if (el === targetElement) return false;
            
            const rect = el.getBoundingClientRect();
            
            return !(
                rect.right < targetRect.left ||
                rect.left > targetRect.right ||
                rect.bottom < targetRect.top ||
                rect.top > targetRect.bottom
            );
        });
    }
    
    byTextContent(text, exact = false) {
        return this.getAllElements().filter(el => {
            const content = el.textContent.trim();
            
            if (exact) {
                return content === text;
            }
            
            return content.toLowerCase().includes(text.toLowerCase());
        });
    }
    
    byComputedStyle(property, value) {
        return this.getAllElements().filter(el => {
            const style = getComputedStyle(el);
            return style[property] === value;
        });
    }
    
    byAttribute(name, value = null) {
        if (value === null) {
            return this.getAllElements().filter(el => el.hasAttribute(name));
        }
        return this.getAllElements().filter(el => 
            el.getAttribute(name) === value
        );
    }
    
    inViewport() {
        const viewport = {
            top: 0,
            left: 0,
            bottom: window.innerHeight,
            right: window.innerWidth
        };
        
        return this.getAllElements().filter(el => {
            const rect = el.getBoundingClientRect();
            return (
                rect.top < viewport.bottom &&
                rect.bottom > viewport.top &&
                rect.left < viewport.right &&
                rect.right > viewport.left
            );
        });
    }
}
*/

// =============================================================================
// EXERCISE 5: DOM Diff Utility
// Compare two DOM structures
// =============================================================================

/*
 * TODO: Create DOMDiff class that:
 * - Compares two DOM elements/subtrees
 * - Identifies added, removed, changed nodes
 * - Tracks attribute changes
 * - Returns patch operations
 */

class DOMDiff {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class DOMDiff {
    diff(oldNode, newNode) {
        const patches = [];
        this.walkAndDiff(oldNode, newNode, patches, []);
        return patches;
    }
    
    walkAndDiff(oldNode, newNode, patches, path) {
        // Node removed
        if (!newNode) {
            patches.push({
                type: 'REMOVE',
                path: [...path],
                oldNode
            });
            return;
        }
        
        // Node added
        if (!oldNode) {
            patches.push({
                type: 'ADD',
                path: [...path],
                newNode
            });
            return;
        }
        
        // Different node types
        if (oldNode.nodeType !== newNode.nodeType) {
            patches.push({
                type: 'REPLACE',
                path: [...path],
                oldNode,
                newNode
            });
            return;
        }
        
        // Text node changed
        if (oldNode.nodeType === Node.TEXT_NODE) {
            if (oldNode.textContent !== newNode.textContent) {
                patches.push({
                    type: 'TEXT',
                    path: [...path],
                    oldValue: oldNode.textContent,
                    newValue: newNode.textContent
                });
            }
            return;
        }
        
        // Different elements
        if (oldNode.tagName !== newNode.tagName) {
            patches.push({
                type: 'REPLACE',
                path: [...path],
                oldNode,
                newNode
            });
            return;
        }
        
        // Check attributes
        this.diffAttributes(oldNode, newNode, patches, path);
        
        // Check children
        const oldChildren = Array.from(oldNode.childNodes);
        const newChildren = Array.from(newNode.childNodes);
        const maxLength = Math.max(oldChildren.length, newChildren.length);
        
        for (let i = 0; i < maxLength; i++) {
            this.walkAndDiff(
                oldChildren[i],
                newChildren[i],
                patches,
                [...path, i]
            );
        }
    }
    
    diffAttributes(oldEl, newEl, patches, path) {
        const oldAttrs = this.getAttributes(oldEl);
        const newAttrs = this.getAttributes(newEl);
        
        // Check for changed or removed attributes
        for (const [name, value] of Object.entries(oldAttrs)) {
            if (!(name in newAttrs)) {
                patches.push({
                    type: 'REMOVE_ATTR',
                    path: [...path],
                    name
                });
            } else if (newAttrs[name] !== value) {
                patches.push({
                    type: 'SET_ATTR',
                    path: [...path],
                    name,
                    oldValue: value,
                    newValue: newAttrs[name]
                });
            }
        }
        
        // Check for new attributes
        for (const [name, value] of Object.entries(newAttrs)) {
            if (!(name in oldAttrs)) {
                patches.push({
                    type: 'SET_ATTR',
                    path: [...path],
                    name,
                    newValue: value
                });
            }
        }
    }
    
    getAttributes(element) {
        const attrs = {};
        for (const attr of element.attributes || []) {
            attrs[attr.name] = attr.value;
        }
        return attrs;
    }
    
    apply(root, patches) {
        for (const patch of patches) {
            const target = this.getNodeByPath(root, patch.path);
            
            switch (patch.type) {
                case 'REMOVE':
                    target.parentNode.removeChild(target);
                    break;
                case 'ADD':
                    // Handle add
                    break;
                case 'REPLACE':
                    target.parentNode.replaceChild(
                        patch.newNode.cloneNode(true),
                        target
                    );
                    break;
                case 'TEXT':
                    target.textContent = patch.newValue;
                    break;
                case 'SET_ATTR':
                    target.setAttribute(patch.name, patch.newValue);
                    break;
                case 'REMOVE_ATTR':
                    target.removeAttribute(patch.name);
                    break;
            }
        }
    }
    
    getNodeByPath(root, path) {
        let node = root;
        for (const index of path) {
            node = node.childNodes[index];
        }
        return node;
    }
}
*/

// =============================================================================
// EXERCISE 6: Shadow DOM Query
// Create utilities for querying across shadow boundaries
// =============================================================================

/*
 * TODO: Create ShadowQuery class that:
 * - Queries elements across shadow DOM boundaries
 * - Supports CSS selectors
 * - Can traverse into open shadow roots
 * - Returns flat list of matching elements
 */

class ShadowQuery {
  // YOUR CODE HERE:
}

// SOLUTION:
/*
class ShadowQuery {
    static query(selector, root = document) {
        const results = [];
        
        // Query current context
        results.push(...root.querySelectorAll(selector));
        
        // Find all elements with shadow roots
        const walker = document.createTreeWalker(
            root,
            NodeFilter.SHOW_ELEMENT,
            null
        );
        
        while (walker.nextNode()) {
            const node = walker.currentNode;
            if (node.shadowRoot) {
                results.push(...ShadowQuery.query(selector, node.shadowRoot));
            }
        }
        
        return results;
    }
    
    static queryOne(selector, root = document) {
        // Check current context first
        const result = root.querySelector(selector);
        if (result) return result;
        
        // Search in shadow roots
        const walker = document.createTreeWalker(
            root,
            NodeFilter.SHOW_ELEMENT,
            null
        );
        
        while (walker.nextNode()) {
            const node = walker.currentNode;
            if (node.shadowRoot) {
                const shadowResult = ShadowQuery.queryOne(selector, node.shadowRoot);
                if (shadowResult) return shadowResult;
            }
        }
        
        return null;
    }
    
    static closest(element, selector) {
        let current = element;
        
        while (current) {
            if (current.matches && current.matches(selector)) {
                return current;
            }
            
            if (current.parentElement) {
                current = current.parentElement;
            } else if (current.host) {
                // Cross shadow boundary
                current = current.host;
            } else {
                current = null;
            }
        }
        
        return null;
    }
    
    static getPath(element) {
        const path = [];
        let current = element;
        
        while (current) {
            path.unshift(current);
            
            if (current.parentElement) {
                current = current.parentElement;
            } else if (current.host) {
                path.unshift(current.host.shadowRoot);
                current = current.host;
            } else {
                current = null;
            }
        }
        
        return path;
    }
}
*/

// =============================================================================
// TEST YOUR IMPLEMENTATIONS
// =============================================================================

function runTests() {
  console.log('Advanced DOM Traversal - Exercises');
  console.log('==================================');
  console.log('These exercises require a browser environment.');
  console.log('Use the solutions as reference for implementation.');

  // Test structure
  console.log('\nTest your implementations in a browser with:');
  console.log('1. Create an HTML file with test elements');
  console.log('2. Include this script');
  console.log('3. Test each class/function in console');
}

// Run tests
runTests();
Exercises - JavaScript Tutorial | DeepML