How to turn a very long column into multiple shorter ones? - javascript

This is a challenge question / problem. Hope you find it interesing.
Scenario: You have a very long list (unreasonably long) in a single column. It would be much better displayed in multiple shorter columns. Using jQuery or another tool, what do you do?
The format of the list is as follows:
<div class="toc">
<dl>
<dt>item 1</dt>
<dd>related to 1</dd>
<dt>item 2</dt>
<dd>related to 2</dd>
<dt>item 3</dt>
<dd>related to 3</dd>
<dt>item 4</dt>
<dd>related to 4</dd>
<dt>item 5</dt>
<dd>related to 5</dd>
<dt>item 6</dt>
<dd>related to 6</dd>
<dt>item 7</dt>
<dd>related to 7</dd>
<dt>item 8</dt>
<dd>related to 8</dd>
<dt>item 9</dt>
<dd>related to 9</dd>
<dt>item 10</dt>
<dd>related to 10</dd>
</dl>
</div>
Caveat: The dd's may contain nested dl's, dt's, & dd's.
Also be sure to keep related items in the same column (ie. if dt 7 is col x, so should dd 7).
This problem inspired by the somewhat ridiculously laid out Zend Framework manual.
Edit: See below for answer.

i would do a count of the array of $("dt") then if it's over a certain size inject a closing and opening then using styling to float them into columns.
Josh

Ignoring:
Also be sure to keep related items in the same column (ie. if dt 7 is col x, so should dd 7).
one possible solution could be column-count, e.g:
http://pici.se/pictures/small/vekPcSkFE.png
However, it's a part of CSS 3 so browser support ... you know. :(

I would first write the complete data in a json then check the amount of dt's + dd's + related children amount by considering this sum as total row amount.
Then i would check the predefined row amount per column and divide this json into proper parts.
And finally from this parts i'd create columns without breaking related items.
Sinan.

Answer So this problem is more difficult than it first appears.
My initial thought was that I would wrap the column in <table><tr><td></td></tr></table> and then after every nth parent dd output a </td><td>. Simple, right? Except you can't really output a closing tag like that. Ultimately when you write $('</td><td>').after('.toc > dl > dd'), you'll be creating nodes - that's nodes with opening and closing tags. Since you must create nodes, the browser will ignore the first closing tag.
Well, let's say that you solve that problem somehow. What are you're iteration conditions? My first attempt was to construct a for loop. It seems reasonable. For every nth parent dd, do whatever you need to do. However, how do you construct those conditions in jQuery? You have less than, greater than, and equal. But you don't have greater than or equal to (>=) or less than or equal to (<=), and this is a critical distinction.
You might try something like this:
for (i = 0; i < len; i+=40) { // where 40 determines the # per col
get elements i thru i+40;
wrap this set and float left
}
So how would you do this in jQuery?
// note that you must use > to prevent nested descendants from being selected
var len = jQuery('.toc > dl > dd').size()
for (i = 0; i < len; i+=40) {
// this selector says give me the first 40 child elements
// whatever type of element they may be
$('.toc > dl > *:gt('+i+'):lt('+(i+40)').wrapAll('<div style="float:left">');
// however because we don't have >= and :gt won't accept negatives as an input
// we must either do a special case for the first iteration
// or construct a different selector
$('.toc > dl > *:eq('+i+')', ' +
'.toc > dl > *:gt('+i+'):lt('+(i+40)')
.wrapAll('<div style="float:left">');
}
You could also do something with jQuery's add() method to add the first element of each iteration to your set, but you must maintain document order in your selection or jQuery will rearrange the set, so you have to do that first.
Ultimately, the for loop made sense initially, but it ran into problems with challenging selectors. Of course, we're not using the $('selector').each(function () { }); construct because that would only be useful if we could output independent closing tags.
So what did I end up with? Final Answer:
$('.toc').after('<div id="new"></div>');
do {
var curSet = $('.toc > dl > *:lt(40)')
.appendTo('#new').wrapAll('<div style="float:left"></div>');
} while (curSet.size());
This approach appends a new div after the old one. Then iteratively grabs the first 40 elements from the old and appends them to the new div after wrapping them in a div that will float left, looping as long as there are elements left to grab, and it maintains order.
Not terribly complicated after you figure it out, but there were a few gotcha's throughout the problem that I hope you find interesting. I did.
To finish up the ultimate goal of making the documentation significantly more useful:
I added some style and used the dt's as togglers to show the dd's. I also used a simple php proxy wrapper (5-10 LOC) so I could bring in any given, desired doc page thru an ajax call without remote ajax warnings.
I ended up with a nice little document in a single, navigable page that loads in < 2 secs (and uses ajax to load all subsequent pages in < 1 sec) rather than a monstrous page that takes 15-20 sec to load per page!
Problem solved. Something much more enjoyable and useful in 10-15 lines of javascript (total with the reorganizing, toggling, and ajax code), < 10 lines of PHP, and a few style rules.
No more slow Zend docs and endless scrolling.

Related

Count how many times a specific value appears in a table

I've an html table that contains a list of names within sentences, which are inside <td> elements (i.e. <td>Mike (Jeremy's father) used to play piano</td> or <td>The only one who speaks french is Desmond, but Mike speaks very well spanish</td>). I would like to count how many times a specific name appears within the entire table. I need this to be done through javascript/jquery, and not via php.
Any help would be appriciated!
You could use
var nbMikeInTheTable = (' '+$('table').text()+' ').split('Mike').length-1;
(the added ' ' are for the compatibility with the buggy implementation of split on IE)
Or, if you want to be sure it's the entire word and it's not cut by HTML
var nbMikeInTheTable = $('table').html().split(/\W+/).filter(
function(v){return v=="Mike"}
).length;
If you want to guard against end of words and end of cells and be sure it's not part of an attribute of a td, it's a little more complex :
var nbTimesMikeIsInTheTable = $('table td').map(function(){
return $(this).text().split(/\W+/).filter(function(v){return v=="Mike"}).length
}).toArray().reduce(function(p,v){return p+v});
This isn't elegant but is probably not needed ;)
Demonstration (open the console)

