Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 15 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
15
Dung lượng
555,98 KB
Nội dung
Licensed to JamesCarlson@aol.com Lists, Trees, and Tables 307 <li><span>Dot-Com millionaires</span> <ul> <li>Joel Mynor</li> ⋮ The tree can nest as far as is needed—just repeat the structure inside the appropriate child list item. Because it’s nice and consistent, you can easily generate the HTML on the server. With the list on the page, the next step is to pretty it up with some CSS. That’s in your court, but our code will add a few extra classes you can use to customize the display. The handle class will be assigned to the element we’ll insert to act as the toggle handle. When a branch of the tree is opened, it’ll receive the opened class; otherwise it’ll have the closed class. We’ve used these classes below to add a CSS sprite, which will change between a plus sign and a minus sign: chapter_08/04_expandable_tree/script.js (excerpt) .handle { background: transparent url(tree-handle.png) no-repeat left top; display:block; float:left; width:10px; height:10px; cursor:pointer; } .closed { background-position: left top; } .opened { background-position: left -10px; } The code for the tree is remarkably simple, thanks to the recursive nature of a tree: we just have to do one small piece of code, and attach it to each subcategory. Our plan of attack for creating the expanding/collapsing tree effect is to first hide all of the nested ul categories. Then we’ll add in a new element before the category title that contains a click event handler—this will open and close its branch: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 308 jQuery: Novice to Ninja chapter_08/04_expandable_tree/script.js (excerpt) $('#celebTree ul') .hide() .prev('span') .before('<span></span>') .prev() .addClass('handle closed') .click(function() { // plus/minus handle click }); Six chained actions. Bet you’re feeling some of that jQuery power coursing through your veins right about now! Here we see where consistent markup helps us out: in each subcategory list we look for the previous span element—that’s the subcategory title. Then we insert a new span element right before the title. Because our handle was added before the title, we need to move back to it with the prev action. We add the handle and closed (it’s closed by default because of the hide action) classes to it, and an event handler for when it’s clicked. At this stage the tree will be fully collapsed, with our brand-new handle prepended to the titles. All that’s left to do is toggle the branches when we click on it: chapter_08/04_expandable_tree/script.js (excerpt) // plus/minus handle click $(this) .toggleClass('closed opened') .nextAll('ul') .toggle(); When the handle is clicked, we toggle the closed and opened classes with toggleClass. If you specify multiple class names with toggleClass, any specified class that exists on the element is removed, and any that are absent from the element are added. Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Lists, Trees, and Tables 309 Advanced toggleClass Another neat trick of toggleClass is that it accepts a second parameter: a Boolean value that specifies whether the class should be added or removed. This might sound strange, but it’s a nice shortcut. Consider this code: if (x == 1) { $(this).addClass('opened'); } else { $(this).removeClass('opened'); } With the toggleClass(class, switch) syntax, we can replace the if statement with the following concise syntax: $(this).toggleClass('opened', x == 1); Finding the subcategory that we need to open and close is easy, thanks to the nextAll action. jQuery will check the next sibling, see that it’s a span element (the category title), filter it out based on our expression, and move to the next sibling … which is a ul item. Bingo! We just toggle this and the tree swings open and closed. Event Delegation Event delegation is a topic that’s applicable everywhere, but is particularly important if you’re dealing with large trees. The idea is that instead of applying individual event handlers to every node in your tree, you apply a single event handler to inter- cept the click—and then figure out who the click was aimed at and run the appro- priate action. If you’ve been following along closely this might sound a bit familiar to you. We covered the live method in the section called “Prepare for the Future: live and die” in Chapter 6. live handles event delegation for you—that’s the magic that makes it possible—but it comes with a potential gotcha that you need to be aware of. To acquire a better understanding of how event delegation works and why it is im- portant, let’s use it for real. We’ll start with the following HTML that displays our products in a categorized list: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 310 jQuery: Novice to Ninja chapter_08/05_event_delegation/index.html (excerpt) <span>Selection:</span> <span id="current"> Choose a celebrity </span> <ul id="picker"> <li class="category"> <span class="title">A-List</span> <ul> <li>Beau Dandy</li> <li>Glendatronix</li> </ul> </li> <li class="category"> <span class="title">B-List</span> <ul> <li>Mo' Fat</li> <li>DJ Darth Fader</li> </ul> </li> </ul> When users clicks on a celebrity, their selection should appear above the list in the format “category > celebrity” as in Figure 8.4. So what’s the best way to capture this information? Figure 8.4. Delegating events If we added a click event handler to every single list item—$('#picker li').click(…) —we could end up with hundreds of handlers. This would severely impact performance, as the browser would need to keep track of them all and check Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Lists, Trees, and Tables 311 them all whenever a click occurred on the page. With event delegation, we add our lone event handler to the parent of the list, and then access the target of the event to determine the element that was actually clicked on. The target property of an event is the actual DOM element—so it needs to be wrapped in the jQuery selector to obtain a jQuery object: $('#picker').click(function(e) { $('#current').text($(e.target).text()); }); Our list acts as if each item has its own handler! Nice, but what was the gotcha mentioned earlier? Event delegation works because of event bubbling (which we looked at in the section called “Event Propagation” in Chapter 5)—the events will bubble up until our parent handler catches them. The problem occurs if a handler catches the event before the parent and stops the event from propagating (with e.stopPropagation, or "return false"). If the event is stopped on its way up, event delegation will fail! That’s why it’s important that you know how events are being handled under the hood—it’ll save you a lot of headaches when dealing with otherwise incomprehensible bugs. We’ve handled any clicks with a single handler, but we now need to find out a bit more about where the element is located. Specifically, how can we find out which category the element is in? How about this: (excerpt) $('#picker').click(function(e) { var celebrity = $(e.target).text(); var category = $(e.target) .closest('.category') .find('.title') .text(); $('#current').text(category + " > " + celebrity); }); We’ve asked the closest method to find the closest element with the category class . If the element itself doesn’t have that class, closest will check its parent … and so on until it finds a matching element. This saves us having long strings of parent().parent().parent(), and also lets us be more flexible in how we structure our HTML. Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 312 jQuery: Novice to Ninja Tables If HTML lists are the unsung heroes of the new Web, tables are that bad kid who ends up turning out good. The tables themselves were never to blame—we misused and abused them for years as a hack to lay out our web designs in a reasonable cross- browser manner. But that’s what CSS is for, not poor old tables. And now that CSS has come of age, we can finally return to using tables solely for their original purpose: displaying tabular data. StarTrackr! has stacks of data to display. So much so that it’s growing out of hand: the tables are becoming overly large and unreadable, the information has no paging or sorting mechanisms, and there’s no way to edit the information easily. We saw how easy it was to add zebra striping and row highlighting to tables in Chapter 2; this will give us a few quick wins, but to address the more serious table issues, we’re going to need some extra help from jQuery. Fixed Table Headers The header row of a table is of paramount importance: without it, you’d be stuck with rows of meaningless data. But if you’re dealing with a large number of rows, you’ll find that the headers become less and less helpful, as they scroll out of sight and out of mind. Paging the data generally takes care of the problem—but if you need to have all the data on one page at the same time, you’ll have to think of another option. Keeping the header row fixed at the top of the table is an effective way to keep track of what our columns are about, and it also looks really cool! As the user scrolls the table to reveal new data, the header follows along. You can see this in action in Figure 8.5. Figure 8.5. Fixed header row Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Lists, Trees, and Tables 313 If your table is the only element on the page, position: fixed can be used to affix the thead element in place. However, position: fixed can only position an element relative to the viewport, rather than its containing element. This means that for tables contained inside other elements (which will almost always be the case), we need to turn to jQuery. Let’s have a look at how we can achieve this effect. Our markup is the same Celebrities table we added zebra-striping to back in Chapter 2: chapter_08/06_fixed_table_headers/index.html (excerpt) <table id="celebs"> <thead> <tr> <th>Id</th> <th>Name</th> <th>Occupation</th> <th>Approx. Location</th> <th>Price</th> </tr> </thead> <tbody> ⋮ </tbody> </table> Moving the thead around is tricky. Some browsers let you move it with impunity, while in others it’s surprisingly resistant to styling. So we’ll employ a trick: we’ll duplicate the contents of the thead in the form of an unordered list, styled to look exactly like the thead. Then we’ll give that position: absolute;, and move it around the screen as the user scrolls. We start by creating a TABLE widget to hold our code, with a fixHeader method that we’ll use for our fixed headers effect. The method will expect to receive a selector string pointing at a table on the page. We start by storing a few selections inside variables and in data, to speed up our subsequent code: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 314 jQuery: Novice to Ninja chapter_08/06_fixed_table_headers/script.js (excerpt) var TABLE = {}; TABLE.fixHeader = function(table) { $(table).each(function() { var $table = $(this); var $thead = $table.find('thead'); var $ths = $thead.find('th'); $table.data('top', $thead.offset().top); $table.data('left', $thead.offset().left); $table.data('bottom', $table.data('top') + $table.height() - ➥$thead.height()); ⋮ We first declare a closure to hold on to our widget’s context. Then we select any tables that match the selector passed in to our method. We loop over them with each, and store a reference to the table itself ($table), the thead ($thead), and the collection of th elements it contains ($ths). Finally, we store the left and top offsets of the $thead, as well as the location of the bottom of the table (to avoid the header moving past the bottom!). Use each When Writing Selector-based Functions When writing this sort of utility function, you should always anticipate the pos- sibility of your selector returning more than one element. In this case, our page only has one table on it, so the method would work fine if we omitted the each loop. But in the interests of preparing for the future, and making our code reusable, we’ve included the loop anyway; even if the table selector returns multiple tables, our function will handle them all with grace. Next, we create our faux header—a ul—and copy over the contents of the table header: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Lists, Trees, and Tables 315 chapter_08/06_fixed_table_headers/script.js (excerpt) var $list = $('<ul class="faux-head"></ul>'); $ths.each(function(i) { _th = $(this); $list.append($("<li></li>") .addClass(_th.attr("class")) .html(_th.html()) .width(_th.width()) .click(function() { _th.click() }) ).hide().css({left: _table.left}); }); $('body').append($list); With the real th elements collected in $ths, we use an each action to go through each one and craft our mimics. We copy the original elements’ class, HTML con- tents, width, and click event handlers. Some of this is unnecessary in our simple example, but it’s good to be prepared! After the list is fully populated, we hide it and position it directly over our real thead before slipping it into the page. Append as Infrequently as Possible You may wonder why we wait until the list is fully constructed before appending it to the page. While appending the list to the page first, and subsequently append- ing each item to it would have the same desired effect, the method we’ve chosen to adopt executes much more quickly in the browser. Every time you insert a new element into the DOM, the browser needs to recalculate the position of every element on the page. If you do this a lot (especially if you do it in a loop!), your script can become very slow. The method we’ve used above—storing the new elements in a variable, processing them as necessary, and then appending them all in one fell swoop—ensures optimal performance. With our thead mimic now nicely in place, we need to react to the scroll event and position it appropriately: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 316 jQuery: Novice to Ninja chapter_08/06_fixed_table_headers/script.js (excerpt) $(window).scroll(function() { setTimeout(function() { if ($table.data('top') < $(document).scrollTop() && ➥$(document).scrollTop() < $table.data('bottom')) { $list .show() .stop() .animate({ top: $(document).scrollTop(), opacity: 1 }); } else { $list.fadeOut(function() { $(this).css({top: $table.data('top')}); }); } }, 100); }); We set the timeout for 100 milliseconds after the scroll event fires; it’s a very short time, but enough to ensure that we avoid constantly animating as the user scrolls. We check to see if we’ve scrolled the thead out of the viewport, but not past the bottom of the table; if we have, we reveal our mimic and animate it to the correct position. If we’ve scrolled back high enough so that the original thead is visible, or down past the bottom of the table, we fade out the impostor list (and position it back at the top, so that it animates from the correct position when it reappears). And there you have it! We can call our TABLE.fixHeader("#celebs") and scroll the page, and the new “thead” follows along to keep the identifying labels visible at all times. Repeating Header Another approach to the disappearing header row problem is to simply repeat the header at regular intervals. This is particularly handy if the intention is for the data to be printed out—as cool as it looks, our animated table header is unhelpful if you need to sort through a dozen pages of printed tables! The goal would be to take the first row of the table, and copy it every, say, ten rows. The result is shown in Figure 8.6. Licensed to JamesCarlson@aol.com [...]... huge long table can be quite scary to encounter on a web site—especially as screen real estate is so valuable Adding paging to a table lets us display a small subset of the data at any one time, while allowing the user to move through it easily with navigation buttons Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Data Grids 320 jQuery: Novice to Ninja Pagination is often handled... accepts the selector string for our table, and a number representing how many rows to leave between each repeat of the header We then cycle through each element our selector finds (so our function can be applied to more than one table on the same page) For each item, we set up a shortcut to the $(this) element and grab the number of rows in the table It’s important to store Licensed to JamesCarlson@aol.com... element Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Figure 8.6 Repeating the table header 317 318 jQuery: Novice to Ninja But the :nth-child selector also accepts other values, which cause it to select multiple rows as the same time If you pass in the text values even or odd, you’ll select all of the even or odd child elements Coolest of all, you can pass it an equation to figure out... table rows into your browser it might become a little sluggish But for smaller sets, it can make sense to load everything onto the page at once; all data is stored locally, and there are no refreshes every time the user wants to move through the data Our jQuery pagination widget is shown in Figure 8.7 The table paging widget that we’ll create will have clickable Next and Previous but tons, as well... header as the final row of the table; if the total number of rows divides evenly by our repeater number, we need to remove the last row “These changes to the admin section are just great,” says our client in a here comes a big request kind of way, “but it would be great if we could replace the old desktop application that the marketing manager uses It hooks into the same database, but it lets her sort... equation to figure out which children to select! You can use the letter “n” to indicate repetition; for example, :nth-child(10n) will select every tenth row You can then augment this with a plus or minus to offset which rows are selected For example, if you want to select the third row, and then every tenth row after that, you could use :nth-child(10n+3) Finally, if you like to think backwards, you could achieve... important to store Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com That all works okay, but it does have a bug: if the last row of the table matches our equation, the header row will be repeated as the final row—which looks a bit weird Also, we want to apply the repeating header to a couple of tables, and want to avoid having to copy/paste code Next chapter, we’ll look at making our... id="celebs"> ⋮ Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com Figure 8.7 Paging tables Lists, Trees, and Tables 321 Adding the Controls Dynamically Another option would be to build the navigation controls completely in jQuery This means you could easily apply it to any table, and the controls would be added automatically Such an approach is often favored by... the number of rows is going to change! Next comes the workhorse: we copy out the first row (as we did before) and insert it every “nth” row This needed to be slightly rewritten with a find action, so it could be run on any of a number of tables matched by the selector—but otherwise it’s exactly the same as our first effort The final part of the code does a small bit of math to determine whether we’ve... (direction) { reveal = function (current) { // 5 Reveal the correct rows }; // 4 Move previous and next } }; It looks as if there’s a lot to cover—but be assured, it’s stuff you already know To start off, we grab the table and rows we want to paginate, and do some calculations to figure out how many pages there’ll be: chapter_08/08_pagination/script.js (excerpt) // 1 Set up paging information var $table = . nicely in place, we need to react to the scroll event and position it appropriately: Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 316 jQuery: Novice to Ninja chapter_08/06_fixed_table_headers/script.js. ($table.data('top') < $(document).scrollTop() && ➥$(document).scrollTop() < $table.data('bottom')) { $list .show() .stop() .animate({ top: $(document).scrollTop(),. element. Licensed to JamesCarlson@aol.com Licensed to JamesCarlson@aol.com 318 jQuery: Novice to Ninja But the :nth-child selector also accepts other values, which cause it to select multiple