Slide Sorter

« Return to page index

Create a view for sorting images (and potentially other content) which are displayed in a grid layout, where by each item occupies a single cell rather than a whole row. This uses a modified version of the javascript that powers the drag and drop functionality of folder_contents.

Introduction

Brief description of the motivation and implementation.

The site this is used for has folderish content types that can contain multiple images, for which the users requested a simple method for sorting. 

First Attempt

Initially, I simply customized folder_contents to additionally display an icon of the image, and make use of the existing Ajax-based drag and drop reordering.  This fell short in that the icons were too small to be able to accurately distinguish one from the other.

Custom Folder Contents View

Second Attempt

Next, I further customized folder_contents using the thumbnail view and also providing links to larger views that would be displayed in a dialog box. This solution has the drawback that the drag and drop functionality is only usable when all the images fit in the window.  When a folder contains more than 5-6 images, the user would have to drag and drop an image as far as is viewable, then stop, scroll the window further and repeat the drag and drop process.  When moving an image from the bottom to the top of a list of 30, this quickly became unusable. 

Aside: Although this template is not used primarily for sorting, it is used for edting (renaming, rotating) multiple images at once.

Further customized folder_contents

Final Solution

The ultimate solution would resemble iPhoto's sorting capabilities, in that all the images would be displayed in a table-like view, such that each data cell would contain an image, which could be re-ordered by dragging to any position in the grid.  This requires a new page template that positions the images with css rather than a table, and a modified version of the dragdropreorder.js used by folder_contents.  I also added the ability to double-click on an image to view the full-size version in a dialog box.

Slide Sorter screenshot

Get this working on my site

Quick instructions of how to get this slide sorter functionality working on my site

  1. Add each of the attached source files to the custom folder or a products skin folder
  2. Add an action to the folder content type, with:
    1.  Title = Slide Sorter
    2. id = slide_sorter
    3. URL = string:$object_url/slide_sorter
    4. category = folder
  3. Navigate to your folder containing many images
  4. Click on the Slide Sorter tab
  5. Drag and drop images to sort
  6. Double click an image to see bigger view

Create slide sorter view

Describes the process and choices made when creating the slide sorter view

Table vs Divs

In the folder_contents table, each row is draggable, and so each element had a common parent of the <table> or <tbody> tag.  However, if we use a table with each image in a <td>, all the draggable element would not have the same parent (unless the table consisted of one row);
Therefore, I decided to use CSS to make the images display in a grid rather than an html table element, since this would mean all draggable elements could have the same parent node in the element tree.

Keeping all draggable elements inside the same parent element means that we have less to customize of the javascript used by folder_contents. 

Also, the use of the CSS float property to position these elements means that the number of columns cam be gracefully determined by the size of the window.

Key components

There a few parts to the template which are necessary for the javascript to work.

  1. The parent element (the one containing all of the sortable items), must have the id= 'slide-sortable'
  2. Each of the sortable elements must have the class= 'sortable-cell'
  3. Each sortable element must also have a unique ID (ex. id = 'folder-content-item-my_img')

Use with custom image types

To use this with other content types, simply change the filter used in the call to getFolderContents, from {'portal_type':'Image'} to something more suitable.

Unrequired Extra

On each sortable element, I also include the attribute ondblclick, which calls a javascript function to open a dialog box of a larger view of the image.  This particular javascript function (written by John Gardner), resizes the pop-up window to the size of the image.  For this functionality, one must include this 'ondblclk' attribute, aswell as the script element that calls openpopup.js.  Of course none of this is required for the primary sorting functionality.

Note:  Instead of explicitly including the javascript files using the 'script' elements in this template, they can also be included using the portal_javascripts tool.

This is the source code for the slide sorter template:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
      lang="en-US"
      metal:use-macro="here/main_template/macros/master"
      i18n:domain="plone">
<body>
<div metal:fill-slot="main">
    <tal:protect tal:condition="python: not checkPermission('List folder contents', here)" 
                 tal:replace="here/raiseUnauthorized" />
    <metal:main_macro define-macro="main">
    <metal:pic_sorter_macro define-macro="pic_sorter"
     tal:define="imgs python:context.getFolderContents({'portal_type':'Image'});">
    <h1 class="documentFirstHeading">Slide Sorter</h1>
    <script type="text/javascript" src="openpopup.js"></script>
    <script type="text/javascript" src="dragdropslidesorter.js"></script>
    <tal:albumsimages tal:condition="imgs">
          <div id="slide-sortable"
                 class="listing"
                 width="100%">
                <tal:items tal:repeat="item imgs">
                    <tal:itemdefs tal:define="index repeat/item/index;
                                              numCols python:3;
                                              sameRow python:index % numCols;">
                        <span tal:define="oddrow             repeat/item/odd;
                                        item_url             item/getURL|item/absolute_url;
                                        item_id              item/getId;
                                        item_title_or_id     item/pretty_title_or_id;
                                        item_description     item/Description;
                                        oddEven              python:test(oddrow, 'even', 'odd');
                                        large_view           string:$item_url/image_large;"
                             tal:attributes="class string:$oddEven sortable-cell;
                                             id string:folder-contents-item-${item_id};
                                             ondblclick string:return openPopup('$large_view', 
                                                                                '$item_title_or_id');" >
                                   <img src="" alt='' tal:attributes="src string:${item_url}/image_thumb;
                                                                      alt item_title_or_id;
                                                                      title item_description" />
                                   <span tal:content="item_title_or_id" />
                        </span>
                     </tal:itemdefs>
                </tal:items>
                <div class="visualClear"><!-- --></div>
            </div>
    </tal:albumsimages>

    <p class="discreet"
       i18n:domain="atcontenttypes"
       i18n:translate="text_no_albums_uploaded"
       tal:condition="python:not imgs">
        No images uploaded yet.
    </p>

    </metal:pic_sorter_macro>
