javascript

exercises

exercises.js
/**
 * 19.4 Resize Observer - Exercises
 * 
 * Practice implementing ResizeObserver patterns
 */

// ============================================
// EXERCISE 1: Container Query Polyfill
// ============================================

/**
 * Create a container query polyfill that:
 * - Applies styles based on container size
 * - Supports multiple breakpoints
 * - Uses data attributes for configuration
 * 
 * Requirements:
 * - Parse breakpoints from data-container-query attribute
 * - Apply classes based on container width
 * - Support min/max width conditions
 */

class ContainerQueryPolyfill {
    // Your implementation here
}

/*
// SOLUTION:
class ContainerQueryPolyfill {
    constructor(container, options = {}) {
        this.container = container;
        this.options = options;
        this.queries = this.parseQueries();
        this.children = [];
        
        this.observer = new ResizeObserver(entries => {
            const width = entries[0].contentRect.width;
            const height = entries[0].contentRect.height;
            this.evaluateQueries(width, height);
        });
        
        this.init();
    }
    
    init() {
        // Find all children with container queries
        this.children = Array.from(
            this.container.querySelectorAll('[data-container-query]')
        ).map(element => ({
            element,
            queries: this.parseElementQueries(element)
        }));
        
        this.observer.observe(this.container);
    }
    
    parseQueries() {
        const queryAttr = this.container.dataset.containerBreakpoints || '';
        const queries = {};
        
        // Parse format: "small:300,medium:600,large:900"
        queryAttr.split(',').forEach(part => {
            const [name, size] = part.split(':');
            if (name && size) {
                queries[name.trim()] = parseInt(size);
            }
        });
        
        return queries;
    }
    
    parseElementQueries(element) {
        const queryAttr = element.dataset.containerQuery || '';
        const queries = [];
        
        // Parse format: "min-width:300:class-name max-width:600:other-class"
        queryAttr.split(/\s+/).forEach(part => {
            const [condition, value, className] = part.split(':');
            if (condition && value && className) {
                queries.push({
                    condition: condition.trim(),
                    value: parseInt(value),
                    className: className.trim()
                });
            }
        });
        
        return queries;
    }
    
    evaluateQueries(width, height) {
        // Set container size custom properties
        this.container.style.setProperty('--container-width', `${width}px`);
        this.container.style.setProperty('--container-height', `${height}px`);
        
        // Apply breakpoint classes to container
        Object.entries(this.queries).forEach(([name, breakpoint]) => {
            const className = `container-${name}`;
            if (width >= breakpoint) {
                this.container.classList.add(className);
            } else {
                this.container.classList.remove(className);
            }
        });
        
        // Evaluate child element queries
        this.children.forEach(({ element, queries }) => {
            queries.forEach(query => {
                const matches = this.evaluateCondition(query.condition, query.value, width, height);
                if (matches) {
                    element.classList.add(query.className);
                } else {
                    element.classList.remove(query.className);
                }
            });
        });
    }
    
    evaluateCondition(condition, value, width, height) {
        switch (condition) {
            case 'min-width': return width >= value;
            case 'max-width': return width <= value;
            case 'min-height': return height >= value;
            case 'max-height': return height <= value;
            case 'width': return width === value;
            case 'height': return height === value;
            default: return false;
        }
    }
    
    addBreakpoint(name, value) {
        this.queries[name] = value;
        const rect = this.container.getBoundingClientRect();
        this.evaluateQueries(rect.width, rect.height);
    }
    
    removeBreakpoint(name) {
        delete this.queries[name];
        this.container.classList.remove(`container-${name}`);
    }
    
    refresh() {
        this.children = Array.from(
            this.container.querySelectorAll('[data-container-query]')
        ).map(element => ({
            element,
            queries: this.parseElementQueries(element)
        }));
        
        const rect = this.container.getBoundingClientRect();
        this.evaluateQueries(rect.width, rect.height);
    }
    
    destroy() {
        this.observer.disconnect();
    }
}
*/


// ============================================
// EXERCISE 2: Adaptive Grid System
// ============================================

/**
 * Create an adaptive grid system that:
 * - Automatically calculates optimal column count
 * - Maintains minimum item width
 * - Handles gaps and padding
 * 
 * Requirements:
 * - Calculate columns based on container width
 * - Support minimum and maximum item widths
 * - Emit events when layout changes
 */

class AdaptiveGrid {
    // Your implementation here
}

