Get string position of current element? - javascript

Any way to get the ".indexOf()" string position of the start and end of a jQuery-selected element relative to the parent?
Example:
<ul><li>Coffee</li><li>Coffee</li><li>Coffee</li></ul>
After selecting the 2nd <li> item via jquery, the function would return 15 (or 29 with an argument switch).
I am thinking about a verbose javascript workaround but it gets nasty and unreliable very fast. Also it might not work if there are other tag types or text mixed in between. Bad. Not to take seriously.
//http://stackoverflow.com/a/14482123
function nthIndex(str, pat, n){
var L= str.length, i= -1;
while(n-- && i++<L){
i= str.indexOf(pat, i);
}
return i;
}
function strpos(jelem, tag) {
var nth = jelem.index();
var contents = jelem.parent().text();
var pos = nthIndex(contents, tag, nth+1);
if (tag.indexOf('/') == 1) {
return pos+4; //because ending of '</li>' is +4 (very bad)
} else {
return pos;
}
}
strpos(jelem, '<li>'); //or '</li>' to return the ending position

Like adeneo said, this seems like an XY problem, but you really need a parser and I doubt you feel like writing your own.
One possibility would be to use the native .outerHTML and sum the .length of all the .previousSibling nodes, and then the .length of current one if you need the end position.
However, you should know that this has nothing to do with the original HTML. These will be HTML strings rendered by the browser after analysing the current state of the DOM.
Here's a quick example that receives an element and returns the start (or optionally the end) position:
function getPos(origElem, doEnding) {
var sum = 0;
var elem = origElem;
while ((elem = elem.previousSibling)) {
sum += elem.outerHTML.length;
}
if (doEnding) {
sum += origElem.outerHTML.length-1;
}
return sum;
}
var li = document.querySelector("li:nth-child(2)");
var p = document.querySelector("pre");
p.textContent = "Position is: " + getPos(li);
p.textContent += "\nEnd position is: " + getPos(li, true);
<ul><li>Coffee</li><li>Coffee</li><li>Coffee</li></ul>
<pre></pre>

Related

Unusual browser error and advice on a Calculate Sum program for javaScript