</metal:main_macro>
</div>
</body>

 

Create javascript for drag and drop sorting

Modify the javascript used by folder_contents to allow sorting of individual cells in our grid

The main difference between this javascript file and the one used for folder_contents, is that I replaced  the swapElement function with an insertElement function. 
When sorting the rows in folder_contents, as you drag an item up or down, it swaps location with the next item it comes to;  However, with a two dimensional grid layout, this works fine when you drag an item along the same row, but if you drag it up to the next row, it swaps postion with that item. This is not the desired affect, as items jump around the screen.

This "bug" can also been demonstrated in folder_contents by selecting an item to drag, dragging it outside of the table, and then back into the table at a different point.  The item where you re-enter the table then gets swapped with the position of the dragged-item.  This is only a visual affect, but is a little confusing.

Instead of swapping elements, we simply want to insert the element we are dragging, and bump the other items along.

Below is a snippet of the dragdropslidesorter.js, showing the new insertElement function:

<snip>
dndSlideSorter.insertElement = function(child1, child2) {
    // child1 = target (current location of cursor)
    // child2 = item being dragged
    var parent = child1.parentNode;
    var children = parent.childNodes;
    var items = new Array();
    // get index of target and dragged item
    var target_pos, drag_pos = 0;
    for (var i = 0; i < children.length; i++) {
        var node = children[i];
        items[i] = node
        if (node.id) {
            removeClassName(node, "even");
            removeClassName(node, "odd");
            if (node.id == child1.id)
                target_pos = i;
            if (node.id == child2.id)
                drag_pos = i;
        }
    }
    // move dragged item to index of target (don't swap, just insert)
    // swapping meant that with new layout, index 1 could get swapped with
    // 7 and images visually jump around rather than insert
    if (drag_pos > target_pos)
        {
        // insert dragged
        items.splice(target_pos,0,items[drag_pos]);
        // delete original
        items.splice(drag_pos+1,1);
        }
    else{
        // insert dragged
        items.splice(target_pos+1,0,items[drag_pos]);
        // delete original
        items.splice(drag_pos,1);
        }

    Sarissa.clearChildNodes(parent);
    var pos = 0;
    for (var i = 0; i < items.length; i++) {
        var node = parent.appendChild(items[i]);
        if (node.id) {
            if (pos % 2)
                addClassName(node, "even");
            else
                addClassName(node, "odd");
            pos++;
        }
    }
}
<snip>

The other major change between the original dragdropreorder.js and this one,  is to fetch our draggable items identified by the id #slide-sortable and class .sortable-cell rather than by the table elements.

Add styles

In order for the images to appear in a grid, we must add some Cascading Style Sheet (CSS) declarations

To make the images appear as if they are positioned with a table, we use the CSS float property.

Below is the complete style sheet used:

/* ######################## */
/* ## Slide Sorter styles ## */
/* ######################## */
#slide-sortable {
    border: 1px solid #ccc;
}
span.sortable-cell {
    float: left;
    height: 185px;
    width: 143px;
    margin: 0em;
    padding: 0px 6px 0px 9px;
    text-align: center;
    background-image: url('&dtml-portal_url;/polaroid-single.png');
    background-repeat: no-repeat;
}
span.sortable-cell img {
    border: 1px solid #ccc;
    display: block;
    margin:  auto;
    margin-top: 30px;
}
#slide-sortable span.dragging  {
    background-color: yellow;
    background-image: url('');
}
.dragging img {
    border:1px solid black;
}
/* ######################## */
/* ## Slide Sorter styles ## */
/* ######################## */

Conclusion

Lessons learnt

We all know that Plone has lots of awesome functionality built in. However, if your very specific use case is not covered, it is not hard to customize something similar to get exactly what you need.

This tutorial describes the process taken to build a drag and drop slide sorter, starting with the folder_content's drag and drop functionality as a base. 

I started by fiddling with the existing page template. When it became apparent that this was not enough, I looked to make a few small changes to the javascript. The majority (if not all) of the core Plone code is very well written and easy to understand. By digging around a little, it is easy to find exactly what you are after.