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!
"#rubyrails-chp-6-fig-4">Figure 6-4 gives you a preview of how
this is going to look when we're done.
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 s:
<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
s side-by-side as you saw them in
"#rubyrails-chp-6-fig-4">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 ids 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 is empty (see
"#rubyrails-chp-6-fig-5">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.
 |