In psuedo code, this is what I want.
var selector = $(this).cssSelectorAsString(); // Made up method...
// selector is now something like: "html>body>ul>li>img[3]"
var element = $(selector);
The reason is that I need to pass this off to an external environment, where a string is my only way to exchange data. This external environment then needs to send back a result, along with what element to update. So I need to be able to serialize a unique CSS selector for every element on the page.
I noticed jquery has a selector method, but it does not appear to work in this context. It only works if the object was created with a selector. It does not work if the object was created with an HTML node object.
I see now that a plugin existed (with the same name I thought of too), but here's just some quick JavaScript I wrote. It takes no consideration to the ids or classes of elements – only the structure (and adds :eq(x) where a node name is ambiguous).
jQuery.fn.getPath = function () {
if (this.length != 1) throw 'Requires one element.';
var path, node = this;
while (node.length) {
var realNode = node[0], name = realNode.name;
if (!name) break;
name = name.toLowerCase();
var parent = node.parent();
var siblings = parent.children(name);
if (siblings.length > 1) {
name += ':eq(' + siblings.index(realNode) + ')';
}
path = name + (path ? '>' + path : '');
node = parent;
}
return path;
};
(License: MIT)
TL;DR - this is a more complex problem than it seems and you should use a library.
This problem appears easy at the first glance, but it's trickier than it seems, just as replacing plain URLs with links is non-trivial. Some considerations:
Using descendant selectors vs. child selectors can lead to cases where the selector isn't unique.
Using :eq() limits the usefulness of the solution, as it will require jQuery
Using tag+nth-child selectors can result in unnecessarily long selectors
Not taking advantage of ids makes the selector less robust to changes in the page structure.
Further proof that the problem isn't as easy as it seems: there are 10+ libraries that generate CSS selectors, and the author of one of them has published this comparison.
jQuery-GetPath is a good starting point: it'll give you the item's ancestors, like this:
var path = $('#foo').getPath();
// e.g., "html > body > div#bar > ul#abc.def.ghi > li#foo"
Here's a version of Blixt's answer that works in IE:
jQuery.fn.getPath = function () {
if (this.length != 1) throw 'Requires one element.';
var path, node = this;
while (node.length) {
var realNode = node[0];
var name = (
// IE9 and non-IE
realNode.localName ||
// IE <= 8
realNode.tagName ||
realNode.nodeName
);
// on IE8, nodeName is '#document' at the top level, but we don't need that
if (!name || name == '#document') break;
name = name.toLowerCase();
if (realNode.id) {
// As soon as an id is found, there's no need to specify more.
return name + '#' + realNode.id + (path ? '>' + path : '');
} else if (realNode.className) {
name += '.' + realNode.className.split(/\s+/).join('.');
}
var parent = node.parent(), siblings = parent.children(name);
if (siblings.length > 1) name += ':eq(' + siblings.index(node) + ')';
path = name + (path ? '>' + path : '');
node = parent;
}
return path;
};
I just wanted to share my version too because it is very clear to understand. I tested this script in all common browsers and it is working like a boss.
jQuery.fn.getPath = function () {
var current = $(this);
var path = new Array();
var realpath = "BODY";
while ($(current).prop("tagName") != "BODY") {
var index = $(current).parent().find($(current).prop("tagName")).index($(current));
var name = $(current).prop("tagName");
var selector = " " + name + ":eq(" + index + ") ";
path.push(selector);
current = $(current).parent();
}
while (path.length != 0) {
realpath += path.pop();
}
return realpath;
}
Same solution like that one from #Blixt but compatible with multiple jQuery elements.
jQuery('.some-selector') can result in one or many DOM elements. #Blixt's solution works unfortunately only with the first one. My solution concatenates all them with ,.
If you want just handle the first element do it like this:
jQuery('.some-selector').first().getPath();
// or
jQuery('.some-selector:first').getPath();
Improved version
jQuery.fn.extend({
getPath: function() {
var pathes = [];
this.each(function(index, element) {
var path, $node = jQuery(element);
while ($node.length) {
var realNode = $node.get(0), name = realNode.localName;
if (!name) { break; }
name = name.toLowerCase();
var parent = $node.parent();
var sameTagSiblings = parent.children(name);
if (sameTagSiblings.length > 1)
{
allSiblings = parent.children();
var index = allSiblings.index(realNode) +1;
if (index > 0) {
name += ':nth-child(' + index + ')';
}
}
path = name + (path ? ' > ' + path : '');
$node = parent;
}
pathes.push(path);
});
return pathes.join(',');
}
});
If you are looking for a comprehensive, non-jQuery solution then you should try axe.utils.getSelector.
Following up on what alex wrote.
jQuery-GetPath is a great starting point but I have modified it a little to incorporate :eq(), allowing me to distinguish between multiple id-less elements.
Add this before the getPath return line:
if (typeof id == 'undefined' && cur != 'body') {
allSiblings = $(this).parent().children(cur);
var index = allSiblings.index(this);// + 1;
//if (index > 0) {
cur += ':eq(' + index + ')';
//}
}
This will return a path like "html > body > ul#hello > li.5:eq(1)"
Update: This code was changed since then. You may find the implementation of the function now at css-login.js
Original answer:
You may also have a look at findCssSelector, which is used in Firefox developer tools to save the currently selected node upon page refreshes. It doesn't use jQuery or any library.
const findCssSelector = function(ele) {
ele = getRootBindingParent(ele);
let document = ele.ownerDocument;
if (!document || !document.contains(ele)) {
throw new Error("findCssSelector received element not inside document");
}
let cssEscape = ele.ownerGlobal.CSS.escape;
// document.querySelectorAll("#id") returns multiple if elements share an ID
if (ele.id &&
document.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
return "#" + cssEscape(ele.id);
}
// Inherently unique by tag name
let tagName = ele.localName;
if (tagName === "html") {
return "html";
}
if (tagName === "head") {
return "head";
}
if (tagName === "body") {
return "body";
}
// We might be able to find a unique class name
let selector, index, matches;
if (ele.classList.length > 0) {
for (let i = 0; i < ele.classList.length; i++) {
// Is this className unique by itself?
selector = "." + cssEscape(ele.classList.item(i));
matches = document.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique with a tag name?
selector = cssEscape(tagName) + selector;
matches = document.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
// Maybe it's unique using a tag name and nth-child
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = selector + ":nth-child(" + index + ")";
matches = document.querySelectorAll(selector);
if (matches.length === 1) {
return selector;
}
}
}
// Not unique enough yet. As long as it's not a child of the document,
// continue recursing up until it is unique enough.
if (ele.parentNode !== document) {
index = positionInNodeList(ele, ele.parentNode.children) + 1;
selector = findCssSelector(ele.parentNode) + " > " +
cssEscape(tagName) + ":nth-child(" + index + ")";
}
return selector;
};
$.fn.getSelector = function(){
var $ele = $(this);
return '#' + $ele.parents('[id!=""]').first().attr('id')
+ ' .' + $ele.attr('class');
};
Related
I'm using JavaScript to remove, order up, order down a text row, it runs normally in IE, but not in Chrome or Firefox.
When I run, I received a message from console bug:
Uncaught TypeError: Failed to execute 'removeChild' on 'Node': parameter 1 is not of type 'Node'.
How to fix the error?
function dels(index) {
var frm = document.writeForm;
var opts = frm['ans' + index].value = ''; // eval("frm.ans_list" + index + ".options");
for (var i = 0; i < opts.length; i++) {
if (opts[i].selected) {
opts[i--].removeChild(true);
}
}
eval("frm.ans" + index + ".value = '' ");
setting_val(index);
}
function up_move(index) {
var frm = document.writeForm;
var opts = eval("frm.ans_list" + index + ".options"); // frm['ans' + index].value = '';
for (var i = 0; i < opts.length; i++) {
if (opts[i].selected && i > 0) {
tmp = opts[i].cloneNode(true);
opts[i].removeChild(true);
opts[i - 1].insertAdjacentElement("beforeBegin", tmp).selected = true;
}
}
setting_val(index);
}
**(UPDATED)**
function down_move(index)
{
var frm = document.writeForm;
var opts=frm["ans_list" + index].options // eval("frm.ans_list" + index + ".options"); // frm['ans' + index].value = '';
for (var i=opts.length-1; i>=0; i--) {
if (opts[i].selected && i<opts.length-1) {
tmp = opts[i].cloneNode(true);
opts[i].removeChild(true);
opts[i].insertAdjacentElement("afterEnd", tmp).selected = true;
}
}
setting_val(index);
}
<span class="bt_test_admin bg_type_01">Delete</span>
<span class="bt_test_admin bg_type_01">▲ Order</span>
<span class="bt_test_admin bg_type_01">▼ Order</span>
Wrong use of removeChild
if (opts[i].selected) {
opts[i--].removeChild(true);
}
The function is intended as:
ParentNode.removeChild(ChildNode);
// OR
ChildNode.parentNode.removeChild(ChildNode);
MDN Documentation on removeChild
Also, you can replace all your evals
eval("frm.ans" + index + ".value = '' ")
eval("frm.ans_list" + index + ".options")
It would be better written as
frm["ans" + index].value = ""
frm["ans_list" + index].options
Finally,
tmp = opts[i].cloneNode(true);
opts[i].removeChild(true);
opts[i].insertAdjacentElement("afterEnd", tmp).selected = true;
Cloning a node, appending the clone, and removing the original would be optimized as moving the original to its new location.
But, you try to remove the original, then insert the clone after the original. It's odd.
If I correctly understood what you try to do, this function could help you.
function reverse_options_order(select_element)
{
// we store the current value to restore it after reordering
const selected_value = select_element.value;
// document fragment will temporarily hold the children
const fragment = document.createDocumentFragment();
while (select_element.lastChild)
{
// last child become first child, effectively reversing the order
fragment.appendChild(select_element.lastChild);
}
// appending a fragment is equal to appending all its children
// the fragment will "merge" with the select_element seamlessly
select_element.appendChild(fragment);
select_element.value = selected_value;
}
You can use the same method to reverse any nodes order
I have a problem with the javascript replace function and I don't succeed to resolve it.
This is my code : https://jsfiddle.net/r36k20sa/1/
var tags = ['zazie', 'johnny'];
tags.forEach(function(element) {
content = content.replace(
new RegExp("(?!<a.*?>.*?)(\\b" + element + "\\b)(?!.*?<\\/a>)", "igm"),
'$1'
);
});
In the tags array, if I reverse the array "johnny" then "zazie" all tags are well selected otherwise, some tags are missing. (The last in this example). What can be the trick?
What can be explained that ? It seems like the javascript replace function runs asynchronous?
Thanks for your help.
Are you seriously using regex to process HTML when you have a DOM parser at your fingertips?
var content = document.getElementById('content');
function findTextNodes(root,ret) {
// recursively descend into child nodes and return an array of text nodes
var children = root.childNodes, l = children.length, i;
ret = ret || [];
for( i=0; i<l; i++) {
if( children[i].nodeType == 1) { // ElementNode
// excluding A tags here, you might also want to exclude BUTTON tags
if( children[i].nodeName != "A") {
findTextNodes(children[i],ret);
}
}
if( children[i].nodeType == 3) { // TextNode
ret.push(children[i]);
}
}
return ret;
}
var textNodes = findTextNodes(content);
// now search those text node contents for matching tags.
var tags = ['zazie','johnny'], tagcount = tags.length, regexes, tag;
for( tag=0; tag<tagcount; tag++) {
regexes[tag] = new RegExp("\b"+tags[tag]+"\b","i");
}
var node, match, index, tagtext, newnode;
while(node = textNodes.shift()) {
for( tag=0; tag<tagcount; tag++) {
if( match = node.nodeValue.match(regexes[tag])) {
index = match.index;
textNodes.unshift(node.splitText(index + tags[tag].length));
tagtext = node.splitText(index);
newnode = document.createElement('a');
newnode.href = "";
newnode.className = "esk-seo-plu-link";
newnode.style.cssText = "background:red;color:white";
tagtext.parentNode.replaceChild(newnode,tagtext);
newnode.appendChild(tagtext);
}
}
}
// and done - no more action needed since it was in-place.
See it in action
Please replace . with \\.
var tags = ['zazie', 'johnny'];
tags.forEach(function(element) {
content = content.replace(
new RegExp("(?!<a.*?>\\.*?)(\\b" + element + "\\b)(?!\\.*?<\\/a>)", "igm"),
'$1'
);
});
I'm working on a script and need to split strings which contain both html tags and text. I'm trying to isolate the text and elimanate the tags
For example, I want this:
string = '<p><span style="color:#ff3366;">A</span></p><p><span style="color:#ff3366;text-decoration:underline;">B</span></p><p><span style="color:#ff3366;text-decoration:underline;"><em>C</em></span></p>';
to be split like this:
separation = string.split(/some RegExp/);
and become:
separation[0] = "<span style="color:#ff3366;">A</span>";
separation[1] = "<span style="color:#ff3366;text-decoration:underline;">B</span>";
separation[2] = "<span style="color:#ff3366;text-decoration:underline;"><em>C</em></span>";
After that I would like to split the sepeartion string like this:
stringNew = '<span style="color:#ff3366;">A</span>';
extendedSeperation = stringNew.split(/some RegExp/);
extendedSeperation[0] = "A";
extendedSeperation[1] = "style="color:#ff3366;";
Don't use RegEx for reasons explained in comments.
Instead, do this:
Create an invisible node:
node = $("<div>").css("display", "none");
Attach it to the body:
$("body").append(node);
Now inject your HTML into the node:
node.html(myHTMLString);
Now you can traverse the DOM tree and extract/render it as you like, much like this:
ptags = node.find("p") // will return all <p> tags
To get the content of a tag use:
ptags[0].html()
Finally, to clear the node do:
node.html("");
This should be enough to get you going.
This way you leverage the internal parser of the browser, as suggested in the comments.
Your exact expectations are a little unclear, but based only on the information given here is an example that may give you ideas.
Does not use RegExp
Does not use jQuery or any other library
Does not append and remove elements from the DOM
Is well supported across browsers
function walkTheDOM(node, func) {
func(node);
node = node.firstChild;
while (node) {
walkTheDOM(node, func);
node = node.nextSibling;
}
}
function textContent(node) {
if (typeof node.textContent !== "undefined" && node.textContent !== null) {
return node.textContent;
}
var text = ""
walkTheDOM(node, function (current) {
if (current.nodeType === 3) {
text += current.nodeValue;
}
});
return text;
}
function dominate(text) {
var container = document.createElement('div');
container.innerHTML = text;
return container;
}
function toSeparation(htmlText) {
var spans = dominate(htmlText).getElementsByTagName('span'),
length = spans.length,
result = [],
index;
for (index = 0; index < length; index += 1) {
result.push(spans[index].outerHTML);
}
return result;
}
function toExtendedSeperation(node) {
var child = dominate(node).firstChild,
attributes = child.attributes,
length = attributes.length,
text = textContent(child),
result = [],
style,
index,
attr;
if (text) {
result.push(text);
}
for (index = 0; index < length; index += 1) {
attr = attributes[index]
if (attr.name === 'style') {
result.push(attr.name + '=' + attr.value);
break;
}
}
return result;
}
var strHTML = '<p><span style="color:#ff3366;">A</span></p><p><span style="color:#ff3366;text-decoration:underline;">B</span></p><p><span style="color:#ff3366;text-decoration:underline;"><em>C</em></span></p>',
separation = toSeparation(strHTML),
extendedSeperation = toExtendedSeperation(separation[0]),
pre = document.getElementById('out');
pre.appendChild(document.createTextNode(JSON.stringify(separation, null, 2)));
pre.appendChild(document.createTextNode('\n\n'));
pre.appendChild(document.createTextNode(JSON.stringify(extendedSeperation, null, 2)));
<pre id="out"></pre>
Of course you will need to make modifications to suit your exact needs.
I am trying to getElementById("game_image") and the TagName is 'img' I want the data within the 'src' tag, specifically the 'key=f430a2c1' token.:
<img id="game_image" src="img/index.php?key=f430a2c1&rand=956875" alt="game image." style="padding-right:150px;" />
*
$("#b_hint").click(function(){
// var images = document.getElementsByTagName("img"),
// wanted;
var data = document.getElementById("game_image"), wanted;
wanted = data[1].src;
// if (images.length) {
// wanted = images[1].src;
// if (wanted) {
// wanted = wanted.split("?");
// if (wanted.length >= 2 && wanted[1].indexOf("&") !== -1) {
// wanted = wanted[1].split("&")[0];
// }
// }
//}
//if (typeof wanted !== "string") {
// wanted = "";
// }
alert(wanted);
wanted = data.src;
getElementById returns a single element, not a NodeList, so you don't need to use array syntax.
wanted = data.src;
wanted = wanted.substring(wanted.indexOf('?') + 1)
Taking into account the the src URL could contain more parameters separated by '&', you can extract key=... like this:
function getKey(url) {
var idx1 = url.indexOf("?") + 1;
if (idx1 == -1) { return ""; }
var idx2 = url.indexOf("&");
if (idx2 == -1) { idx2 = url.length; }
return url.substring(idx1, idx2);
}
$("#b_hint").click(function() {
var img = document.getElementById("game_image");
var wanted = getKey(img.src);
...
NOTE:
For the getKey() function to work properly, the 'key' parameter must come straight ater the '?' (e.g. ...?key=..., but not ...?some=other&key=...).
See also this regex-powered solution:
var regex = new RegExp(".*?\\?.*?\\b(key=[^&]+)");
$("#b_hint").click(function() {
var src = document.getElementById("game_image").src;
var wanted = regex.test(src) ? src.match(regex)[1] : "";
...
This will properly match the key=<value> part, even if 'key' is not the first parameter in the query string.
Jquery?!
working fiddle -> http://jsfiddle.net/mpyfQ/8/
var data = $("#game_image").attr("src");
data = data.substring(data.indexOf('?') + 1);
The answer should be InnerHTML.
A container div.example can have different 1st-level child elements (section, div, ul, nav, ...). Quantity and type of those elements can vary.
I have to find the type (e.g. div) of the direct child that occurs the most.
What is a simple jQuery or JavaScript solution?
jQuery 1.7.1 is available, although it should work in IE < 9 (array.filter) as well.
Edit: Thank you #Jasper, #Vega and #Robin Maben :)
Iterate through the children using .children() and log the number of element.tagNames you find:
//create object to store data
var tags = {};
//iterate through the children
$.each($('#parent').children(), function () {
//get the type of tag we are looking-at
var name = this.tagName.toLowerCase();
//if we haven't logged this type of tag yet, initialize it in the `tags` object
if (typeof tags[name] == 'undefined') {
tags[name] = 0;
}
//and increment the count for this tag
tags[name]++;
});
Now the tags object holds the number of each type of tag that occurred as a child of the #parent element.
Here is a demo: http://jsfiddle.net/ZRjtp/ (watch your console for the object)
Then to find the tag that occurred the most you could do this:
var most_used = {
count : 0,
tag : ''
};
$.each(tags, function (key, val) {
if (val > most_used.count) {
most_used.count = val;
most_used.tag = key;
}
});
The most_used object now holds the tag used the most and how many times it was used.
Here is a demo: http://jsfiddle.net/ZRjtp/1/
Edit: I think a jQuery function like below should be more useful..
DEMO
$.fn.theMostChild = function() {
var childs = {};
$(this).children().each(function() {
if (childs.hasOwnProperty(this.nodeName)) {
childs[this.nodeName] += 1;
} else {
childs[this.nodeName] = 1;
}
});
var maxNode = '', maxNodeCount = 0;
for (nodeName in childs) {
if (childs[nodeName] > maxNodeCount) {
maxNode = nodeName;
maxNodeCount = childs[nodeName];
}
}
return $(maxNode);
}
And then you can,
$('div.example').theMostChild().css('color', 'red');
A function like below should give you the count of child elements, from which you can get the max count. See below,
DEMO
$(function () {
var childs = {};
$('div.example').children().each(function () {
if (childs.hasOwnProperty(this.nodeName)) {
childs[this.nodeName] += 1;
} else {
childs[this.nodeName] = 1;
}
});
for (i in childs) {
console.log(i + ': ' + childs[i]);
}
});
That is not possible without some information about the expected types of child nodes.
EDIT : It is possible as Jasper pointed out that we need not know the tag names before hand. The following works in case you're looking only within a specific set of selectors.
var selectorArray = ['div', 'span', 'p',........]
var matches = $(div).children(selectorArray.join());
var max = 0, result = [];
$.each(selectorArray, function(i, selector){
var l = matches.filter(selector).length;
if(l > max){
max = l;
result[max] = selector;
}
});
result[max] gives you the tag name and max gives you the occurrence count