/*
// SOLUTION:
class AdaptiveGrid {
    constructor(container, options = {}) {
        this.container = container;
        this.options = {
            minItemWidth: options.minItemWidth || 200,
            maxItemWidth: options.maxItemWidth || 400,
            gap: options.gap || 16,
            padding: options.padding || 16
        };
        
        this.columns = 0;
        this.itemWidth = 0;
        this.listeners = new Map();
        
        this.observer = new ResizeObserver(entries => {
            const width = entries[0].contentRect.width;
            this.calculateLayout(width);
        });
        
        this.init();
    }
    
    init() {
        this.container.style.display = 'grid';
        this.container.style.gap = `${this.options.gap}px`;
        this.container.style.padding = `${this.options.padding}px`;
        
        this.observer.observe(this.container);
    }
    
    calculateLayout(containerWidth) {
        const availableWidth = containerWidth - (this.options.padding * 2);
        
        // Calculate optimal number of columns
        let columns = Math.floor(
            (availableWidth + this.options.gap) / 
            (this.options.minItemWidth + this.options.gap)
        );
        
        // Ensure at least 1 column
        columns = Math.max(1, columns);
        
        // Calculate actual item width
        let itemWidth = (availableWidth - (this.options.gap * (columns - 1))) / columns;
        
        // Clamp to max width if needed
        if (itemWidth > this.options.maxItemWidth && columns > 1) {
            columns--;
            itemWidth = (availableWidth - (this.options.gap * (columns - 1))) / columns;
        }
        
        // Only update if changed
        if (columns !== this.columns || Math.abs(itemWidth - this.itemWidth) > 1) {
            const previousColumns = this.columns;
            this.columns = columns;
            this.itemWidth = itemWidth;
            
            this.applyLayout();
            
            this.emit('layoutChange', {
                columns,
                itemWidth,
                previousColumns,
                containerWidth
            });
        }
    }
    
    applyLayout() {
        this.container.style.gridTemplateColumns = 
            `repeat(${this.columns}, ${this.itemWidth}px)`;
    }
    
    getLayout() {
        return {
            columns: this.columns,
            itemWidth: this.itemWidth,
            gap: this.options.gap,
            padding: this.options.padding
        };
    }
    
    setOptions(options) {
        Object.assign(this.options, options);
        this.container.style.gap = `${this.options.gap}px`;
        this.container.style.padding = `${this.options.padding}px`;
        
        const rect = this.container.getBoundingClientRect();
        this.calculateLayout(rect.width);
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
    
    destroy() {
        this.observer.disconnect();
        this.container.style.display = '';
        this.container.style.gridTemplateColumns = '';
        this.container.style.gap = '';
        this.container.style.padding = '';
    }
}
*/


// ============================================
// EXERCISE 3: Responsive Chart Wrapper
// ============================================

/**
 * Create a responsive chart wrapper that:
 * - Debounces resize events
 * - Scales chart appropriately
 * - Maintains aspect ratio option
 * 
 * Requirements:
 * - Work with any chart library
 * - Support aspect ratio lock
 * - Handle responsive font sizing
 */

class ResponsiveChartWrapper {
    // Your implementation here
}