So I have a problem for class:
Calculate Sums
Create a function called calculateSums that will accept an array of numbers, and return true if the sum of all the positive numbers is greater than or equal to the sum of the absolute value of all the negative numbers.
Method of completion
You should use a while loop in your solution.
It does not matter if you consider 0 a positive or negative number
because it will not change the sum of either side.
After your loop finishes, you should simply return whether the sum of
positive numbers is greater than or equal to the sum of negative
numbers.
It should be possible to call the function as follows:
calculateSums([-1,2]) returns true
calculateSums([-1,2,-3]) returns false
In your code, call your function several times and display the arrays used and the result on your HTML page.
I am having trouble figuring out how to use a while loop to do this. I have one, but it's not right. I am also trying to display the result using "document.getElementByID("message").innerHTML..." and I am getting an error I don't understand.
Here is my code:
/**
* This function calculates the absolute sum of an array of numbers
* #inputs a - an array of numbers
* #returns compare - a boolean
*/
function calculateSum(a) {
//declare variables and set them equal to 0.
var result = 0;
var possum = 0;
var negsum = 0;
var compare;
while (possum >= negsum) {
for (var i = 0; i < a.length; i++) {
var num = a[i];
result = result + Math.abs(num);
if (num%2 == 0) {
possum += result;
} else {
negsum += result;
}
result = 0;
}
if (negsum > possum) {
compare = false;
break;
} else {
compare = true;
break;
}
}
if (compare == true) {
document.getElementById("message").innerHTML = compare;
} else {
document.getElementById("message").innerHTML = compare;
}
return compare;
}
Here is my HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Calculate Sums</title>
<script src = "assignment3.js"></script>
</head>
<body>
<script>
calculateSum([-1,2,3,-2]);
calculateSum([-3,1,-5,2]);
</script>
<p id = "message"></p>
</body>
</html>
I would love someone to help understand the error and offer suggestions of a better way to do this.
Here is the error in the browser:
Uncaught TypeError: Cannot set property 'innerHTML' of null
at calculateSum (assignment3.js:34)
at calculateSums.html:12
I am still very new to coding, so I am often times finding myself frustrated. I really appreciate the help I've found on this site.
Update: I figured out how to fix the while statement.
while (possum >= negsum || negsum > possum)
I'm not sure this is exactly how my teacher imagined it being done because we also have these acceptance criteria:
Calculate Sums
Your code must include a function called calculateSums that accepts
an array of numbers and includes a return statement
Your function must include a while loop.
Your code must use Math.abs() to get the absoluate value of any
negative numbers.
Your code must NOT include an infinite loop.
Your code must conditionally return true or false.
Your code must include multiple variations of tests to show your
function works.
The primary problem in your code is as follows:
When the browser receives the HTML markup in your code, it is processed from top to bottom.
So based on the coed you have shared the code executes in the following order:
The head section loads first as it is on the top. And consequently, your external script assignment3.js also gets loaded. (now the function in the script is available on the global namespace)
<head>
<meta charset="UTF-8" />
<title>Calculate Sums</title>
<script src = "assignment3.js"></script>
</head>
The browser then moves on to the body
<body>
<script>
calculateSum([-1,2,3,-2]);
calculateSum([-3,1,-5,2]);
</script>
<p id = "message"></p>
</body>
First, your script tag executes
<script>
calculateSum([-1,2,3,-2]);
calculateSum([-3,1,-5,2]);
</script>
This works well until this line in the calculateSum function
document.getElementById("message").innerHTML = compare;
Because, at this point, your browser has not gotten around to rendering the <p> tag (the script comes before and executes before this can happen). So document.getElementById("message") can't find the p tag and ends up returning nothing (null). And when you try to access a property on null, you get this error Cannot set property 'innerHTML' of null.
So to fix that, (and as a best practice in general) it's best to put your scripts at the end of the body tag like this (and not in the head):
<body>
<p id = "message"></p>
<script src = "assignment3.js"></script>
<script>
calculateSum([-1,2,3,-2]);
calculateSum([-3,1,-5,2]);
</script>
</body>
This ensures that your page load is not blocked by scripts and also has the side effect that the DOM will have been rendered and ready when your code executes.
Coming to your assignment problem specifically, you can use a while loop to iterate over the array and solve it in a simple manner.
Some change notes:
Move DOM manipulation out of the calculateSum method. This method now has a single clear purpose of calculating the sum and return either true or false.
Write a new function runTestCases which basically creates an array of arrays for the different tests we want to run and runs the calculateSum method for each. It also updates the DOM to reflect the result as stated in your problem statement.
Used while loop to iterate over the array in calculateSum
/**
* This function calculates the absolute sum of an array of numbers
* #inputs a - an array of numbers
* #returns compare - a boolean
*/
function calculateSum(a) {
//declare variables and set them equal to 0.
var result = 0;
var possum = 0;
var negsum = 0;
var currentIndex = 0;
while (currentIndex < a.length) {
var e = a[currentIndex++];
if (e < 0) {
// Negative number
negsum += Math.abs(e);
} else {
// Positive number
possum += e;
}
}
return possum >= negsum;
}
function runTestCases() {
// Array of test cases. (You can add or remove tests as needed)
var tests = [
[-1, 2],
[-1, 2, -3]
];
// Get reference of the list element to show the results
var ul = document.getElementById("result");
// Iterate over the tests aray and update dom to show each result
for (var i = 0; i < tests.length; i++) {
var test = tests[i]; // Get the current test case
var result = calculateSum(test); // Get the result
// Create a new DOM element to display the result
var li = document.createElement('li');
li.innerHTML = JSON.stringify(test) + " <b> " + result + "</b>";
//Appenf newly created element to the list
ul.append(li);
}
}
runTestCases();
<div>
<ul id="result"></ul>
</div>
Here is an answer that I think covers all the requirements:
function calculateSums(nums) {
var posSum = 0,
negSum = 0,
i = 0,
num;
while (i < nums.length) {
num = nums[i];
if (num < 0) {
negSum += Math.abs(num);
} else {
posSum += num;
}
++i;
}
return posSum >= negSum;
}
Uncaught TypeError: Cannot set property 'innerHTML' of null
at calculateSum (assignment3.js:34)
at calculateSums.html:12
You have to call the calculateSum() function after the p tag. It is trying to assign value before it is declare. Put your script before the tag.

