2010-09-01 3 views
16

Я получил эту функцию, чтобы получить cssPath:Получить путь к CSS от Dom элемента

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?

+2

Это, вероятно, следует ': эк (1)' или ': п-й ребенок (2)' вместо '[1 ] ', если вы хотите использовать селектор CSS. –

+0

Или просто введите элемент уникальный идентификатор с JavaScript? Я вижу, почему cssPath может быть полезен как плагин FireBug или что-то в этом роде, но для обычного кода введение идентификаторов является наиболее эффективным. – BGerrissen

+0

На самом деле, я считаю, что есть плагин FireBug, который получает cssPath из элемента FireFinder; oP – BGerrissen

ответ

11

Чтобы всегда получить нужный элемент, вам нужно будет использовать :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 и т.д.).

+0

Да, он выглядит великолепно. Является ли он совместимым с IE? – jney

+0

@jney: Если вы имеете в виду селектор ': nth-child()', то нет. – Gumbo

17

На самом деле ответ имеет ошибку: цикл 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(" > "); 
    } 
+0

': nth-of-type()' работает по-другому от ': nth-child()' - иногда это не просто замена одной на другую. – BoltClock

+1

'if (nth! = 1)' нехорошо, чтобы иметь ультра-специфический путь, вы всегда должны использовать ребенка, даже если он равен 1. – Sych

+0

@Sych, почему? Кажется, что это нормально, и добавление 'nth-of-type' в 'html' не будет работать, например. – jtblin

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(' > '); 
} 
3

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

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

  2. Соберите селекторную строку, используя 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; 
    } 
} 
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(" > "); 
    } 
}; 
Смежные вопросы