Section 6.4. Drag and Drop Everything (Almost Everything)


6.4. Drag and Drop Everything (Almost Everything)

We have already displayed a list of thumbnails of all photos that are in the slideshow and enabled the user to drag them around to rearrange their order in the slideshow. Now let's add a second list of thumbnails, showing all photos that are not being used in the slideshow.

We'll let the user add a photo to the slideshow by dragging it from the list of unused photos and dropping it onto the slideshow thumbnails. Similarly, we can enable the user to remove photos from the slideshow by dragging its thumbnail from the slideshow and dropping on the unused photos list. Finally, we'll allow the user to filter the unused photos list by category.

As you might expect, we can accomplish all that in a very small amount of code. We will add a mere 58 lines of Ruby code to the models and controllers, 47 lines to the view templates, and 16 lines to our CSS stylesheet! Figure 6-4 gives you a preview of how this is going to look when we're done.

Figure 6-4. Preview of drag-and-drop slideshow editing

Let's start by updating the slideshow's edit template. Edit photos/app/views/slideshows/edit. rhtml to look like this:

 <h1>Editing slideshow</h1> <div id='slideshow-contents'>   <p style='text-align: center;'><b>Slideshow Photos</b></p>   <div id='slideshow-thumbs'>     <%= render :partial => 'show_slides_draggable' %>   </div> </div> <div id='slideshow-photo-picker'>   <p style='text-align: center;'><b>Unused Photos</b></p>   <div id='slideshow-photos'>     <%= render :partial => 'photo_picker' %>   </div> </div> <div id='slideshow-attributes'>   <p><%= link_to 'Play this Slideshow', :action => 'show', :id => @slideshow %></p>   <div style='border: thin solid; padding-left: 1em;'>     <p style='text-align: center;'><b>Attributes</b></p>     <%= start_form_tag :action => 'update', :id => @slideshow %>       <%= render :partial => 'form' %>       <%= submit_tag 'Save Attributes' %>     <%= end_form_tag %>   </div>   <p>     <b>Hint:</b> Drag and drop photos between the     two lists to add and remove photos from the     slideshow. Drag photos within the slideshow to     rearrange their order.   </p> </div> <%= drop_receiving_element("slideshow-contents",           :update => "slideshow-thumbs",           :url => {:action => "add_photo" },           :accept => "photos",           :droponempty => "true",           :loading => visual_effect(:fade),           :complete => visual_effect(:highlight, 'sortable_thumbs')           ) %> 

This file has been almost entirely rewritten, so there are no marked -as-changed lines. You can see that I have laid out this edit page into three sections:

 <div id='slideshow-contents'> ... </div> <div id='slideshow-photo-picker  '> ... </div> <div id='slideshow-attributes'> ... </div> 

Only the slideshow-photo-picker is new. It shows the list of unused photos that can be added to the slideshow. We will set up the CSS stylesheet to display these sections side-by-side as you saw them in Figure 6-4.

slideshow-contents is rendered by the partial template show_slides_draggable , slideshow-photo-picker is rendered by the partial template photo_picker , and slideshow-attributes is mostly rendered by the form partial template that was generated from the scaffolding. I say "mostly" because I added a few things inline around the rendering of form .

Finally, notice two Ajax related helpers: drop_receiving_element and observe_field . We'll come back to these in a little bit after we have discussed some prerequisite details.

Now, make these changes to photos/app/controllers/slideshows_controller.rb , replacing the edit method and creating the unused_photos method:

 def edit     @slideshow = Slideshow.find(params[:id])     session[:slideshow] = @slideshow     @photos = unused_photos(@slideshow)   end   def unused_photos(slideshow)     all_photos = Photo.find(:all)     candidates = []     for photo in all_photos        in_slideshow = false        for slide in slideshow.slides             if slide.photo.thumbnail === photo.thumbnail                in_slideshow = true                break             end        end        candidates << photo if not in_slideshow     end     return candidates   end 

The purpose of this code is to retrieve all the data needed by the edit.rhtml view template:



@slideshow = Slideshow.find(params[:id])

The id of the slideshow that you want to edit is passed in the request parameters from the browser. Here you retrieve that id and read that slideshow from the database, which you store in the instance variable @slideshow to make it available to the view template.



session[:slideshow] = @slideshow