/*
// SOLUTION:
class ResponsiveChartWrapper {
    constructor(container, chartInstance, options = {}) {
        this.container = container;
        this.chart = chartInstance;
        this.options = {
            aspectRatio: options.aspectRatio || null,
            debounceDelay: options.debounceDelay || 150,
            minWidth: options.minWidth || 200,
            minHeight: options.minHeight || 150,
            responsiveFontSize: options.responsiveFontSize !== false,
            baseFontSize: options.baseFontSize || 12,
            fontSizeRange: options.fontSizeRange || [10, 18]
        };
        
        this.resizeTimeout = null;
        this.currentSize = { width: 0, height: 0 };
        
        this.observer = new ResizeObserver(entries => {
            const { width, height } = entries[0].contentRect;
            this.debouncedResize(width, height);
        });
        
        this.init();
    }
    
    init() {
        // Setup container
        this.container.style.position = 'relative';
        this.container.style.overflow = 'hidden';
        
        this.observer.observe(this.container);
    }
    
    debouncedResize(width, height) {
        if (this.resizeTimeout) {
            clearTimeout(this.resizeTimeout);
        }
        
        this.resizeTimeout = setTimeout(() => {
            this.handleResize(width, height);
        }, this.options.debounceDelay);
    }
    
    handleResize(containerWidth, containerHeight) {
        let width = Math.max(containerWidth, this.options.minWidth);
        let height = containerHeight;
        
        // Apply aspect ratio if set
        if (this.options.aspectRatio) {
            height = width / this.options.aspectRatio;
            
            // If height exceeds container, fit to height instead
            if (height > containerHeight) {
                height = Math.max(containerHeight, this.options.minHeight);
                width = height * this.options.aspectRatio;
            }
        } else {
            height = Math.max(height, this.options.minHeight);
        }
        
        // Skip if no change
        if (Math.abs(width - this.currentSize.width) < 1 && 
            Math.abs(height - this.currentSize.height) < 1) {
            return;
        }
        
        this.currentSize = { width, height };
        
        // Calculate font size
        let fontSize = this.options.baseFontSize;
        if (this.options.responsiveFontSize) {
            fontSize = this.calculateFontSize(width);
        }
        
        // Update chart
        this.updateChart(width, height, fontSize);
    }
    
    calculateFontSize(width) {
        const [minSize, maxSize] = this.options.fontSizeRange;
        const minWidth = this.options.minWidth;
        const maxWidth = 1200;
        
        // Linear interpolation
        const ratio = Math.max(0, Math.min(1, (width - minWidth) / (maxWidth - minWidth)));
        return Math.round(minSize + (maxSize - minSize) * ratio);
    }
    
    updateChart(width, height, fontSize) {
        // Generic chart update - adapt based on library
        if (this.chart.resize) {
            this.chart.resize({ width, height });
        } else if (this.chart.setSize) {
            this.chart.setSize(width, height);
        } else if (this.chart.update) {
            this.chart.update({
                chart: { width, height }
            });
        }
        
        // Update font size if chart supports it
        if (this.chart.setFontSize) {
            this.chart.setFontSize(fontSize);
        }
        
        console.log(`Chart resized to ${width}x${height}, font: ${fontSize}px`);
    }
    
    setAspectRatio(ratio) {
        this.options.aspectRatio = ratio;
        const rect = this.container.getBoundingClientRect();
        this.handleResize(rect.width, rect.height);
    }
    
    getSize() {
        return { ...this.currentSize };
    }
    
    forceResize() {
        const rect = this.container.getBoundingClientRect();
        this.handleResize(rect.width, rect.height);
    }
    
    destroy() {
        if (this.resizeTimeout) {
            clearTimeout(this.resizeTimeout);
        }
        this.observer.disconnect();
    }
}
*/


// ============================================
// EXERCISE 4: Element Size Monitor
// ============================================

/**
 * Create an element size monitoring system:
 * - Track multiple elements
 * - Generate size change events
 * - Provide size history
 * 
 * Requirements:
 * - Support size thresholds for significant changes
 * - Track size over time
 * - Calculate size change velocity
 */

class ElementSizeMonitor {
    // Your implementation here
}