Error in javascript recursive function

I have the following piece of code (for the present case it may be considered as a function to remove the attributes from a valid html string fed as input):
function parse(htmlStr)
{
console.log(htmlStr);
result+="<"+htmlStr.tagName.toLowerCase()+">";
var nodes=htmlStr.childNodes;
for(i=0;i<nodes.length;i++) {
var node=nodes[i];
if(node.nodeType==3) {
var text=$.trim(node.nodeValue);
if(text!=="") {
result+=text;
}
}
else if(node.nodeType==1) {
result+=parse(node);
}
}
result+="</"+htmlStr.tagName.toLowerCase()+">";
return result;
}
But it is not working as expected. For example, in the following case when I feed it the following html as input:
<div id="t2">
Hi I am
<b>
Test
</b>
</div>
it returns <div>Hi I am<div>Hi I am<b>Test</b></div>.
Also the page crashes if some large input is given to the function.
NOTE: I know there are better implementations of removing attributes from a string using jQuery, but I need to work with the above function here & also the complete code is not for removing attributes, the above is just a shortened part of the code
There is something wrong with your result variable. It is undefined and global. In each recursion you would append the same string to itself, which also makes it crashing for huge inputs. (I can't reproduce anything, it crashes right away with a Undefined variable Error)
BTW: Your argument is no htmlStr, it is a domNode. And you're not parsing anything. Please don't use wrong self-documenting variable names.
Corrected version:
function serialize(domElement) {
var tagname = domElement.tagName.toLowerCase();
var result = "<"+tagname+">";
// ^^^ ^ not a +=
var children = domElement.childNodes;
for (var i=0; i<children.length ;i++) {
// ^^^ was also missing
if (children[i].nodeType == 3) {
result += children[i].data;
} else if (children[i].nodeType == 1) {
result += serialize(children[i]);
// ^^ add a child's result here
}
}
result += "</"+tagname+">";
return result;
}
I would not use trim(), that would produce <div>Hi<b>I</b>am</div> from <div>Hi <b>I</b> am</div>. You might do something like .replace(/\s+/g, " ").
This result+=parse(node); -> In you case you shouldn't merge the result inside recursion like that..
What happens is the return result from <b> recursion call appends the existing result with returned result. Where the existing result is <div>Hi I am and the returned result is <div>Hi I am<b>Test and so at the end of recursion you have <div>Hi I am<div>Hi I am<b>Test.
var result = '';
function parse(htmlStr) {
result += "<" + htmlStr.tagName.toLowerCase() + ">";
var nodes = htmlStr.childNodes;
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.nodeType == 3) {
var text = $.trim(node.nodeValue);
if (text !== "") {
result += text;
}
} else if (node.nodeType == 1) {
parse(node);
}
}
console.log(result);
result += "</" + htmlStr.tagName.toLowerCase() + ">";
return result;
}
Fixed fiddle: http://jsfiddle.net/FBnYT/
Change
result+="<"+htmlStr.tagName.toLowerCase()+">";
to:
var result="<"+htmlStr.tagName.toLowerCase()+">";
WOrks fine in demo: http://jsfiddle.net/qtuUA/
The crash occurs because the loop control variable is not locally scoped. So in addition to the other recommended changes:
for (var i = 0; i < nodes.length; i++)
...

Converting html to textual representation with preserved whitespace meaning of tags -- how?

Consider such html piece:
<p>foo</p><p>bar</p>
If you run (for example) jQuery text for it you will get "foobar" -- so it is raw text actually, not textual representation.
I am looking for some ready to use library to get textual representation, in this case it should be -- "foo\nbar". Or clever hints how to make this as easy as possible ;-).
NOTE: I am not looking for beautiful output text, but just preserved meaning of whitespaces, so for:
<tr><td>foo</td><td>bar</td></tr>
<tr><td>1</td><td>2</td></tr>
I will be happy with
foo bar
1 2
it does NOT have to be:
foo bar
1 2
(but of course no harm done).
Have you looked at the innerText or textContent properties?
function getText(element){
var s = "";
if(element.innerText){
s = element.innerText;
}else if(element.textContent){
s = element.textContent;
}
return s;
}
Example
Adds a PRE tag to the body and appends the body text.
document.body.appendChild(
document.createElement('pre')
)
.appendChild(
document.createTextNode(
getText(document.body)
)
);
Edit
Does using a range work with firefox?
var r = document.createRange();
r.selectNode(document.body);
console.log(r.toString());
Edit
It looks like you're stuck with a parsing function like this then.
var parse = function(element){
var s = "";
for(var i = 0; i < element.childNodes.length; i++){
if(/^(iframe|noscript|script|style)$/i.test(element.childNodes[i].nodeName)){
continue;
}else if(/^(tr|br|p|hr)$/i.test(element.childNodes[i].nodeName)){
s+='\n';
}else if(/^(td|th)$/.test(element.childNodes[i].nodeName)){
s+='\t';
}
if(element.childNodes[i].nodeType == 3){
s+=element.childNodes[i].nodeValue.replace(/[\r\n]+/, "");
}else{
s+=parse(element.childNodes[i]);
}
}
return s;
}
console.log(parse(document.body));
I started writing my own function probably at the same time as Zapthedingbat, so just for the record:
var NodeTypeEnum = { Element : 1,Attribute : 2, Text: 3, Comment :8,Document :9};
function doTextualRepresentation(elem)
{
if (elem.nodeType==NodeTypeEnum.Text)
return elem.nodeValue;
else if (elem.nodeType==NodeTypeEnum.Element || elem.nodeType==NodeTypeEnum.Document)
{
var s = "";
var child = elem.firstChild;
while (child!=null)
{
s += doTextualRepresentation(child);
child = child.nextSibling;
}
if (['P','DIV','TABLE','TR','BR','HR'].indexOf(elem.tagName)>-1)
s = "\n"+s+"\n";
else if (['TD','TR'].indexOf(elem.tagName)>-1)
s = "\t"+s+"\t";
return s;
}
return "";
}
function TextualRepresentation(elem)
{
return doTextualRepresentation(elem).replace(/\n[\s]+/g,"\n").replace(/\t{2,}/g,"\t");
}
One thing I am surprised with -- I couldn't get
for (var child in elem.childNodes)
working, and it is a pity, because I spend most time in C# and I like this syntax, theoretically it should work in JS, but it doesn't.

More efficient comparison of numbers