Finding a Nested <li>, and modifying it

I'm writing a very conflated Android Unit Conversion application, using all kinds of terrible things to make it pretty. Essentially its executing JS via vebviews, and displaying html formatted according to arbitrary size lists in fancy spinners, with abbreviations underneath... blah blah...
Anyway. Right now I've got somthing like this:
for (l = 0; l < this.slotData.length; l += 1) {
out += '<li>' + this.slotData[l].values[i] + " "+'<li class="sw-right">'+this.slotData[l].abbreviations[i]+'</li></li>';
}
Where slotData[l] represents say "Milimeters" and abbreviations is "Mm", which is underneath, and formatted slightly differently. The idea is that you can enter numbers, and they will show up, underneath the Large Milimeters, next to the small Mm, so that you can see "4400mm".
I'm trying to access and modify the child li, of class "sw-right" at a different point in my code. Wondering if anyone knows an easy way to do this? I can access the parent without trouble, but I'm not sure what the proper way to go from there is...
My access is somthing like this:
this.slotEl[slot].......
Anythoughts?
Thanks guys...
Nathaniel.
You should not try to nest <li> elements. HTML syntax does not permit nesting list items. Since the </li> closing tag is optional, a browser might well just treat your nested item as a sibling of the first one anyway, and discard the second close tag as a double-close on the second item.
Addional: Since you are trying to compose a list item of two parts, maybe what you really want is a definition list. For setting attributes on ranges nested inside of tags, consider using span.
I suggest using jquery:
$('li.sw-right').html('html_you_want_set');
you could use
var myNestedLI = document.getElementsByClassName( 'sw-right' )[ 0 ]
or
var topElements = document.getElementsByTagName( 'li' );
var nestedElements = []
for( var i = 0; i < topElements.length; i ++ )
{
var theseElements = topElements[ i ].getElementsByTagName( 'li' )
for( var j = 0 ; j < theseElements.length; j ++ ) nestedElements.push( theseElements[ j ] )
}
// nestedElements now contains all nested LI elements
As Sparky pointed out, you can't nest <li> tags like that. The second li will be treated as a separate list item from the first, making them siblings. Instead what you want are two separate <div>s inside your <li>. Div is a generic block-level element. (Block-level means there's a line break between them, and generic means that the div's meaning and style is entirely up to you.)
I'd suggest this more descriptive markup:
<li class="calculated-value">
<div class="unit">*large unit name*</div>
<div class="value">*small value with abbreviation*</div>
</li>
In your CSS, the following rule will make the value div display smaller:
li.calculated-value div.value { font-size: 75%; }
(You can add additional rules to give it an indentation, better spacing, bold or color settings, etc. And you can add another line for div.unit that styles that one separately.)

IE8 not displaying JSON data using JQUERY in a list