/*
// SOLUTION:
class ElementSizeMonitor {
    constructor(options = {}) {
        this.options = {
            significantChangeThreshold: options.significantChangeThreshold || 10,
            historyLength: options.historyLength || 100,
            sampleInterval: options.sampleInterval || 16
        };
        
        this.elements = new Map();
        this.listeners = new Map();
        
        this.observer = new ResizeObserver(entries => {
            const timestamp = Date.now();
            entries.forEach(entry => this.handleResize(entry, timestamp));
        });
    }
    
    monitor(element, id = null) {
        const elementId = id || element.id || `element-${this.elements.size}`;
        
        this.elements.set(element, {
            id: elementId,
            history: [],
            lastWidth: 0,
            lastHeight: 0,
            velocity: { width: 0, height: 0 },
            isAnimating: false
        });
        
        this.observer.observe(element);
    }
    
    handleResize(entry, timestamp) {
        const data = this.elements.get(entry.target);
        if (!data) return;
        
        const { width, height } = entry.contentRect;
        
        // Calculate change
        const deltaWidth = width - data.lastWidth;
        const deltaHeight = height - data.lastHeight;
        
        // Update history
        const historyEntry = {
            timestamp,
            width,
            height,
            deltaWidth,
            deltaHeight
        };
        
        data.history.push(historyEntry);
        
        // Trim history
        if (data.history.length > this.options.historyLength) {
            data.history.shift();
        }
        
        // Calculate velocity (pixels per second)
        if (data.history.length >= 2) {
            const prev = data.history[data.history.length - 2];
            const timeDelta = (timestamp - prev.timestamp) / 1000;
            if (timeDelta > 0) {
                data.velocity = {
                    width: deltaWidth / timeDelta,
                    height: deltaHeight / timeDelta
                };
            }
        }
        
        // Detect animation
        const isAnimating = Math.abs(data.velocity.width) > 50 || 
                           Math.abs(data.velocity.height) > 50;
        
        if (isAnimating !== data.isAnimating) {
            data.isAnimating = isAnimating;
            this.emit(isAnimating ? 'animationStart' : 'animationEnd', {
                element: entry.target,
                id: data.id
            });
        }
        
        // Emit change events
        const significantChange = 
            Math.abs(deltaWidth) >= this.options.significantChangeThreshold ||
            Math.abs(deltaHeight) >= this.options.significantChangeThreshold;
        
        if (significantChange) {
            this.emit('significantChange', {
                element: entry.target,
                id: data.id,
                width,
                height,
                deltaWidth,
                deltaHeight,
                velocity: data.velocity
            });
        }
        
        this.emit('resize', {
            element: entry.target,
            id: data.id,
            width,
            height,
            deltaWidth,
            deltaHeight
        });
        
        data.lastWidth = width;
        data.lastHeight = height;
    }
    
    getStats(element) {
        const data = this.elements.get(element);
        if (!data) return null;
        
        const history = data.history;
        if (history.length === 0) return null;
        
        const widths = history.map(h => h.width);
        const heights = history.map(h => h.height);
        
        return {
            id: data.id,
            current: {
                width: data.lastWidth,
                height: data.lastHeight
            },
            velocity: data.velocity,
            isAnimating: data.isAnimating,
            stats: {
                minWidth: Math.min(...widths),
                maxWidth: Math.max(...widths),
                avgWidth: widths.reduce((a, b) => a + b, 0) / widths.length,
                minHeight: Math.min(...heights),
                maxHeight: Math.max(...heights),
                avgHeight: heights.reduce((a, b) => a + b, 0) / heights.length
            },
            historyLength: history.length
        };
    }
    
    getHistory(element, limit = null) {
        const data = this.elements.get(element);
        if (!data) return [];
        
        const history = [...data.history];
        return limit ? history.slice(-limit) : history;
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    off(event, callback) {
        const callbacks = this.listeners.get(event);
        if (callbacks) {
            const index = callbacks.indexOf(callback);
            if (index > -1) callbacks.splice(index, 1);
        }
        return this;
    }
    
    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
    
    unmonitor(element) {
        this.observer.unobserve(element);
        this.elements.delete(element);
    }
    
    destroy() {
        this.observer.disconnect();
        this.elements.clear();
        this.listeners.clear();
    }
}
*/


// ============================================
// EXERCISE 5: Fluid Typography System
// ============================================

/**
 * Create a fluid typography system:
 * - Scale fonts based on container width
 * - Support multiple text elements
 * - Handle minimum and maximum sizes
 * 
 * Requirements:
 * - Calculate font sizes using CSS clamp-like logic
 * - Support different scaling for headings vs body
 * - Provide presets for common use cases
 */

class FluidTypography {
    // Your implementation here
}

