Created
November 17, 2025 18:12
-
-
Save blizzardengle/fc64e30533659752def23c6ae0c08b55 to your computer and use it in GitHub Desktop.
Finds the nearest element matching the querySelector based on DOM traversal distance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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