Ajax actions requests will be coming in as the user makes changes, and you need to know what slideshow to change. This line saves a reference to the slideshow in the session hash. I'm using a key value of :slideshow to save and retrieve this from the session, but that value is arbitrary and could have been any unique identifier.



@photos = unused_photos(@slideshow)

This line calls the new method unused_photos to retrieve a list of all photos that are not in the slideshow; it then saves that list in @photos .



def unused_photos(slideshow)

This method returns a list of photos that are not in the slideshow. The logic should be self-explanatory. First, create an empty array ( candidates = [] ), and then iterate through the list of all photos, adding them to the array ( candidates << photo ) if they are not already in the slideshow. The technique used here is grossly inefficient, but it will suffice for our purposes.

We still need to create the photo_picker template that generates the HTML to display all the photos that can still be added to a slideshow, so go ahead and create the file photos/app/views/slideshows/_photo_picker.rhtml with this in it:

 <% for photo in @photos %>     <%= image_tag("photos/#{photo.thumbnail}",                    :style => "vertical-align: middle",                    :id => "photo_#{photo.id}",                    :class => "photos") %>     <%= draggable_element "photo_#{photo.id}", :revert => true %> <% end %> 

This template iterates through the list of photos in @photos . For each photo, it uses the image_tag helper to create an HTML image tag and the draggable_element helper to generate the JavaScript code that makes it draggable. You can see that the first parameter of draggable_element matches the value of the id attribute ( :id => "photo_#{photo.id}" ) on the image tag. The draggable_element helper expects the id of the HTML element that it should make draggable, followed by zero or more options. The single option used here ( :revert => true ) says to move the element back to its original position after it is dropped.

But where can these draggable images be dropped? Recall that at the end of the slideshow's edit.rthtml template we had:

 <%= drop_receiving_element("slideshow-contents",           :update => "slideshow-thumbs",           :url => {:action => "add_photo" },           :accept => "photos",           :droponempty => "true",           :loading => visual_effect(:fade),           :complete => visual_effect(:highlight, 'sortable_thumbs')           ) %> 

Just like the draggable_element helper, the drop_receiving_element helper expects the ID of the HTML element onto which you can drop something that was declared as draggable. The remaining parameters are options that given as name /value pairs (the order is not important). These options are doing a lot, so let's go through them one at a time:



:update => "slideshow-thumbs"

This gives the ID of the HTML element that should be updated when a photo is dropped on our slideshow-contents div . The :position and :url options say how, and with what, that HTML element should be updated. When the :position option is omitted (as it is here), the HTML returned from the server replaces the target element's HTML. The :position option says that the returned HTML should be inserted into target element, instead of replacing it. The value :position can be specified as :before , :top , :bottom , and :after .



:url => {:action => "add_photo" }

