Получить путь CSS из элемента Dom
Я получил эту функцию, чтобы получить 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?
8 ответов
Чтобы всегда получить нужный элемент, вам нужно будет использовать :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
, так далее.).
Ответ выше на самом деле содержит ошибку - цикл while преждевременно обрывается, когда встречается с неэлементным узлом (например, с текстовым узлом), что приводит к некорректному селектору CSS.
Вот улучшенная версия, которая исправляет эту проблему плюс:
- Останавливается, когда он встречает первый элемент предка с назначенным ему идентификатором
- Пользы
nth-of-type()
сделать селекторы более читабельными
var cssPath = function(el) { if (!(el instanceof Element)) вернуть; var path = []; while (el.nodeType === Node.ELEMENT_NODE) { var selector = el.nodeName.toLowerCase(); if (el.id) { селектор += '#' + el.id; path.unshift(селектор); перерыв; } еще { var sib = el, nth = 1; while (sib = sib.previousElementSibling) { if (sib.nodeName.toLowerCase() == селектор) энный ++; } если (nth!= 1) селектор += ":nth-of-type("+nth+")"; } path.unshift(селектор); el = el.parentNode; } return path.join(" > "); }
Выполнение обратного поиска в селекторе 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;
}
}
Два других предоставленных ответа имели пару предположений о совместимости браузера, с которыми я столкнулся. Приведенный ниже код не будет использовать 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(' > ');
}
There are some js libraries that do exactly this:
I am using the first one and with success so far
Я как-то нахожу все реализации нечитаемыми из-за ненужной мутации. Здесь я предоставляю мой в 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(" > ");
}
};
function cssPath (e, anchor) {
var selector;
var parent = e.parentNode, child = e;
var tagSelector = e.nodeName.toLowerCase();
while (anchor && parent != anchor || !anchor && parent.nodeType === NodeTypes.ELEMENT_NODE) {
var cssAttributes = ['id', 'name', 'class', 'type', 'alt', 'title', 'value'];
var childSelector = tagSelector;
if (!selector || parent.querySelectorAll (selector).length > 1) {
for (var i = 0; i < cssAttributes.length; i++) {
var attr = cssAttributes[i];
var value = child.getAttribute(attr);
if (value) {
if (attr === 'id') {
childSelector = '#' + value;
} else if (attr === 'class') {
childSelector = childSelector + '.' + value.replace(/\s/g, ".").replace(/\.\./g, ".");
} else {
childSelector = childSelector + '[' + attr + '="' + value + '"]';
}
}
}
var putativeSelector = selector? childSelector + ' ' + selector: childSelector;
if (parent.querySelectorAll (putativeSelector).length > 1) {
var siblings = parent.querySelectorAll (':scope > ' + tagSelector);
for (var index = 0; index < siblings.length; index++)
if (siblings [index] === child) {
childSelector = childSelector + ':nth-of-type(' + (index + 1) + ')';
putativeSelector = selector? childSelector + ' ' + selector: childSelector;
break;
}
}
selector = putativeSelector;
}
child = parent;
parent = parent.parentNode;
}
return selector;
};
Лучше поздно, чем никогда: я пришел к этому вопросу и попытался использовать выбранный ответ, но в моем случае это не сработало, потому что это не очень конкретно для моего случая. Поэтому я решил написать свое собственное решение - надеюсь, оно может кому-то помочь.
Это решение выглядит следующим образом:
tag.class#id[name][type]:nth-child(?)
и нацелены на
>
.
function path(e) {
let a = [];
while (e.parentNode) {
let d = [
e.tagName.toLowerCase(),
e.hasAttribute("class") ? e.getAttribute("class") : "",
e.hasAttribute("id") ? e.getAttribute("id") : "",
e.hasAttribute("name") ? e.getAttribute("name") : "",
e.hasAttribute("type") ? e.getAttribute("type") : "",
0 // nth-child
];
// Trim
for (let i = 0; i < d.length; i++) d[i] = typeof d[i] == "string" ? d[i].trim() : d[i];
if (d[1] != "") d[1] = "."+d[1].split(" ").join(".");
if (d[2] != "") d[2] = "#"+d[2];
if (d[3] != "") d[3] = '[name="'+d[3]+'"]';
if (d[4] != "") d[4] = '[type="'+d[4]+'"]';
// Get child index...
let s = e;
while (s) {
d[5]++;
s = s.previousElementSibling;
}
d[5] = d[5] != "" ? ":nth-child("+d[5]+")" : ":only-child";
// Build the String
s = "";
for (let i = 0; i < d.length; i++) s += d[i];
a.unshift(s);
// Go to Parent
e = e.parentNode;
}
return a.join(">");
}
Я знаю, что это не так читабельно (я использую его в своем беспорядочном коде), но это даст вам точные элементы, которые вы ищете. Просто попробуйте.