Rails, jQuery UI (Sortable), and Ordering of Slides
I am partly sharing this issue and solution to the world, but mostly just recording somewhere what I came up with. Now, onto the quick read.
A web design client recently asked me to build a web site for him that would allow him to create slide shows of his art work. One of the criteria was to create a way to set the order and, at a later date, re-arrange slides in the slide shows. Turning to jQuery UI, specifically the Sortable (doc), and a simple rails controller this task was pretty trivial. My initial concern was that a slide show could consist of hundreds of slides and doing any sort of ajax updating of the slides would be too slow. Turns out, this was a real concern and after reading all kinds of blogs I was unable to find a work-able ajaxively awesome solution. So, I turned back to the non web2.0 design of just having a Save Order button. The methodology of my solution is straight forward: allow the user to drag and drop to any order configuration they please and then save that order. Upon clicking save via the user interface, invoke the following javascript command that will build the query string and do a window relocation. I'd rather use GET than POST for no real reason outside of having to hack around the authenticity token. After all, what's the point of having an auth token if you're just going to override it in a members-only section. Here is the complete code for the view, broken into pieces for clarity.
CSS:
#sortable { list-style-type: none; margin: 0; padding: 0; width: 100%; } #sortable li { margin: 0 3px 3px 3px; padding: 0.4em; padding-left: 1.5em; font-size: 1.4em; height: 18px; } #sortable li span { position: absolute; margin-left: -1.3em; }
JS:
<script type="text/javascript"> // Initialize sortiable on #sortable div $(function() { $("#sortable").sortable(); $("#sortable").disableSelection(); }); // Prepare and go-to proper url for updating order of slides. // Called via html anchor tag by user function update_order() { window.location.href = "/slides/order?"+$('#sortable').sortable('serialize'); }
Sample #sortable:
<ul id="sortable"> <li id="slide_2" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>Lack of a better Title</li> <li id="slide_3" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>A fireplace for two</li> <li id="slide_1" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>My Super Sweet Villa</li> <li id="slide_4" class="ui-state-default full-width"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>A view from above</li> </ul>
Anchor link:
<a href="#" onClick='update_order(); return false()'>Save Order</a>
Controller:
def order if params[:slide] params[:slide].each_with_index { |slide, index| Slide.update(slide.to_i, :slide_position => (index +1)) } flash[:notice] = "Slide order has been updated." redirect_to(slides_path) else @slides = Slide.ordered end end
Now, for some disclaimers... You should add in error handling to all of these pieces! For brevity, I've included the main pieces of the task and not my error handling code. You can also use post and add in (instead of looking for params[:slide]) if request.post?... Just don't forget to put in the authenticity_token to your ajax or form post (forms do this automatically in rails if protect_from_forgery is enabled).
Efficiency: This is a O(n) algorithm, because it has to iterate over each element being sorted and do three things: fetch from the database, update the attribute, and save. The last two steps can be combined. I'm using the handy ActiveRecord::Base extension in Rails called update to do this. Here's that code so you don't have to look it up:
# File vendor/rails/activerecord/lib/active_record/base.rb, line 744 744: def update(id, attributes) 745: if id.is_a?(Array) 746: idx = -1 747: id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) } 748: else 749: object = find(id) 750: object.update_attributes(attributes) 751: object 752: end 753: end
I was unable to locate any kind of conditional MySQL update that would allow different values to be updated for different rows all in one swoop. I'll admit my research on this topic was about 30 minutes of blog and StackOverflow reading... If you have something more efficient please reply.
Here are the link(s) to relevant information:
jQuery UI Demos (and download) -- I'm using the latest code base as of this post. The styles you see within the bullet list items are from the jQuery UI theme.
jQuery & Navigation: sliding sub navs
We've all seen those web sites that have navigation links that, when clicked, slide down a sub navigation. I recently put one together for thatwasmean.com and thought I would take a moment to clean up, generalize, and make easily viewable an example. You can find the working example in my examples directory, here.
To make this functionality possible I utilized jQuery's slideUp and slideDown functionality. This is a neat little function that changes the display property from displaying the full element to slowly hiding the top part of it until the whole element is hidden, at which point the display CSS attribute is set to none.
I am going to put the main parts of the example here in this point, but keep in mind you will have to set up the remaining html tags (like body and head) as well as include the jQuery framework. It is encouraged to view the working example to see some of the added styling to make things look a little prettier (since in this post scope I am only including the necessities)
Let's begin with the CSS styles to set things up. We have three sub navigation items and so we need to set those three sub nav items with a display property set to none. You will also note I have modified text alignment in here, since each sub nav requires different aligning. The 'flash_about' is the container for the navigation as well as the sub navigation items. This is easy to change if you wish, but I stuck it all in one place as it suited my needs.
#flash_about { text-align:right; } .sub_nav_box { font-weight: normal; padding:0.3em 1em 1em; border-top:1px dotted #C1CDCD; } #add_new_thing { display: none; text-align: left; } #about { display: none; text-align: left; } #browse_by { display: none; text-align: center; } #browse_by > .link { margin-left: 0.2em; } .nav_link { margin-left: 1em; }
Next we need our JavaScript... using the jQuery functions mentioned above and a little jQuery helper magic we have a toggle_div function that takes a div ID and will toggle that div. This function is cool in the sense that it will hide all other opened sub nav items and only show the one you are selecting. Further, if you click a navigation link and its corresponding sub navigation item is already being displayed then the function will slide up that sub navigation item. (The $ symbol represents a call to jQuery.)
function toggle_div(div_id) { var div = $('#'+div_id); var divs = $('#flash_about').children('div').not(div); if(div.css('display') == 'none') { divs.slideUp(function() { div.slideDown(); }); } else { div.slideUp(); } }
Lastly, let's get the HTML in here to make all of this mean something to the viewer. I have left some content in the sub navigation items to give the overall example some meaning. (I strongly dislike examples where everything is named 'example 1, example 2, examples 3-- show me some content good sir!)
<div id="flash_about"> <a class="nav_link" onclick="return toggle_div('about');" href="#">About</a> <a class="nav_link" onclick="return toggle_div('browse_by');" href="#">Categories</a> <a class="nav_link" onclick="return toggle_div('add_new_thing');" href="#">Submit something</a> <div id="add_new_thing" class="sub_nav_box"> <div style="margin-bottom: 0.4em; margin-top: 0.4em;"> <span id="add_new_thing_title" style="color: #333; font-size: 16px;">Type your thing here... </span></div> <textarea id="add_new_thing_body" class="thing_input" style="border: 2px solid #666;" cols="40" rows="10"></textarea> <div id="new_thing_buttons"> <a class="button" href="#">add new thing</a> or <a onclick="toggle_div('add_new_thing');" href="#">cancel</a></div> </div> <div id="about" class="sub_nav_box"> Welcome to my web site. This is the about us... it is awesome. Feel free to browse the site and check out some of our cool features.</div> <div id="browse_by" class="sub_nav_box"> <a class="link" href="#">link</a> <a class="link" href="#">link</a> <a class="link" href="#">link</a> <a class="link" href="#">link</a> <a class="link" href="#">link</a> <a class="link" href="#">link</a> <a class="link" href="#">link</a></div> </div>
And there we have it. Go check out the example to see it in action. I hope you enjoyed this post. If you have any requests on JavaScript/jQuery examples let me know... As I find more time I will put up more examples of common uses of JS & jQuery.
Show/Hide ToolTips with jQuery (revisited)
Finally, I got around to putting together a very simple demonstration (no graphics or styling) of using jQuery to show and hide (toggle) tool tips. I have put together I demo you can access at examples/jquery/show_hide_tooltips_ex1. Ctrl+U to view source.
What I have put together in this demonstration:
- an on ready document function that tells jQuery when the web page has rendered to become active
- a binded click event on all input boxes with a class named 'form_field' that will call back a function when clicked
- a call back function that will hide all other tooltips and display the one corresponding to the input box clicked
Here is the source code to view. Again, to see it functioning go to the above link! And don't forget for all of this to function you must include the jQuery JavaScript framework into your <head></head> html page. You can link or grab a copy from Google Code (here), and the version I am using is jquery-1.3.2.min.js.
$('.form_field').bind("click", function() { var divs = $('.tooltip_example1'); var field = this.id; var div = $('.tooltip_example1[id='+field+']'); if(div.css('display') == 'none') { divs.hide(function() { div.show(); }); } else { div.hide(); } }); });
And within the body of your html page you should add the following html
<table border="0"> <tbody> <tr> <td> Username</td> <td> <input id="username" class="form_field" size="30" type="text" /></td> <td> <div id="username" class="tooltip_example1" style="display:none"> This is how we'll identify you</div></td> </tr> <tr> <td>Password</td> <td> <input id="password" class="form_field" size="30" type="text" /></td> <td> <div id="password" class="tooltip_example1" style="display:none"> And how you keep it safe</div></td> </tr> <tr> <td colspan="3"> <input type="button" value="Submit" /></td> </tr> </tbody></table>
The value in this kind of toggling is as a user goes through a form they can see suggested messages by you, the web developer.
Show/Hide ToolTips with jQuery
An Updated Post Can Be Found Here
As a developer I have faced many problems with tooltips and getting div's to appear, disappear, and jump through hoops. Today I figured out a nice way.
This code below dynamically shows and hides certain div's. Using no onClick event handling. There are 3 pre-reqs,
- you have an outside ID that contains all of your tooltip ID's.
- you assign your tool tip ID's to be uniqueNameToolTip. This will allow the below code to successfully identify and modify each ID independently.
- in your CSS for the aToolTip class you should set "visibility: hidden". This will hide the tool tip by default.
Here is the code below. I have made bold the appended "ToolTip" for each unique ID and the "#Change_Me" which needs to be changed to reflect your code.
<script type="text/javascript"><!--mce:0--></script>
This code should be inserted in to your <head>.
In your body, you will have code that looks like:
<div id="CHANGE_ME"> <table border="0" cellspacing="4" cellpadding="8"> <tbody> <tr> <td style="text-align:right">name</td> <td></td> <td id="uniqueNameToolTip"> <img src="/images/ToolTipName.jpg" alt="" /></td> </tr> </tbody></table> </div>
That's it. Now, when the page loads you will find that when you focus on the text box the tool tip in the right column of the table will appear. I know the formatting is a bit off, bear with me! Question/Comments please post. Note this was developed on an asp.net page, but the concept and functionality (of the javascript) persists on asp, asp.net, php, so forth.