2010-09-01 23 views
16

Tengo esta función para obtener una cssPath:Obtener ruta CSS del elemento 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])); 

pero me dio algo como esto:

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

Pero para ser totalmente correcta, debería tener este aspecto :

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

¿Alguien tuvo alguna idea de implementarlo simplemente en javascript?

+2

Es probablemente debería ser ': eq (1)' o ': nth-child (2)' en lugar de '[1 ] 'si quieres un selector de CSS. –

+0

¿O simplemente le da al elemento una identificación única con JavaScript? Puedo ver por qué cssPath podría ser útil como un complemento de FireBug o algo así, pero para el código regular, la introducción de los ID es la más efectiva. – BGerrissen

+0

De hecho, creo que hay un complemento FireBug que obtiene un cssPath de un elemento llamado FireFinder; oP – BGerrissen

Respuesta

11

Para obtener siempre el elemento correcto, deberá usar :nth-child() o :nth-of-type() para los selectores que no identifiquen un elemento de forma exclusiva. Así que trate de esto:

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

Se podría añadir una rutina para comprobar si hay elementos únicos en su contexto correspondiente (como TITLE, BASE, CAPTION, etc.).

+0

Sí, se ve genial. ¿Es compatible con IE también? – jney

+0

@jney: si te refieres al selector ': nth-child()', entonces no. – Gumbo

17

La respuesta anterior tiene un error: el bucle while se rompe prematuramente cuando se encuentra con un nodo que no es un elemento (por ejemplo, un nodo de texto) y da como resultado un selector de CSS incorrecto.

Aquí es una versión mejorada que solucione el problema más:

  • detiene cuando encuentra el primer elemento ancestro con un ID asignado a él
  • Usos nth-of-type() para que los selectores más legible
 
    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()' funciona de forma diferente a ': nth-child()' - a veces no es una simple cuestión de reemplazar uno con el otro. – BoltClock

+1

'if (nth! = 1)' no es bueno, para tener una ruta ultraespecífica, siempre debe usar child incluso si es 1. – Sych

+0

@Sych, ¿por qué? Parece que funciona bien y agregar 'nth-of-type' a 'html' no funcionaría, por ejemplo. – jtblin

5

Las otras dos respuestas proporcionadas tenían un par de supuestos con la compatibilidad del navegador que encontré. A continuación, el código no usará nth-child y también tiene la verificación anteriorElementSibling.

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

Hacer una búsqueda del selector CSS inverso es algo intrínsecamente complicado. en general, me he encontrado dos tipos de soluciones:

  1. ascender por el árbol DOM para ensamblar la cadena de selección de una combinación de los nombres de los elementos, clases, y el atributo id o name. El problema con este método es que puede dar como resultado selectores que devuelven varios elementos, lo que no lo reducirá si les solicitamos que seleccionen solo un elemento único.

  2. Ensamble la cadena del selector usando nth-child() o nth-of-type(), lo que puede dar como resultado selectores muy largos. En la mayoría de los casos, cuanto más largo es un selector, mayor es la especificidad que tiene, y cuanto mayor es la especificidad, más probable es que se rompa cuando cambie la estructura DOM.

La solución a continuación es un intento de abordar estos dos problemas. Es un enfoque híbrido que genera un selector de CSS único (es decir, document.querySelectorAll(getUniqueSelector(el)) siempre debe devolver una matriz de un elemento). Si bien la cadena del selector devuelto no es necesariamente la más corta, se deriva con la mira puesta en la eficacia del selector de CSS al tiempo que se equilibra la especificidad dando prioridad a nth-of-type() y nth-child() al final.

Puede especificar qué atributos incorporar en el selector actualizando la matriz aAttr. El requisito mínimo del navegador es 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

De alguna manera encuentro todas las implementaciones ilegibles debido a una mutación innecesaria. Aquí proporciono mina en ClojureScript y 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(" > "); 
    } 
}; 
Cuestiones relacionadas