This option constructs the URL that is sent to the server (via a background Ajax request) when a photo is dropped (you've seen this before). This executes the add_photo method in the current controller (the SlideshowsController ). The add_photo action adds the dropped photo to the slideshow and returns an HTML fragment that will replace the existing HTML in the target element, which, as you will see, is a rerendering of the slideshow's contents, which now include the added photo.



:accept => "photos"

Without this option, you could drop any draggable element here. However, this line says that only HTML elements that have the class attribute "photos" can be dropped here. Remember that in our photo picker template we gave each photo class attribute of "photos" .



:droponempty => "true"

This option says that the user can drop photos here even if the target is completely empty.



:loading => visual_effect(:fade) :complete => visual_effect(:highlight, 'sortable_thumbs')

:loading and :complete (plus a few more events) specify client-side JavaScript event handlers that are executed at specific points in the progress of the Ajax request. In both cases, we are displaying a visual effect that gives the user positive feedback. The :loading event occurs when the browser begins loading the response, and the :complete event occurs when its all finished. The code specifies that the dropped photo will fade until it becomes invisible. It also highlights the target area on which the photo was dropped.

Now we need to create the add_photo method to actually add a dropped photo to the slideshow. Edit photos/app/controllers/slideshows_controller.rb , and add this:

 def add_photo     slideshow_id = session[:slideshow].id     photo_id = params[:id].split("_")[1]     slide = Slide.new(  )     slide.photo_id = photo_id     slide.slideshow_id = slideshow_id     if !slide.save       flash[:notice] = 'Error: unable to add photo.'     end     @slideshow = Slideshow.find(slideshow_id)     session[:slideshow] = @slideshow     render_partial 'show_slides_draggable'   end 

Let's walk through this code:



slideshow_id = session[:slideshow].id

This line retrieves the current slideshow from the session hash and gets the slideshow's id .



photo_id = params[:id].split("_")[1]

The id attribute of the dropped photo get passed as the :id parameter. If you recall from the photo_picker template, we set those id s to values such as "photo_1" and "photo_19", so the remainder of this line of code splits the string on the underscore , grabs the second half, and assigns it to photo_id .

The next five lines create a new slide, assign to it the photo id and the slideshow id , and then save it to the database.

Finally, we render and return the show_slides_draggable partial, after setting @slideshow to the current slideshow (which is needed by the partial template).

All that code handles dragging new photos to add to the slideshow. Now we just need to add a little more code to implement dragging a photo from the slideshow to the unused photos list as an intuitive way to remove photos from the slideshow.

The displayed list of photos in the slideshow are already draggable because we made them into a sortable list. The only problem with the current implementation is that the photos can be dragged vertically only. They need to be dragged both vertically for reordering and horizontally to the unused photos column.

We can drag the photos only vertically because the default option for a sortable list is :constraint => 'vertical' . Fortunately, you can change this by editing the file photos/app/views/slideshows/_show_slides_draggable.rhtml and changing the call to the sortable_element helper to add this :constraint option:

 <%= sortable_element('sortable_thumbs',                      :url => {:action => 'update_slide_order'},                      :constraint => '') %> 

Now you can drag those photos anywhere . But you still need to make the unused photos list into a drop receiver that uses Ajax to remove the dropped photo from the slideshow.

To do so, edit photos/app/views/slideshows/edit.rhtml, and add this at the end:

 <%= drop_receiving_element("slideshow-photo-picker",           :update => "slideshow-photos",           :url => {:action => "remove_slide" },           :accept => "slides",           :droponempty => "true",           :loading => visual_effect(:fade),           :complete => visual_effect(:highlight, 'slideshow-photos')           ) %> 

This code is almost identical to the other drop_receiving_element we used. The difference is that the target is the slideshow-photo-picker , and the action taken on a drop is to call the remove_slide method. Also, notice that you can drop only "slides" here (that is, HTML elements with a class attribute of slides ). If you go back and take a look at how we defined the partial template photos/app/views/slideshows/_show_slides_draggable.rhtml , you will see that we did, indeed, make each item in the sortable list a slide.

Add the remove_slide method to photos/app/controllers/slideshows_controller.rb :

 def remove_slide   slideshow_id = session[:slideshow].id   slide_id = params[:id].split("_")[1]   Slide.delete(slide_id)   @slideshow = Slideshow.find(slideshow_id)   session[:slideshow] = @slideshow   @photos = unused_photos(@slideshow)   render_partial 'photo_picker' end 

In this code, you get the id of slide you want to remove, and then delete it from the slide database table. Remember, this action does not delete the photo from the database. The slide data says what photos are in a given slideshow, and deleting an entry from the slide table removes that slide from its slideshow. Finally, you render the HTML for the photo picker, which now includes the removed slide.

I'll bet you're anxious to see all this in action. All you need to do is to update the style sheet and then try it out. Edit photos/public/stylesheets/slideshows.css , and add the following:

 #slideshow-photo-picker {    float: left;    width: 10em;    text-align: center;    border-right: thin solid #bbb;    padding: 0.50em;    padding-bottom: 10em; } img.thumbnail {    border: 2px solid black;    margin-bottom: 1em; } img.photos {    border: 2px solid black;    margin-bottom: 1em; } 

Whew! That's it: try it now!

The first thing you'll notice is that the Unused Photos section is empty (see Figure 6-5). That's because all the photos are currently in the slideshow. Just drag a few of the slides out of the slideshow and drop them into the Unused Photos column; then you'll have something more like Figure 6-6.

Figure 6-5. Drag and drop add and remove

Figure 6-6. Some unused photos



Ruby on Rails[c] Up and Running
Ruby on Rails[c] Up and Running
ISBN: B003D3OGCY
EAN: N/A
Year: 2006
Pages: 94

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net