/*
// SOLUTION:
class FluidTypography {
    constructor(container, options = {}) {
        this.container = container;
        this.options = {
            minWidth: options.minWidth || 320,
            maxWidth: options.maxWidth || 1200,
            baseFontSize: options.baseFontSize || 16,
            scaleRatio: options.scaleRatio || 1.25
        };
        
        this.presets = {
            h1: { minSize: 24, maxSize: 48, lineHeight: 1.1 },
            h2: { minSize: 20, maxSize: 36, lineHeight: 1.2 },
            h3: { minSize: 18, maxSize: 28, lineHeight: 1.3 },
            h4: { minSize: 16, maxSize: 22, lineHeight: 1.4 },
            body: { minSize: 14, maxSize: 18, lineHeight: 1.5 },
            small: { minSize: 12, maxSize: 14, lineHeight: 1.4 },
            ...options.presets
        };
        
        this.elements = [];
        
        this.observer = new ResizeObserver(entries => {
            const width = entries[0].contentRect.width;
            this.updateTypography(width);
        });
        
        this.init();
    }
    
    init() {
        // Find all elements with data-fluid-type attribute
        this.elements = Array.from(
            this.container.querySelectorAll('[data-fluid-type]')
        ).map(element => ({
            element,
            preset: element.dataset.fluidType,
            customConfig: this.parseCustomConfig(element)
        }));
        
        // Also auto-detect headings if enabled
        if (this.options.autoDetect !== false) {
            ['h1', 'h2', 'h3', 'h4', 'p'].forEach(tag => {
                this.container.querySelectorAll(tag).forEach(element => {
                    if (!element.dataset.fluidType) {
                        this.elements.push({
                            element,
                            preset: tag === 'p' ? 'body' : tag,
                            customConfig: null
                        });
                    }
                });
            });
        }
        
        this.observer.observe(this.container);
    }
    
    parseCustomConfig(element) {
        const min = element.dataset.fluidMin;
        const max = element.dataset.fluidMax;
        const lineHeight = element.dataset.fluidLineHeight;
        
        if (min || max || lineHeight) {
            return {
                minSize: min ? parseFloat(min) : null,
                maxSize: max ? parseFloat(max) : null,
                lineHeight: lineHeight ? parseFloat(lineHeight) : null
            };
        }
        
        return null;
    }
    
    updateTypography(containerWidth) {
        // Calculate scale factor (0 to 1)
        const scale = Math.max(0, Math.min(1,
            (containerWidth - this.options.minWidth) / 
            (this.options.maxWidth - this.options.minWidth)
        ));
        
        this.elements.forEach(({ element, preset, customConfig }) => {
            const config = this.getConfig(preset, customConfig);
            if (!config) return;
            
            // Calculate fluid font size
            const fontSize = this.interpolate(
                config.minSize, 
                config.maxSize, 
                scale
            );
            
            // Apply styles
            element.style.fontSize = `${fontSize}px`;
            element.style.lineHeight = String(config.lineHeight);
        });
        
        // Set CSS custom property for width-based calculations
        this.container.style.setProperty('--fluid-scale', scale);
        this.container.style.setProperty('--container-width', `${containerWidth}px`);
    }
    
    getConfig(preset, customConfig) {
        const base = this.presets[preset] || this.presets.body;
        
        if (customConfig) {
            return {
                minSize: customConfig.minSize ?? base.minSize,
                maxSize: customConfig.maxSize ?? base.maxSize,
                lineHeight: customConfig.lineHeight ?? base.lineHeight
            };
        }
        
        return base;
    }
    
    interpolate(min, max, scale) {
        return min + (max - min) * scale;
    }
    
    addPreset(name, config) {
        this.presets[name] = {
            minSize: config.minSize || 14,
            maxSize: config.maxSize || 18,
            lineHeight: config.lineHeight || 1.5
        };
    }
    
    applyTo(element, preset) {
        this.elements.push({
            element,
            preset,
            customConfig: null
        });
        
        const rect = this.container.getBoundingClientRect();
        this.updateTypography(rect.width);
    }
    
    generateCSS() {
        // Generate CSS custom properties for use without JS
        let css = `:root {\n`;
        css += `  --fluid-min-width: ${this.options.minWidth}px;\n`;
        css += `  --fluid-max-width: ${this.options.maxWidth}px;\n\n`;
        
        Object.entries(this.presets).forEach(([name, config]) => {
            css += `  /* ${name} */\n`;
            css += `  --fluid-${name}-min: ${config.minSize}px;\n`;
            css += `  --fluid-${name}-max: ${config.maxSize}px;\n`;
            css += `  --fluid-${name}-lh: ${config.lineHeight};\n\n`;
        });
        
        css += `}\n`;
        
        return css;
    }
    
    destroy() {
        this.observer.disconnect();
        this.elements.forEach(({ element }) => {
            element.style.fontSize = '';
            element.style.lineHeight = '';
        });
    }
}
*/


// ============================================
// TEST UTILITIES
// ============================================

console.log('=== ResizeObserver Exercises ===');
console.log('');
console.log('Exercises:');
console.log('1. ContainerQueryPolyfill - Container-based responsive styles');
console.log('2. AdaptiveGrid - Auto-calculating grid columns');
console.log('3. ResponsiveChartWrapper - Chart library resize handling');
console.log('4. ElementSizeMonitor - Size tracking with velocity');
console.log('5. FluidTypography - Container-based font scaling');
console.log('');
console.log('These exercises require a browser environment.');
console.log('Uncomment solutions to see implementations.');

// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
    module.exports = {
        ContainerQueryPolyfill,
        AdaptiveGrid,
        ResponsiveChartWrapper,
        ElementSizeMonitor,
        FluidTypography
    };
}
Exercises - JavaScript Tutorial | DeepML