I'm developing an asynchronous database searching tool. It currently works with firefox and chrome, but has one enormous hiccup with internet explorer (version 8).
Students can enter their prospective MCAT and GPA scores and then jquery sorts out the schools where they place in the top 25% or the middle 50%. Basically, it's a neurotic premed student's dream (or nightmare).
The jquery cycles through the JSON data, displaying each item that matches the criteria in a <li> item. Again, it works great in ff and chrome, but in internet explorer it refuses to display the list items. It does, however, display the proper count of items, which means that the json data is going through properly.
After searching through stackoverflow I saw some commentary (colorful, often!) on how IE refuses to allow placing elements in tables and some other innerhtml elements using jquery.
I'm wondering if this is the problem, and though I found a similar problem on this question I can't quite figure out how to adapt it for my project (I'm new to javascript).
Any help would be wonderful. The code can be found below.
-samuel
$.getJSON("schoolgrabber.php?jsoncallback=?", function(data){
//loop through the items in the array
for(var x=0; x < data.length; x++){
if( MCAT >= data[x].TopPercentileMCAT && data[x].TopPercentileMCAT!=''){
var li = $("<li>").appendTo("#schoollist");
var school= data[x].School;
//add the actual information into the li using a chain event
//the event begins by appending an <a> tag, with the name of the school inside (text(data[x].School)
//then it adds a unique id, which is the iteration number the counter is on above (x)
//then it adds the url, and adds the school variable, which allows the schools to be clicked over to the results page
//then it puts all of that inside li, as declared above (slightly backwards, but hey)
$("<a>").text(data[x].School).attr("id",x).attr("href", "results.php?school=" + school).appendTo(li);
$("#schoollist li").addClass("school");
var quantity = $(".school").length;
$('#schoolquantity').empty();
$('#schoolquantity').append(quantity);
}}
});
Instead of using jQuery and chaining to build up your DOM, try instead to build out a string of the HTML you want rendered and add that completed string just the one time.
Not only might it fix your bug, but you'll also get better performance. What I'm doing is building up the list's full HTML and calculating the quantity. Then, when I'm completely finished building the HTML, I add it to the DOM. IMO the way I have below is also more readable. Note that I haven't tested this, but you should get the idea:
$.getJSON("schoolgrabber.php?jsoncallback=?", function(data){
//loop through the items in the array
var html = [];
var parentElement = $("#schoollist");
var quantity = 0;
for(var x=0; x < data.length; x++){
if( MCAT >= data[x].TopPercentileMCAT && data[x].TopPercentileMCAT!=''){
var school= data[x].School;
html.push("<li class=\"school\">");
html.push(" <a id=\"" + x + "\" href=\"results.php?school=" + school "\">");
html.push( data[x].School);
html.push(" </a>");
html.push("</li>");
quantity++;
}
}
parentElement.html(html.join(""));
$('#schoolquantity').html(quantity);
});
Remember that everytime you alter the DOM the browser has to do a bunch of stuff to update the webpage. This is costly. You want to avoid adding/grabbing from the DOM as much as possible and instead "batch" your alterations (As a side note, if you want to alter the DOM alot without "batching", take a look at the jquery detach method).
Good luck, hope this works for you!

Why does replaceChild() behave oddly when replacing one kind of element with another?

I am relatively new at javascript, and found an interesting behavior that I can't explain today. I have a custom <hr> (with an image) on a website, which displays oddly in IE7 and below. To overcome this, I wanted to use replaceChild() in combination with getElementsByTag(). Initially, I simply tried to loop over the list, so:
var hrules = document.getElementsByTagName('hr');
for (var i=0; i < hrules.length; i++) {
var newHrule = document.createElement("div");
newHrule.className = 'myHr';
hrules[i].parentNode.replaceChild(newHrule, hrules[i]);
document.write(i);
}
However, this does not work: it actually only gets half the elements, skipping every other one. Printing i gives half-integer values of the actual number of <hr> elements in the document (e.g. if there are 7 <hr/> elements, it prints 4. By contrast, the following does work:
var hrules = document.getElementsByTagName('hr');
var i = 0;
while (i < hrules.length) {
var newHrule = document.createElement("div");
newHrule.className = 'myHr';
hrules[i].parentNode.replaceChild(newHrule, hrules[i]);
document.write(i);
}
i is printed the same number of times as there are hrules in the document (but of course is always 0, since I'm not incrementing it), and the hrules are replaced correctly. I recognize that the while here might as well be while(true)--it's just going until it runs out of <hr> elements, but appears to stop after that (it's not printing any more 0s).
I've tried this with a number of different types of elements, and observed that this only occurs when replacing one kind of element with another. I.e., replacing p with div, span with p, etc. If I replace p with p, div with div, etc. the original example works correctly.
Nothing in the documentation I've found (w3schools, various Google search, here, etc.) suggests an obvious answer.
What is going on here? First, why does the second example I offered work - is replaceChild() iterating over the elements automatically? Second, why is the behavior different for different types of element?
document.getElementsByTagName is a live access to all the HR elements in the document - it's updated whenever you change the document. You don't get a snapshot of all the HRs in the document whenever you call it.
So, with the first code, you are both incrementing i and reducing the size of hrules.length each time round the loop. This explains why you only see half the steps you expect.
Here's the solution I ended up using, in case anyone else (like #Pav above) is curious.
var hrules = document.getElementsByTagName('hr');
/* Each repetition will delete an element from the list */
while (hrules.length) {
var newHrule = document.createElement("div");
newHrule.className = 'ieHr';
/* Each iteration, change the first element in the list to a div
* (which will remove it from the list and thereby advance the "head"
* position forward. */
hrules[0].parentNode.replaceChild(newHrule, hrules[0]);
}
Essentially, what happens is you get a list of all the hrules in the document. This list is dynamically updated as you interact with it (see Matthew Wilson's answer). Each time you change the first element of the list to a div, it gets removed from the list, and the list is updated accordingly. The result is that you simply need to act on the first element of the list each time until the length of the list is 0.
That's admittedly a little counterintuitive, but it's how the list works.

How do I select arbitrary text on the page using javascript?

Let's say I have a contentEditable div, to the user can edit and change the text and elements inside it. How do I arbitrarily change the selection in this div with javascript? By "change" I don't mean "change the contents of whatever the user has selected", I mean actually change what is selected. The user should then be able to type over the selection, replacing it with something else.
This should take into account that I may want to select text across elements. For instance:
<p>Some text <span>goes</span> here.</p>
I may for instance want to select "Some text go", or everything inside the <p>.
This only needs to work in Safari/Webkit.
Thanks in advance. As a native code developer, I find the DOM and Javascript in general quite frustrating.
Just to answer my own question in detail so anyone searching for something similar doesn't have to go looking elsewhere...
The code I ended up using was something like this:
var range = document.createRange();
range.setStart( <get the node the selection starts in>, <char offset in that node> );
range.setEnd( <get the node the selection ends in>, <char offset in that node> );
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
Big thanks to James Black for pointing me in the right direction.
Unless you need to write your own, you may want to look at tinyMCE, http://tinymce.moxiecode.com/, as it is a nice WYSIWYG editor in javascript.
In order to do this you will probably want to look at something like this:
http://codingtricks.blogspot.com/2009/03/javascript-select-partial-text-in-div.html
These may also be helpful:
JavaScript ranging gone wrong
https://developer.mozilla.org/en/DOM/window.getSelection
What you are trying to do will be complex, as you will need to take the selected area, remove all the tags, then put in the tag that you want for the selected area.
You can use document.getElementById('your_text_id').setSelectionRange(start, end); and you can use Math.random() to generate random numbers for start and end
While #Lucas's answer is good, there is a lot missing that would allow you to successfully use this. The node the selection starts in has to be the exact node, not a parent. In our case we were trying to put some text into a TextAngular control, then select text that looked liked ____ so the user could "fill in the blank".
Our input was html of the order <p>Some text goes here: _____</p> or
<p>Some partial goes here
<ul>
<li>Option 1</li>
<li>_____</li>
</ul>
To get this to work, we had to write something to find the underscores in the right element
function find_(node) {
var i, n;
// return the node with the _'s
for(i=0; i < node.childNodes.length; ++i) {
n = node.childNodes[i];
console.debug(n);
if(n.textContent) {
console.debug(n, n.textContent);
if(n.textContent.search(/___+/) > 0) {
return n;
}
}
if(n.childNodes) {
console.debug(n, n.childNodes);
n = find_(n);
if(n) {
return n;
}
}
}
return null;
}
So in the end, finding the node to satisfy <get the node the selection starts in> was a lot more work than that simple sentence led me to believe.
In the <ul> case. the node that contains the ____ is firstChild of the li node.
I've put this here to help others that need to do this not wonder why they are getting the error message
IndexSizeError: Failed to execute 'setStart' on 'Range': There is no child at offset 65.
When the problem is they are just looking at the wrong node.

Categories