Использование querySelectorAll для получения прямых потомков


119

Я умею это делать:

<div id="myDiv">
   <div class="foo"></div>
</div>
myDiv = getElementById("myDiv");
myDiv.querySelectorAll("#myDiv > .foo");

То есть я могу успешно получить всех прямых потомков myDivэлемента, у которого есть класс .foo.

Проблема в том, что меня беспокоит, что я должен включить #myDivв селектор, потому что я выполняю запрос к myDivэлементу (поэтому он явно избыточен).

Я должен иметь возможность оставить #myDivвыключенным, но тогда селектор не является допустимым синтаксисом, поскольку он начинается с >.

Кто-нибудь знает, как написать селектор, который получает только прямых потомков элемента, на котором работает селектор?



Ни один из ответов не дал того, что вам нужно? Пожалуйста, оставьте отзыв или выберите ответ.
Рэнди Холл,

@mattsh - Я добавил лучший (IMO) ответ для вашей потребности, проверьте это!
csuwldcat

Ответы:


85

Хороший вопрос. В то время, когда был задан вопрос, универсально реализованного способа выполнения «запросов с корневым комбинатором» (как их назвал Джон Ресиг ) не существовало.

Теперь был представлен псевдокласс : scope . Он не поддерживается в [pre-Chrominum] версиях Edge или IE, но уже несколько лет поддерживается Safari. Используя это, ваш код может стать:

let myDiv = getElementById("myDiv");
myDiv.querySelectorAll(":scope > .foo");

Обратите внимание, что в некоторых случаях вы также можете пропустить .querySelectorAllи использовать другие старые добрые функции DOM API. Например, вместо того, чтобы myDiv.querySelectorAll(":scope > *")вы могли просто написать myDiv.children, например.

В противном случае, если вы еще не можете полагаться на него :scope, я не могу придумать другого способа справиться с вашей ситуацией без добавления дополнительной логики настраиваемого фильтра (например, найти myDiv.getElementsByClassName("foo")чей .parentNode === myDiv), и, очевидно, не идеален, если вы пытаетесь поддерживать один путь кода, который действительно просто хочет принять произвольную строку селектора в качестве ввода и список совпадений в качестве вывода! Но если вы, как и я, задали этот вопрос просто потому, что застряли, думая «все, что у вас было, это молоток», не забывайте, что DOM предлагает множество других инструментов.


3
myDiv.getElementsByClassName("foo")это не то же самое, а myDiv.querySelectorAll("> .foo")больше похоже myDiv.querySelectorAll(".foo")(что, кстати, действительно работает) тем, что он находит всех потомков, .fooа не только потомков.
BoltClock

Какая досада! Я имею в виду: вы совершенно правы. Я обновил свой ответ, чтобы было более ясно, что он не очень хороший в случае OP.
natevw

спасибо за ссылку (которая, кажется, дает окончательный ответ), и спасибо за добавление терминологии «комбинаторные корневые запросы».
mattsh

1
То, что Джон Ресиг называет «запросами с корнем комбинатора», теперь «Селекторы 4» называет их относительными селекторами .
BoltClock

Таблицы стилей @Andrew Scoped (т.е. <style scoped>) не имеют ничего общего с :scopeпсевдоселектором, описанным в этом ответе.
natevw 05

124

Кто-нибудь знает, как написать селектор, который получает только прямых потомков элемента, на котором работает селектор?

Правильный способ написать селектор, «привязанный» к текущему элементу, - это использовать :scope.

var myDiv = getElementById("myDiv");
var fooEls = myDiv.querySelectorAll(":scope > .foo");

Однако поддержка браузера ограничена, и вам понадобится прокладка, если вы хотите ее использовать. Для этого я построил scopedQuerySelectorShim .


3
Стоит отметить, что :scopeспецификация в настоящее время является «Рабочим проектом» и поэтому может быть изменена. Скорее всего, он все еще будет работать так, если / когда он будет принят, но немного рано говорить, что это «правильный способ», по моему мнению.
Зак Лисоби

2
Похоже, что пока что он устарел
Адриан Мойса 08

1
3 года спустя, и вы спасаете мою большую ягодичную мышцу!
Мехрад

5
@ Адриан Мойса:: scope нигде не устарел. Возможно, вы перепутали его с атрибутом scoped.
BoltClock

6

Вот гибкий метод, написанный на ванильном JS, который позволяет вам запускать запрос селектора CSS только для прямых потомков элемента:

var count = 0;
function queryChildren(element, selector) {
  var id = element.id,
      guid = element.id = id || 'query_children_' + count++,
      attr = '#' + guid + ' > ',
      selector = attr + (selector + '').replace(',', ',' + attr, 'g');
  var result = element.parentNode.querySelectorAll(selector);
  if (!id) element.removeAttribute('id');
  return result;
}

Я бы посоветовал вам добавить какую-то метку времени или счетчик к генерируемым идентификаторам. Существует ненулевая (хотя и крошечная) вероятность Math.random().toString(36).substr(2, 10)создания одного и того же токена более одного раза.
Frédéric Hamidi

Я не уверен, насколько вероятно, что вероятность повторения хэшей мизерна, идентификатор применяется только временно на долю миллисекунды, и любой обман также должен быть дочерним элементом того же родительского узла - в основном, шансы астрономические. Тем не менее, добавление счетчика кажется нормальным.
csuwldcat

Не уверен, что вы имеете в виду any dupe would need to also be a child of the same parent node, idатрибуты охватывают весь документ. Вы правы, шансы по-прежнему весьма незначительны, но спасибо за то, что выбрали большую дорогу и добавили этот счетчик :)
Фредерик Хамиди

8
JavaScript не выполняется параллельно, поэтому шансы фактически нулевые.
WGH

1
@WGH Вау, я не могу поверить в это, я абсолютно ошеломлен, что я бы подумал, что пукнул по такой простой вещи (я работаю в Mozilla и часто повторяю этот факт, нужно больше спать! LOL)
csuwldcat

5

если вы точно знаете, что элемент уникален (например, в вашем случае с идентификатором):

myDiv.parentElement.querySelectorAll("#myDiv > .foo");

Для более «глобального» решения: (используйте прокладку matchSelector )

function getDirectChildren(elm, sel){
    var ret = [], i = 0, l = elm.childNodes.length;
    for (var i; i < l; ++i){
        if (elm.childNodes[i].matchesSelector(sel)){
            ret.push(elm.childNodes[i]);
        }
    }
    return ret;
}

где elmваш родительский элемент и selваш селектор. Можно полностью использовать как прототип.


@lazd это не часть вопроса.
Рэнди Холл

Ваш ответ не является «глобальным» решением. У него есть определенные ограничения, которые следует отметить, поэтому мой комментарий.
lazd

@lazd, который отвечает на заданный вопрос. Так почему голосование против? Или вы просто случайно прокомментировали через несколько секунд после того, как проголосовали против ответа годичной давности?
Randy Hall

Решение использует без matchesSelectorпрефикса (что даже не работает в последней версии Chrome), оно загрязняет глобальное пространство имен (ret не было объявлено), оно не возвращает NodeList, как ожидается, querySelectorMethods. Я не думаю, что это хорошее решение, поэтому и проголосовал против.
lazd

@lazd - исходный вопрос использует функцию без префикса и глобальное пространство имен. Это отличный способ решения заданного вопроса. Если вы не думаете, что это хорошее решение, не используйте его и не предлагайте отредактировать. Если это функционально некорректно или спам, рассмотрите возможность голосования против.
Рэнди Холл

2

Следующее решение отличается от предложенных до сих пор и работает для меня.

Причина в том, что вы сначала выбираете всех совпадающих потомков, а затем отфильтровываете те, которые не являются прямыми потомками. Потомок является прямым потомком, если у него нет подходящего родителя с таким же селектором.

function queryDirectChildren(parent, selector) {
    const nodes = parent.querySelectorAll(selector);
    const filteredNodes = [].slice.call(nodes).filter(n => 
        n.parentNode.closest(selector) === parent.closest(selector)
    );
    return filteredNodes;
}

НТН!


Мне это нравится за его краткость и простоту подхода, хотя он, скорее всего, не будет работать хорошо, если будет вызван с высокой частотой с большими деревьями и чрезмерно жадными селекторами.
brennanyoung

1

Я создал функцию для этой ситуации, думал, что поделюсь ею.

getDirectDecendent(elem, selector, all){
    const tempID = randomString(10) //use your randomString function here.
    elem.dataset.tempid = tempID;

    let returnObj;
    if(all)
        returnObj = elem.parentElement.querySelectorAll(`[data-tempid="${tempID}"] > ${selector}`);
    else
        returnObj = elem.parentElement.querySelector(`[data-tempid="${tempID}"] > ${selector}`);

    elem.dataset.tempid = '';
    return returnObj;
}

По сути, то, что вы делаете, генерирует случайную строку (функция randomString здесь - импортированный модуль npm, но вы можете создать свой собственный.), А затем используя эту случайную строку, чтобы гарантировать, что вы получите элемент, который ожидаете в селекторе. Тогда вы можете использовать >после этого.

Причина, по которой я не использую атрибут id, заключается в том, что атрибут id уже может использоваться, и я не хочу его отменять.


0

Что ж, мы можем легко получить всех прямых потомков элемента, используя, childNodesи мы можем выбрать предков с определенным классом querySelectorAll, поэтому нетрудно представить, что мы могли бы создать новую функцию, которая получает оба и сравнивает их.

HTMLElement.prototype.queryDirectChildren = function(selector){
  var direct = [].slice.call(this.directNodes || []); // Cast to Array
  var queried = [].slice.call(this.querySelectorAll(selector) || []); // Cast to Array
  var both = [];
  // I choose to loop through the direct children because it is guaranteed to be smaller
  for(var i=0; i<direct.length; i++){
    if(queried.indexOf(direct[i])){
      both.push(direct[i]);
    }
  }
  return both;
}

Примечание. Это вернет массив узлов, а не список узлов.

использование

 document.getElementById("myDiv").queryDirectChildren(".foo");

0

Я хотел бы добавить, что вы можете расширить совместимость : scope , просто назначив временный атрибут текущему узлу.

let node = [...];
let result;

node.setAttribute("foo", "");
result = window.document.querySelectorAll("[foo] > .bar");
// And, of course, you can also use other combinators.
result = window.document.querySelectorAll("[foo] + .bar");
result = window.document.querySelectorAll("[foo] ~ .bar");
node.removeAttribute("foo");


-2

Я просто делаю это, даже не пробуя. Это сработает?

myDiv = getElementById("myDiv");
myDiv.querySelectorAll(this.id + " > .foo");

Попробуйте, может он сработает, а может и нет. Apolovies, но сейчас я не за компьютером, чтобы попробовать (отвечает с моего iPhone).


3
QSA привязан к вызываемому элементу, поэтому он будет искать другой элемент внутри myDiv с тем же идентификатором, что и myDiv, а затем его дочерние элементы с .foo. Вы можете сделать что-то вроде `document.querySelectorAll ('#' + myDiv.id + '> .foo');
вероятно,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.