DOM API Polyfills and Extensions

1. querySelector and querySelectorAll Polyfills

Method Syntax Returns Browser Support
querySelector element.querySelector(selector) First matching element or null IE8+
querySelectorAll element.querySelectorAll(selector) Static NodeList of matching elements IE8+
document.querySelector document.querySelector(selector) First matching element in document Most common usage
Fallback: getElementById document.getElementById(id) Element with specific ID IE5+, universal support
Fallback: getElementsByClassName element.getElementsByClassName(name) Live HTMLCollection IE9+ (polyfillable for IE8)

Example: querySelector polyfill for very old browsers

// querySelector polyfill (basic - uses Sizzle or native methods)
if (!document.querySelector) {
    document.querySelector = function(selector) {
        // Simple ID selector
        if (selector.charAt(0) === '#') {
            return document.getElementById(selector.slice(1));
        }
        
        // Simple class selector
        if (selector.charAt(0) === '.') {
            var elements = document.getElementsByClassName ?
                document.getElementsByClassName(selector.slice(1)) :
                document.getElementsByTagName('*');
            return elements[0] || null;
        }
        
        // Simple tag selector
        var elements = document.getElementsByTagName(selector);
        return elements[0] || null;
    };
}

// querySelectorAll polyfill (basic)
if (!document.querySelectorAll) {
    document.querySelectorAll = function(selector) {
        var style = document.createElement('style');
        var elements = [];
        var element;
        
        document.documentElement.firstChild.appendChild(style);
        document._qsa = [];
        
        style.styleSheet.cssText = selector + '{x-qsa:expression(document._qsa && document._qsa.push(this))}';
        window.scrollBy(0, 0);
        
        while (document._qsa.length) {
            element = document._qsa.shift();
            element.style.removeAttribute('x-qsa');
            elements.push(element);
        }
        
        document.documentElement.firstChild.removeChild(style);
        return elements;
    };
}

// Modern approach: Use Sizzle library for complex selectors
// https://github.com/jquery/sizzle

Example: getElementsByClassName polyfill for IE8

// getElementsByClassName polyfill
if (!document.getElementsByClassName) {
    document.getElementsByClassName = function(className) {
        return document.querySelectorAll('.' + className);
    };
    
    Element.prototype.getElementsByClassName = function(className) {
        return this.querySelectorAll('.' + className);
    };
}

