Skip to content

Instantly share code, notes, and snippets.

@blizzardengle
Created November 17, 2025 18:12
Show Gist options
  • Select an option

  • Save blizzardengle/fc64e30533659752def23c6ae0c08b55 to your computer and use it in GitHub Desktop.

Select an option

Save blizzardengle/fc64e30533659752def23c6ae0c08b55 to your computer and use it in GitHub Desktop.
Finds the nearest element matching the querySelector based on DOM traversal distance.
/**
* Finds the nearest element matching the querySelector based on DOM traversal distance.
* Can be added to Element.prototype for convenient use like Element.closest()
*
* @example
* // Standalone usage
* const result = nearest(element, '.target');
*
* @example
* // Add to prototype (optional)
* Element.prototype.nearest = function(querySelector, direction, maxDistance) {
* return nearest(this, querySelector, direction, maxDistance);
* };
* element.nearest('.target');
*/
const nearest = (() => {
/**
* Finds the nearest element matching the querySelector based on DOM traversal distance.
*
* @param {Element} startElement - The element to start searching from.
* @param {string} querySelector - The CSS selector to match the target element.
* @param {string} [direction="both"] - The direction to search. Options: "up", "down", "both". Default: "both"
* @param {number} [maxDistance=50] - Maximum traversal distance to search. Prevents runaway searches on complex DOMs. Default: 50
* @returns {Element|null} - The nearest matching element or null if not found.
*/
function nearest(startElement, querySelector, direction = 'both', maxDistance = 50) {
if (!startElement || !(startElement instanceof Element)) {
throw new Error('Invalid starting element provided.');
}
if (typeof querySelector !== 'string') {
throw new Error('Invalid querySelector provided.');
}
if (!['up', 'down', 'both'].includes(direction)) {
throw new Error("Direction must be 'up', 'down', or 'both'.");
}
if (typeof maxDistance !== 'number' || maxDistance < 1) {
throw new Error('maxDistance must be a positive number.');
}
// Check the start element itself first
if (startElement.matches(querySelector)) {
return startElement;
}
// For single direction, use simpler algorithm
if (direction === 'down') return searchDownwards(startElement, querySelector, maxDistance);
if (direction === 'up') return searchUpwards(startElement, querySelector, maxDistance);
// For "both": BFS to find truly nearest
return searchBoth(startElement, querySelector, maxDistance);
}
/**
* Searches downwards from the starting element.
* @private
*/
function searchDownwards(element, querySelector, maxDistance) {
let distance = 0;
// Check next siblings first
let sibling = element.nextElementSibling;
while (sibling && distance < maxDistance) {
if (sibling.matches(querySelector)) return sibling;
sibling = sibling.nextElementSibling;
distance++;
}
// Then check descendants using BFS for proper distance tracking
const queue = [];
const visited = new Set();
let child = element.firstElementChild;
while (child) {
queue.push({ element: child, distance: 1 });
visited.add(child);
child = child.nextElementSibling;
}
while (queue.length > 0) {
const { element: current, distance: dist } = queue.shift();
if (dist > maxDistance) break;
if (current.matches(querySelector)) return current;
// Add next sibling
if (current.nextElementSibling && !visited.has(current.nextElementSibling)) {
visited.add(current.nextElementSibling);
queue.push({ element: current.nextElementSibling, distance: dist });
}
// Add children
let childNode = current.firstElementChild;
while (childNode) {
if (!visited.has(childNode)) {
visited.add(childNode);
queue.push({ element: childNode, distance: dist + 1 });
}
childNode = childNode.nextElementSibling;
}
}
return null;
}
/**
* Searches upwards from the starting element.
* @private
*/
function searchUpwards(element, querySelector, maxDistance) {
let distance = 0;
// Check previous siblings first
let sibling = element.previousElementSibling;
while (sibling && distance < maxDistance) {
if (sibling.matches(querySelector)) return sibling;
sibling = sibling.previousElementSibling;
distance++;
}
// Move to parent and continue
let current = element.parentElement;
distance = 1;
while (current && distance <= maxDistance) {
if (current.matches(querySelector)) return current;
// Check previous siblings of ancestors
sibling = current.previousElementSibling;
while (sibling && distance < maxDistance) {
if (sibling.matches(querySelector)) return sibling;
sibling = sibling.previousElementSibling;
distance++;
}
current = current.parentElement;
distance++;
}
return null;
}
/**
* Searches in both directions using BFS for truly nearest element.
* @private
*/
function searchBoth(startElement, querySelector, maxDistance) {
const queue = [];
const visited = new Set([startElement]);
const addToQueue = (element, distance) => {
if (!element || visited.has(element) || distance > maxDistance) return;
visited.add(element);
queue.push({ element, distance });
};
// Add all immediate neighbors (distance 1)
addToQueue(startElement.parentElement, 1);
addToQueue(startElement.previousElementSibling, 1);
addToQueue(startElement.nextElementSibling, 1);
let child = startElement.firstElementChild;
while (child) {
addToQueue(child, 1);
child = child.nextElementSibling;
}
// BFS traversal
while (queue.length > 0) {
const { element, distance } = queue.shift();
if (element.matches(querySelector)) {
return element;
}
// Don't add neighbors if we're at max distance
if (distance >= maxDistance) continue;
// Add this element's neighbors
addToQueue(element.parentElement, distance + 1);
addToQueue(element.previousElementSibling, distance + 1);
addToQueue(element.nextElementSibling, distance + 1);
let childNode = element.firstElementChild;
while (childNode) {
addToQueue(childNode, distance + 1);
childNode = childNode.nextElementSibling;
}
}
return null;
}
return nearest;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment