Вопрос по javascript, css, css-selectors – Получить путь CSS из элемента Dom

17

Я получил эту функцию, чтобы получить cssPath:

var cssPath = function (el) {
  var path = [];

  while (
    (el.nodeName.toLowerCase() != 'html') && 
    (el = el.parentNode) &&
    path.unshift(el.nodeName.toLowerCase() + 
      (el.id ? '#' + el.id : '') + 
      (el.className ? '.' + el.className.replace(/\s+/g, ".") : ''))
  );
  return path.join(" > ");
}
console.log(cssPath(document.getElementsByTagName('a')[123]));

Но я получил что-то вроде этого:

html > body > div#div-id > div.site > div.clearfix > ul.choices > li

Но чтобы быть полностью правым, это должно выглядеть так:

html > body > div#div-id > div.site:nth-child(1) > div.clearfix > ul.choices > li:nth-child(5)

У кого-нибудь была идея реализовать это просто в javascript?

На самом деле, я верю, что есть плагин FireBug, который получает cssPath от элемента с именем FireFinder; oP BGerrissen
Наверное должно быть:eq(1) или же:nth-child(2) скорее, чем[1] если вы хотите селектор CSS. Andy E
Да, ты прав Энди. Этот синтаксис выглядит как плохая смесь между селектором CSS и XPath. Я должен это исправить. jney
Или просто дать элементу уникальный идентификатор с помощью JavaScript? Я понимаю, почему cssPath может быть полезен в качестве плагина FireBug или чего-то еще, но для обычного кода введение идентификаторов является наиболее эффективным. BGerrissen

Ваш Ответ

5   ответов
5

Два других предоставленных ответа имели пару предположений о совместимости браузера, с которыми я столкнулся. Приведенный ниже код не будет использовать nth-child, а также имеет проверку предыдущего элемента ElementSibling.

function previousElementSibling (element) {
  if (element.previousElementSibling !== 'undefined') {
    return element.previousElementSibling;
  } else {
    // Loop through ignoring anything not an element
    while (element = element.previousSibling) {
      if (element.nodeType === 1) {
        return element;
      }
    }
  }
}
function getPath (element) {
  // False on non-elements
  if (!(element instanceof HTMLElement)) { return false; }
  var path = [];
  while (element.nodeType === Node.ELEMENT_NODE) {
    var selector = element.nodeName;
    if (element.id) { selector += ('#' + element.id); }
    else {
      // Walk backwards until there is no previous sibling
      var sibling = element;
      // Will hold nodeName to join for adjacent selection
      var siblingSelectors = [];
      while (sibling !== null && sibling.nodeType === Node.ELEMENT_NODE) {
        siblingSelectors.unshift(sibling.nodeName);
        sibling = previousElementSibling(sibling);
      }
      // :first-child does not apply to HTML
      if (siblingSelectors[0] !== 'HTML') {
        siblingSelectors[0] = siblingSelectors[0] + ':first-child';
      }
      selector = siblingSelectors.join(' + ');
    }
    path.unshift(selector);
    element = element.parentNode;
  }
  return path.join(' > ');
}
20

Ответ выше на самом деле содержит ошибку - цикл while преждевременно обрывается, когда встречается с неэлементным узлом (например, текстовым узлом), что приводит к некорректному селектору CSS.

Вот улучшенная версия, которая исправляет эту проблему плюс:

Останавливается, когда он встречает первый элемент предка с назначенным ему идентификаторомПользыnth-of-type() сделать селекторы более читабельными
    var cssPath = function(el) {
        if (!(el instanceof Element)) 
            return;
        var path = [];
        while (el.nodeType === Node.ELEMENT_NODE) {
            var selector = el.nodeName.toLowerCase();
            if (el.id) {
                selector += '#' + el.id;
                path.unshift(selector);
                break;
            } else {
                var sib = el, nth = 1;
                while (sib = sib.previousElementSibling) {
                    if (sib.nodeName.toLowerCase() == selector)
                       nth++;
                }
                if (nth != 1)
                    selector += ":nth-of-type("+nth+")";
            }
            path.unshift(selector);
            el = el.parentNode;
        }
        return path.join(" > ");
     }