// Alternative implementation without querySelectorAll
if (!document.getElementsByClassName) {
    document.getElementsByClassName = function(className) {
        var elements = document.getElementsByTagName('*');
        var result = [];
        var pattern = new RegExp('(^|\\s)' + className + '(\\s|$)');
        
        for (var i = 0; i < elements.length; i++) {
            if (pattern.test(elements[i].className)) {
                result.push(elements[i]);
            }
        }
        return result;
    };
}
Note: For complex CSS selector support in legacy browsers, use Sizzle library (jQuery's selector engine) or limit to simple selectors (ID, class, tag).

2. Element.classList and className Manipulation

Method Syntax Description Returns
classList.add element.classList.add('class1', 'class2') Adds one or more classes undefined
classList.remove element.classList.remove('class1') Removes one or more classes undefined
classList.toggle element.classList.toggle('class') Toggles class presence Boolean - true if added
classList.contains element.classList.contains('class') Checks if class exists Boolean
classList.replace element.classList.replace('old', 'new') Replaces one class with another Boolean - true if replaced
classList.item element.classList.item(index) Returns class at index String or null

Example: classList polyfill for IE9

// classList polyfill
if (!('classList' in document.createElement('_'))) {
    (function(view) {
        'use strict';
        
        if (!('Element' in view)) return;
        
        var classListProp = 'classList',
            protoProp = 'prototype',
            elemCtrProto = view.Element[protoProp],
            objCtr = Object,
            strTrim = String[protoProp].trim || function() {
                return this.replace(/^\s+|\s+$/g, '');
            },
            arrIndexOf = Array[protoProp].indexOf || function(item) {
                var i = 0, len = this.length;
                for (; i < len; i++) {
                    if (i in this && this[i] === item) {
                        return i;
                    }
                }
                return -1;
            };
        
        var DOMTokenList = function(elem) {
            var classes = strTrim.call(elem.getAttribute('class') || '');
            var tokens = classes ? classes.split(/\s+/) : [];
            var i = 0;
            var len = tokens.length;
            
            for (; i < len; i++) {
                this.push(tokens[i]);
            }
            
            this._updateClassName = function() {
                elem.setAttribute('class', this.toString());
            };
        };
        
        var classListProto = DOMTokenList[protoProp] = [];
        
        classListProto.item = function(i) {
            return this[i] || null;
        };
        
        classListProto.contains = function(token) {
            token += '';
            return arrIndexOf.call(this, token) !== -1;
        };
        
        classListProto.add = function() {
            var tokens = arguments;
            var i = 0;
            var len = tokens.length;
            var token;
            var updated = false;
            
            do {
                token = tokens[i] + '';
                if (arrIndexOf.call(this, token) === -1) {
                    this.push(token);
                    updated = true;
                }
            } while (++i < len);
            
            if (updated) {
                this._updateClassName();
            }
        };
        
        classListProto.remove = function() {
            var tokens = arguments;
            var i = 0;
            var len = tokens.length;
            var token;
            var updated = false;
            var index;
            
            do {
                token = tokens[i] + '';
                index = arrIndexOf.call(this, token);
                while (index !== -1) {
                    this.splice(index, 1);
                    updated = true;
                    index = arrIndexOf.call(this, token);
                }
            } while (++i < len);
            
            if (updated) {
                this._updateClassName();
            }
        };
        
        classListProto.toggle = function(token, force) {
            token += '';
            var result = this.contains(token);
            var method = result ? force !== true && 'remove' : force !== false && 'add';
            
            if (method) {
                this[method](token);
            }
            
            return (force === true || force === false) ? force : !result;
        };
        
        classListProto.toString = function() {
            return this.join(' ');
        };
        
        // Expose as property
        if (objCtr.defineProperty) {
            var classListPropDesc = {
                get: function() {
                    return new DOMTokenList(this);
                },
                enumerable: true,
                configurable: true
            };
            try {
                objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
            } catch(ex) {
                // IE8 doesn't support enumerable: true
                objCtr.defineProperty(elemCtrProto, classListProp, {
                    get: classListPropDesc.get
                });
            }
        }
    }(window));
}
Note: classList is supported in IE10+. For IE9 and below, this polyfill provides full API compatibility including multiple class names in add/remove.

3. addEventListener and Event Model Polyfills

Method Standard IE Legacy Key Difference
Add Listener addEventListener(type, fn, capture) attachEvent('on'+type, fn) Event name prefix, this context
Remove Listener removeEventListener(type, fn, capture) detachEvent('on'+type, fn) Must use same function reference
Event Object Passed as parameter window.event Access method differs
Prevent Default event.preventDefault() event.returnValue = false Different property
Stop Propagation event.stopPropagation() event.cancelBubble = true Different property
Event Target event.target event.srcElement Different property name

Example: addEventListener polyfill for IE8

// addEventListener polyfill for IE8
if (!Element.prototype.addEventListener) {
    Element.prototype.addEventListener = function(type, fn, capture) {
        var element = this;
        
        // Create wrapper to fix context and event object
        var wrapper = function(e) {
            e = e || window.event;
            
            // Fix event object
            e.target = e.target || e.srcElement;
            e.currentTarget = element;
            e.preventDefault = e.preventDefault || function() {
                e.returnValue = false;
            };
            e.stopPropagation = e.stopPropagation || function() {
                e.cancelBubble = true;
            };
            
            // Call with correct context
            return fn.call(element, e);
        };
        
        // Store wrapper for removal
        if (!element._eventListeners) {
            element._eventListeners = {};
        }
        if (!element._eventListeners[type]) {
            element._eventListeners[type] = [];
        }
        element._eventListeners[type].push({
            listener: fn,
            wrapper: wrapper
        });
        
        // Attach event
        element.attachEvent('on' + type, wrapper);
    };
    
    Element.prototype.removeEventListener = function(type, fn, capture) {
        if (!this._eventListeners || !this._eventListeners[type]) {
            return;
        }
        
        var listeners = this._eventListeners[type];
        for (var i = 0; i < listeners.length; i++) {
            if (listeners[i].listener === fn) {
                this.detachEvent('on' + type, listeners[i].wrapper);
                listeners.splice(i, 1);
                break;
            }
        }
    };
    
    // Also add to Window and Document
    if (!window.addEventListener) {
        Window.prototype.addEventListener = Element.prototype.addEventListener;
        Window.prototype.removeEventListener = Element.prototype.removeEventListener;
        Document.prototype.addEventListener = Element.prototype.addEventListener;
        Document.prototype.removeEventListener = Element.prototype.removeEventListener;
    }
}

// Event object normalization
function normalizeEvent(e) {
    e = e || window.event;
    
    if (!e.target) e.target = e.srcElement;
    if (!e.currentTarget) e.currentTarget = this;
    if (!e.relatedTarget) e.relatedTarget = e.fromElement || e.toElement;
    if (!e.which) e.which = e.charCode || e.keyCode;
    
    if (!e.preventDefault) {
        e.preventDefault = function() {
            e.returnValue = false;
        };
    }
    
    if (!e.stopPropagation) {
        e.stopPropagation = function() {
            e.cancelBubble = true;
        };
    }
    
    return e;
}
Warning: IE8's attachEvent executes handlers in reverse order and with different this context (window instead of element). The polyfill fixes these issues.

4. CustomEvent and Event Constructor Polyfills

Constructor Syntax Properties Use Case
Event new Event(type, options) bubbles, cancelable, composed Basic event creation
CustomEvent new CustomEvent(type, options) Event properties + detail Events with custom data
MouseEvent new MouseEvent(type, options) Mouse-specific properties Simulate mouse interactions
KeyboardEvent new KeyboardEvent(type, options) Key, keyCode, modifiers Simulate keyboard input
Legacy: createEvent document.createEvent('Event') initEvent required IE9-11 fallback

Example: CustomEvent polyfill

// CustomEvent polyfill
(function() {
    if (typeof window.CustomEvent === 'function') return false;
    
    function CustomEvent(event, params) {
        params = params || { bubbles: false, cancelable: false, detail: null };
        var evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        return evt;
    }
    
    window.CustomEvent = CustomEvent;
})();

// Event constructor polyfill
(function() {
    if (typeof window.Event === 'function') return false;
    
    function Event(event, params) {
        params = params || { bubbles: false, cancelable: false };
        var evt = document.createEvent('Event');
        evt.initEvent(event, params.bubbles, params.cancelable);
        return evt;
    }
    
    window.Event = Event;
})();

// Usage examples
var customEvent = new CustomEvent('myevent', {
    detail: { message: 'Hello World' },
    bubbles: true,
    cancelable: true
});

element.dispatchEvent(customEvent);

// Listen for custom event
element.addEventListener('myevent', function(e) {
    console.log(e.detail.message);  // 'Hello World'
});

Example: MouseEvent and KeyboardEvent polyfills

// MouseEvent polyfill
(function() {
    try {
        new MouseEvent('test');
        return false;
    } catch(e) {}
    
    var MouseEventPolyfill = function(eventType, params) {
        params = params || { bubbles: false, cancelable: false };
        var mouseEvent = document.createEvent('MouseEvent');
        mouseEvent.initMouseEvent(
            eventType,
            params.bubbles,
            params.cancelable,
            window,
            params.detail || 0,
            params.screenX || 0,
            params.screenY || 0,
            params.clientX || 0,
            params.clientY || 0,
            params.ctrlKey || false,
            params.altKey || false,
            params.shiftKey || false,
            params.metaKey || false,
            params.button || 0,
            params.relatedTarget || null
        );
        return mouseEvent;
    };
    
    MouseEventPolyfill.prototype = Event.prototype;
    window.MouseEvent = MouseEventPolyfill;
})();

// KeyboardEvent polyfill
(function() {
    try {
        new KeyboardEvent('test');
        return false;
    } catch(e) {}
    
    var KeyboardEventPolyfill = function(eventType, params) {
        params = params || { bubbles: false, cancelable: false };
        var keyboardEvent = document.createEvent('KeyboardEvent');
        var initMethod = keyboardEvent.initKeyboardEvent || keyboardEvent.initKeyEvent;
        
        if (initMethod) {
            initMethod.call(
                keyboardEvent,
                eventType,
                params.bubbles,
                params.cancelable,
                window,
                params.key || '',
                params.location || 0,
                params.ctrlKey || false,
                params.altKey || false,
                params.shiftKey || false,
                params.metaKey || false
            );
        }
        
        return keyboardEvent;
    };
    
    KeyboardEventPolyfill.prototype = Event.prototype;
    window.KeyboardEvent = KeyboardEventPolyfill;
})();

5. Element.matches and Selector API Extensions

Method Standard Name Vendor Prefixes Purpose
matches element.matches(selector) matchesSelector, msMatchesSelector Test if element matches selector
closest element.closest(selector) No prefix needed Find nearest ancestor matching selector
Vendor Prefixes - webkit, moz, ms, o Browser-specific implementations

Example: Element.matches polyfill with vendor prefixes

// Element.matches polyfill
if (!Element.prototype.matches) {
    Element.prototype.matches =
        Element.prototype.matchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector ||
        Element.prototype.oMatchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        function(s) {
            var matches = (this.document || this.ownerDocument).querySelectorAll(s);
            var i = matches.length;
            while (--i >= 0 && matches.item(i) !== this) {}
            return i > -1;
        };
}

// Usage
if (element.matches('.active')) {
    console.log('Element is active');
}

if (element.matches('div.container > p')) {
    console.log('Element matches selector');
}

Example: Element.closest polyfill

// Element.closest polyfill
if (!Element.prototype.closest) {
    Element.prototype.closest = function(s) {
        var el = this;
        
        do {
            if (Element.prototype.matches.call(el, s)) {
                return el;
            }
            el = el.parentElement || el.parentNode;
        } while (el !== null && el.nodeType === 1);
        
        return null;
    };
}

// Usage
var button = document.querySelector('button');
var form = button.closest('form');
var container = button.closest('.container');

// Event delegation with closest
document.addEventListener('click', function(e) {
    var button = e.target.closest('button');
    if (button) {
        console.log('Button clicked:', button);
    }
});
Note: closest is extremely useful for event delegation. It traverses up the DOM tree until it finds a matching element or reaches the document root.

6. Node.contains and DOM Traversal Methods

Method Syntax Returns Use Case
contains node.contains(otherNode) Boolean - true if descendant Check if node is inside another
compareDocumentPosition node.compareDocumentPosition(other) Bitmask of position relationship Detailed position comparison
childElementCount element.childElementCount Number of child elements Count element children (not text nodes)
firstElementChild element.firstElementChild First child element Skip text nodes
lastElementChild element.lastElementChild Last child element Skip text nodes
nextElementSibling element.nextElementSibling Next sibling element Navigate between elements
previousElementSibling element.previousElementSibling Previous sibling element Navigate between elements

Example: Node.contains polyfill

// Node.contains polyfill
if (!Node.prototype.contains) {
    Node.prototype.contains = function(node) {
        if (!(0 in arguments)) {
            throw new TypeError('1 argument is required');
        }
        
        do {
            if (this === node) {
                return true;
            }
        } while (node = node && node.parentNode);
        
        return false;
    };
}

// Usage
var container = document.getElementById('container');
var element = document.getElementById('child');

if (container.contains(element)) {
    console.log('Element is inside container');
}

// Useful for click outside detection
document.addEventListener('click', function(e) {
    var dropdown = document.getElementById('dropdown');
    if (!dropdown.contains(e.target)) {
        // Click was outside dropdown
        closeDropdown();
    }
});

Example: Element traversal property polyfills

// firstElementChild polyfill
if (!('firstElementChild' in document.documentElement)) {
    Object.defineProperty(Element.prototype, 'firstElementChild', {
        get: function() {
            var el = this.firstChild;
            while (el && el.nodeType !== 1) {
                el = el.nextSibling;
            }
            return el;
        }
    });
}

// lastElementChild polyfill
if (!('lastElementChild' in document.documentElement)) {
    Object.defineProperty(Element.prototype, 'lastElementChild', {
        get: function() {
            var el = this.lastChild;
            while (el && el.nodeType !== 1) {
                el = el.previousSibling;
            }
            return el;
        }
    });
}

// nextElementSibling polyfill
if (!('nextElementSibling' in document.documentElement)) {
    Object.defineProperty(Element.prototype, 'nextElementSibling', {
        get: function() {
            var el = this.nextSibling;
            while (el && el.nodeType !== 1) {
                el = el.nextSibling;
            }
            return el;
        }
    });
}

// previousElementSibling polyfill
if (!('previousElementSibling' in document.documentElement)) {
    Object.defineProperty(Element.prototype, 'previousElementSibling', {
        get: function() {
            var el = this.previousSibling;
            while (el && el.nodeType !== 1) {
                el = el.previousSibling;
            }
            return el;
        }
    });
}

// childElementCount polyfill
if (!('childElementCount' in document.documentElement)) {
    Object.defineProperty(Element.prototype, 'childElementCount', {
        get: function() {
            var count = 0;
            var child = this.firstChild;
            while (child) {
                if (child.nodeType === 1) count++;
                child = child.nextSibling;
            }
            return count;
        }
    });
}

Key Takeaways - DOM APIs

  • querySelector/All: Use Sizzle for complex selector support in old browsers
  • classList: Full polyfill available for IE9, provides clean class manipulation
  • addEventListener: Polyfill normalizes IE8 attachEvent differences
  • CustomEvent: Use createEvent fallback for IE9-11
  • matches/closest: Essential for event delegation and selector testing
  • Element traversal: Polyfills skip text nodes for cleaner DOM navigation