I have an array which is part of a small JS game I am working on I need to check (as often as reasonable) that each of the elements in the array haven't left the "stage" or "playground", so I can remove them and save the script load
I have coded the below and was wondering if anyone knew a faster/more efficient way to calculate this. This is run every 50ms (it deals with the movement).
Where bots[i][1] is movement in X and bots[i][2] is movement in Y (mutually exclusive).
for (var i in bots) {
var left = parseInt($("#" + i).css("left"));
var top = parseInt($("#" + i).css("top"));
var nextleft = left + bots[i][1];
var nexttop = top + bots[i][2];
if(bots[i][1]>0&&nextleft>=PLAYGROUND_WIDTH) { remove_bot(i); }
else if(bots[i][1]<0&&nextleft<=-GRID_SIZE) { remove_bot(i); }
else if(bots[i][2]>0&&nexttop>=PLAYGROUND_HEIGHT) { remove_bot(i); }
else if(bots[i][2]<0&&nexttop<=-GRID_SIZE) { remove_bot(i); }
else {
//alert(nextleft + ":" + nexttop);
$("#" + i).css("left", ""+(nextleft)+"px");
$("#" + i).css("top", ""+(nexttop)+"px");
}
}
On a similar note the remove_bot(i); function is as below, is this correct (I can't splice as it changes all the ID's of the elements in the array.
function remove_bot(i) {
$("#" + i).remove();
bots[i] = false;
}
Many thanks for any advice given!
Cache $("#" + i) in a variable; each time you do this, a new jQuery object is being created.
var self = $('#' + i);
var left = parseInt(self.css("left"));
var top = parseInt(self.css("top"));
Cache bots[i] in a variable:
var current = bots[i];
var nextleft = left + current[1];
var nexttop = top + current[2];
Store (cache) the jQuery object of the DOM element within the bot representation. At the moment it's been created every 50ms.
What I mean by this is that for every iteration of the loop, you're doing $('#' + i). Every time you call this, jQuery is building a jQuery object of the DOM element. This is far from trivial compared to other aspects of JS. DOM traversal/ manipulation is by far the slowest area of JavaScript.
As the result of $('#' + i) never changes for each bot, why not store the result within the bot? This way $('#' + i) gets executed once, instead of every 50ms.
In my example below, I've stored this reference in the element attribute of my Bot objects, but you can add it your bot (i.e in bots[i][3])
Store (cache) the position of the DOM element representing the bot within the bot representation, so the CSS position doesn't have to be calculated all the time.
On a side note, for (.. in ..) should be strictly used for iterating over objects, not arrays. Arrays should be iterated over using for (..;..;..)
Variables are extremely cheap in JavaScript; abuse them.
Here's an implementation I'd choose, which incorporates the suggestions I've made:
function Bot (x, y, movementX, movementY, playground) {
this.x = x;
this.y = y;
this.element = $('<div class="bot"/>').appendTo(playground);
this.movementX = movementX;
this.movementY = movementY;
};
Bot.prototype.update = function () {
this.x += this.movementX,
this.y += this.movementY;
if (this.movementX > 0 && this.x >= PLAYGROUP_WIDTH ||
this.movementX < 0 && this.x <= -GRID_SIZE ||
this.movementY > 0 && this.y >= PLAYGROUND_HEIGHT ||
this.movementY < 0 && this.y <= -GRIDSIZE) {
this.remove();
} else {
this.element.css({
left: this.x,
right: this.y
});
};
};
Bot.prototype.remove = function () {
this.element.remove();
// other stuff?
};
var playground = $('#playground');
var bots = [new Bot(0, 0, 1, 1, playground), new Bot(0, 0, 5, -5, playground), new Bot(10, 10, 10, -10, playground)];
setInterval(function () {
var i = bots.length;
while (i--) {
bots[i].update();
};
}, 50);
You're using parseInt. As far as I know, a bitwise OR 0 is faster than parseInt. So you could write
var left = $("#" + i).css("left") | 0;
instead.
Furthermore, I wouldn't make use of jQuery functions to obtain values like these every 50 ms, as there's always a bit more overhead when using those (the $ function has to parse its arguments, etc.). Just use native JavaScript functions to optimize these lines. Moreover, with your code, the element with id i has to be retrieved several times. Store those elements in a variable:
var item = document.getElementById(i);
var iStyle = item.style;
var left = iStyle.left;
…
(Please note that I'm not a jQuery expert, so I'm not 100% sure this does the same.)
Moreover, decrementing while loops are faster than for loops (reference). If there's no problem with looping through the elements in reverse order, you could rewrite your code to
var i = bots.length;
while (i--) {
…
}
Use offset() or position() depending on if you need coordinates relative to the document or the parent. position() is most likely faster since browsers are efficient at finding offsets relative to the parent. There's no need for parsing the CSS. You also don't need the left and top variables since you only use them once. It may not be as readable but you're going for efficiency:
var left = $("#" + i).position().left + bots[i][1];
var top = $("#" + i).position().top + bots[i][2];
Take a look here for a great comparison of different looping techniques in javascript.
Using for...in has poor performance and isn't recommended on arrays. An alternative to looping backwards and still using a for loop is to cache the length so you don't look it up with each iteration. Something like this:
for(var i, len = bots.length; i < len; i++) { ... }
But there are MANY different ways, as shown in the link above and you might want to test several with your actual application to see what works best in your case.

Cannot confirm that element exists on the page when I clearly see it there

I'm using a function which utilizes jQuery in order to grab information from a JSON feed. The problem here is that from the feed I must pick 10 items that meet the criteria of being within the last year (31 billion milliseconds from the request for argument's sake) and I have to specify how many results I want from the feed with a variable 'maxRows' that is inserted into the URL. Here's the function...
function topTen(rows) {
$.getJSON("http://ws.geonames.org/earthquakesJSON?north=90&south=-90&east=-180&west=180&maxRows=" + rows,
function(json) {
var topTen = new Array();
var now = new Date();
var i;
for(i = 0; i < json.earthquakes.length; i++)
{
var time = json.earthquakes[i].datetime;
var then = new Date(time.replace(" ", "T"));
if(now - then < 31536000000) { topTen.push(json.earthquakes[i].eqid); }
}
if(topTen.length >= 10)
{
var html = "The Top Ten Earthquakes Of The Past Year<ol>";
for(i = 1; i <= 10; i++)
{
html += "<li id='number" + i + "' >" + topTen[i - 1] + "</li>";
}
html += "</ol>";
$('#top_ten').html(html);
}
});
}
Now the problem is that from the first request it is likely I will not get 10 results that meet my criteria. So in order to counteract this I try to put the function in a loop until another criteria is met. However, this always winds up failing because the getJSON function (or perhaps the callback) is asynchronous, meaning if I try something like
var rows = 10;
do{
topTen(rows);
rows += 10;
while(!document.getElementById("number10"))
The problem then becomes, however, that the function doing the actual work is not bound by the line-by-line timing of the loop and so the loop itself runs many, many, MANY times before any of the functions actually finish and the loop condition is met. So right now I'm trying to devise another approach that goes something like this
topTen(rows);
rows += 10;
pause(1000);
topTen(rows);
rows += 10;
pause(1000);
topTen(rows);
rows += 10;
pause(1000);
if(document.getElementById("number10"))
alert("There's 10!");
else
alert("There's not 10!");
The pause is basically just what it sounds like and takes in milliseconds. A simple comparison of an initial date object to later date objects in a loop that I copied and pasted. This works to keep the functions from firing off immediately after one another, but then the problem becomes that the if condition is NEVER met. I don't know what it is, but no matter how much time I allow for pausing, the getElementById function never seems to find the element with an id of 'number10' even though I can see it very clearly in Firebug.
I've have been crashing my browser SEVERAL times because of this problem and I am seriously getting PO'd and sick of it. If anyone could find a solution to this problem or even suggest an easier, more elegant solution, I would be eternally grateful.
PS - I've tried things like global variables and using recursion to call topTen() from inside the callback function and send in a larger 'rows' variable, but those don't work because it seems like the callback functions are in their own contained little world where 90% of the rest of my javascript doesn't exist.
You are doing this the wrong way...
You need to wait for one call to return before calling again. Lucky for you, you already have a function being called with it returns. So a simple change to that function and you are done.
var topTenList = new Array();
function topTen(rows) {
$.getJSON("http://ws.geonames.org/earthquakesJSON?north=90&south=-90&east=-180&west=180&maxRows=" + rows,
function(json) {
var now = new Date();
var i;
for(i = 0; i < json.earthquakes.length; i++)
{
var time = json.earthquakes[i].datetime;
var then = new Date(time.replace(" ", "T"));
if(now - then < 31536000000) { topTenList.push(json.earthquakes[i].eqid); }
}
if (topTenList.length < 10)
{
topTen(rows+10);
return;
}
else
{
var html = "The Top Ten Earthquakes Of The Past Year<ol>";
for(i = 1; i <= 10; i++)
{
html += "<li id='number" + i + "' >" + topTenList[i - 1] + "</li>";
}
html += "</ol>";
$('#top_ten').html(html);
}
});
}

Categories