Статьи

Поиск DOM-узла предка

В восьмой статье из этой серии небольших функций я расскажу о функции ancestor() . Как следует из названия, эта функция получает ссылку на предка данного узла в соответствии с именем тега и / или соответствием классу.

Вот код функции ancestor() :

 function ancestor(node, match) { if(!node) { return null; } else if(!node.nodeType || typeof(match) != 'string') { return node; } if((match = match.split('.')).length === 1) { match.push(null); } else if(!match[0]) { match[0] = null; } do { if ( ( !match[0] || match[0].toLowerCase() == node.nodeName.toLowerCase()) && ( !match[1] || new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className) ) ) { break; } } while(node = node.parentNode); return node; } 

Первый аргумент — это ссылка на исходный узел, который может быть любым видом узла DOM , но обычно будет элементом. Второй аргумент — это строка, которая идентифицирует предка — либо в виде простого имени тега, такого как "ul" , либо в качестве селектора класса, такого как ".menu" , либо в виде комбинации этих двух, например "ul.menu" . Функция выполнит итерацию вверх от исходного узла и вернет первый узел-предок, соответствующий строковому шаблону, или null если такого предка не будет найдено.

Для чего предназначена функция

Наиболее распространенный вариант использования этой функции — из кода обработки событий — для идентификации содержащего элемента из цели события, без необходимости знать, какие другие узлы находятся между ними; возможно, мы даже не знаем, какой элемент является предком. Функция ancestor() обрабатывает это, итеративно проверяя родительские узлы на предмет имеющейся у нас информации.

Например, предположим, что мы связываем события focus с группой ссылок меню с кодом обработчика, который должен будет получить ссылку на содержащий элемент списка. Динамические меню обычно должны быть очень гибкими в той разметке, которую они поддерживают, учитывая не только такие простые элементы:

 <li> <a>...</a> </li> 

Но также более сложные элементы, с дополнительными элементами, добавленными для дополнительной семантики или в качестве хуков для стиля:

 <li> <h3> <span> <a>...</a> </span> </h3> </li> 

JavaScript будет добавлен для обработки событий focus ссылки (которые должны быть добавлены индивидуально, так как события фокуса не всплывают ):

 var links = menu.getElementsByTagName('a'); for(var len = links.length, i = 0; i < len; i ++) { links[i].addEventListener('focus', function(e) { var link = e.target; }, false); } 

Тогда функция ancestor() может обработать целевое преобразование:

 var item = ancestor(link, 'li'); 

Гибкость второго аргумента учитывает различные информационные случаи, например, когда мы знаем, что содержащее меню будет иметь class "menu" , но мы не знаем, будет ли это элемент <ul> или <ol> :

 var menu = ancestor(link, '.menu'); 

Или, возможно, у нас есть более глубоко вложенная структура, где отдельные подменю представляют собой неупорядоченные списки ( <ul class="menu"> ), в то время как панель навигации верхнего уровня представляет собой упорядоченный список с тем же именем class ( <ol class="menu"> ). Мы можем определить имя тега и class в совпадении, чтобы получить конкретную ссылку, которую мы хотим:

 var navbar = ancestor(link, 'ol.menu'); 

В этом случае любое количество других элементов "menu" будет проигнорировано, причем предок будет возвращен, только если он соответствует как имени тега, так и class .

Как работает функция

Базовая функциональность — это просто итерация вверх по DOM . Мы начинаем с исходного узла, затем проверяем каждый parentNode до parentNode пор, пока указанный parentNode не будет parentNode , или оставляем итерацию, если у нас заканчиваются узлы (т.е. если мы достигаем #document не найдя нужный узел). Однако у нас также есть некоторый тестовый код, чтобы убедиться, что оба аргумента определены правильно:

 if(!node) { return null; } else if(!node.nodeType || typeof(match) != 'string') { return node; } 

Если аргумент входного node не определен или равен null , то функция возвращает null ; или если входной node не является узлом, или match ввода не является строкой, то функция возвращает исходный узел. Это просто условия безопасности, которые делают функцию более надежной, уменьшая необходимость предварительного тестирования данных, которые ей отправляют.

Затем мы обрабатываем аргумент соответствия, чтобы создать массив из двух значений: первое — это указанное имя тега (или null если не было указано ни одного), а второе — указанное имя класса (или null для ни одного):

 if((match = match.split('.')).length === 1) { match.push(null); } else if(!match[0]) { match[0] = null; } 

Наконец, мы можем выполнить итеративные проверки, сравнивая текущий эталонный узел на каждой итерации с критериями, определенными в массиве match . Если match[0] (имя тега) равно null любой элемент будет совпадать, в противном случае мы сопоставим только элемент с указанным именем тега (преобразуем оба в нижний регистр, чтобы совпадение не учитывало регистр). Аналогично, если match[1] (имя класса) равно null тогда все в порядке, в противном случае элемент должен содержать указанный class :

 do { if ( ( !match[0] || match[0].toLowerCase() == node.nodeName.toLowerCase()) && ( !match[1] || new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className) ) ) { break; } } while(node = node.parentNode); 

Если оба условия совпадают, мы прерываем итерацию, и текущий ссылочный узел возвращается; в противном случае мы продолжаем к следующему parentNode . Если бы мы позволили коду зайти так далеко, когда оба значения соответствия равны null , конечным результатом было бы то, что мы возвращаем исходный node , что в точности соответствует условию безопасности в начале.

Интересной вещью в самой итерации является использование do...while :

 do { ... } while(node = node.parentNode); 

Внутри оценки while мы пользуемся возможностью определять назначение внутри оценки. При каждой оценке ссылка на node преобразуется в его parentNode и переназначается. Это назначение возвращает назначенный node . Ссылка на node будет null если родительский объект не существует, поэтому он не пройдет условие while , поэтому итерация остановится и функция вернет null . Однако, если родительский объект действительно существует, он пройдет условие while , и поэтому итерация будет продолжена, поскольку любая ссылка на узел оценивается как true , а null — как false .

Поскольку число узлов, которые мы должны протестировать, неизвестно, мы должны использовать оператор while для итерации до тех пор, пока существует родитель. Но, используя do...while а не просто while , мы оцениваем исходный узел перед преобразованием в его родителя (поскольку do вычисляется до первого while ). В конечном итоге это означает, что если исходный узел уже прошел условие соответствия, он будет возвращен сразу, и это избавляет нас от необходимости определять отдельное условие if перед итерацией.

Вывод

Функция ancestor() не выиграет призы за изысканность! Но абстракции простой функциональности — это кирпичи и минус программирования, обеспечивающий многократно используемый код, который позволяет многократно набирать одну и ту же базовую логику.