@ Сыч, почему? Кажется, работает нормально и добавлениеnth-of-type к «HTML» не будет работать, например. jtblin
if (nth != 1) это не хорошо, чтобы иметь ультра-специфический путь, вы всегда должны использовать ребенка, даже если он равен 1. Sych
Стоит из:if (nth != 1) мы можем использовать:if (el.previousElementSibling != null || el.nextElementSibling != null), Тогда он сможет добавитьnth-of-type(1), если элемент является первым элементом в наборе, но не добавит его, если он единственный. kremuwa
:nth-of-type() работает не так:nth-child() - иногда это не просто вопрос замены одного другим. BoltClock
@jtblin, потому что, например,.container span поймал бы весь промежуток внутри.container, но.container span:nth-of-type(1)  будет ловить только первый, и это, вероятно, предполагаемое поведение. Sych
0

Я как-то нахожу все реализации нечитаемыми из-за ненужной мутации. Здесь я предоставляю мой в ClojureScript и JS:

(defn element? [x]
  (and (not (nil? x))
      (identical? (.-nodeType x) js/Node.ELEMENT_NODE)))

(defn nth-child [el]
  (loop [sib el nth 1]
    (if sib
      (recur (.-previousSibling sib) (inc nth))
      (dec nth))))

(defn element-path
  ([el] (element-path el []))
  ([el path]
  (if (element? el)
    (let [tag (.. el -nodeName (toLowerCase))
          id (and (not (string/blank? (.-id el))) (.-id el))]
      (if id
        (element-path nil (conj path (str "#" id)))
        (element-path
          (.-parentNode el)
          (conj path (str tag ":nth-child(" (nth-child el) ")")))))
    (string/join " > " (reverse path)))))

Javascript:

const isElement = (x) => x && x.nodeType === Node.ELEMENT_NODE;

const nthChild = (el, nth = 1) => {
  if (el) {
    return nthChild(el.previousSibling, nth + 1);
  } else {
    return nth - 1;
  }
};

const elementPath = (el, path = []) => {
  if (isElement(el)) {
    const tag = el.nodeName.toLowerCase(),
          id = (el.id.length != 0 && el.id);
    if (id) {
      return elementPath(
        null, path.concat([`#${id}`]));
    } else {
      return elementPath(
        el.parentNode,
        path.concat([`${tag}:nth-child(${nthChild(el)})`]));
    }
  } else {
    return path.reverse().join(" > ");
  }
};
13

Чтобы всегда получить нужный элемент, вам нужно будет использовать:nth-child() или же:nth-of-type() для селекторов, которые не уникально идентифицируют элемент. Итак, попробуйте это:

var cssPath = function(el) {
    if (!(el instanceof Element)) return;
    var path = [];
    while (el.nodeType === Node.ELEMENT_NODE) {
        var selector = el.nodeName.toLowerCase();
        if (el.id) {
            selector += '#' + el.id;
        } else {
            var sib = el, nth = 1;
            while (sib.nodeType === Node.ELEMENT_NODE && (sib = sib.previousSibling) && nth++);
            selector += ":nth-child("+nth+")";
        }
        path.unshift(selector);
        el = el.parentNode;
    }
    return path.join(" > ");
}

Вы можете добавить подпрограмму для проверки уникальных элементов в соответствующем контексте (например,TITLE, BASE, CAPTION, так далее.).

Да, это выглядит великолепно. Это совместимо с IE тоже? jney
@jney: Если вы имеете в виду:nth-child() селектор, то нет. Gumbo
4

Выполнение обратного поиска в селекторе CSS - сложная вещь. Я обычно сталкивался с двумя типами решений:

Поднимитесь по дереву DOM, чтобы собрать строку селектора из комбинации имен элементов, классов иid или жеname приписывать. Проблема этого метода заключается в том, что он может привести к тому, что селекторы возвращают несколько элементов, которые не обрезают его, если мы требуем, чтобы они выбирали только один уникальный элемент.

Соберите строку селектора, используяnth-child() или жеnth-of-type(), что может привести к очень длинным селекторам. В большинстве случаев, чем длиннее селектор, тем выше специфичность, и чем выше специфичность, тем больше вероятность его поломки при изменении структуры DOM.

Приведенное ниже решение является попыткой решить обе эти проблемы. Это гибридный подход, который выводит уникальный селектор CSS (т.е.document.querySelectorAll(getUniqueSelector(el)) должен всегда возвращать массив из одного элемента). Хотя возвращаемая строка селектора не обязательно самая короткая, она выводится с учетом эффективности селектора CSS, одновременно балансируя специфичность путем расстановки приоритетовnth-of-type() а такжеnth-child() прошлой.

Вы можете указать, какие атрибуты включить в селектор, обновивaAttr массив. Минимальное требование к браузеру - IE 9.

function getUniqueSelector(elSrc) {
  if (!(elSrc instanceof Element)) return;
  var sSel,
    aAttr = ['name', 'value', 'title', 'placeholder', 'data-*'], // Common attributes
    aSel = [],
    // Derive selector from element
    getSelector = function(el) {
      // 1. Check ID first
      // NOTE: ID must be unique amongst all IDs in an HTML5 document.
      // https://www.w3.org/TR/html5/dom.html#the-id-attribute
      if (el.id) {
        aSel.unshift('#' + el.id);
        return true;
      }
      aSel.unshift(sSel = el.nodeName.toLowerCase());
      // 2. Try to select by classes
      if (el.className) {
        aSel[0] = sSel += '.' + el.className.trim().replace(/ +/g, '.');
        if (uniqueQuery()) return true;
      }
      // 3. Try to select by classes + attributes
      for (var i=0; i<aAttr.length; ++i) {
        if (aAttr[i]==='data-*') {
          // Build array of data attributes
          var aDataAttr = [].filter.call(el.attributes, function(attr) {
            return attr.name.indexOf('data-')===0;
          });
          for (var j=0; j<aDataAttr.length; ++j) {
            aSel[0] = sSel += '[' + aDataAttr[j].name + '="' + aDataAttr[j].value + '"]';
            if (uniqueQuery()) return true;
          }
        } else if (el[aAttr[i]]) {
          aSel[0] = sSel += '[' + aAttr[i] + '="' + el[aAttr[i]] + '"]';
          if (uniqueQuery()) return true;
        }
      }
      // 4. Try to select by nth-of-type() as a fallback for generic elements
      var elChild = el,
        sChild,
        n = 1;
      while (elChild = elChild.previousElementSibling) {
        if (elChild.nodeName===el.nodeName) ++n;
      }
      aSel[0] = sSel += ':nth-of-type(' + n + ')';
      if (uniqueQuery()) return true;
      // 5. Try to select by nth-child() as a last resort
      elChild = el;
      n = 1;
      while (elChild = elChild.previousElementSibling) ++n;
      aSel[0] = sSel = sSel.replace(/:nth-of-type\(\d+\)/, n>1 ? ':nth-child(' + n + ')' : ':first-child');
      if (uniqueQuery()) return true;
      return false;
    },
    // Test query to see if it returns one element
    uniqueQuery = function() {
      return document.querySelectorAll(aSel.join('>')||null).length===1;
    };
  // Walk up the DOM tree to compile a unique selector
  while (elSrc.parentNode) {
    if (getSelector(elSrc)) return aSel.join(' > ');
    elSrc = elSrc.parentNode;
  }
}

Похожие вопросы