Selectable - jQuery UI - Web Development with jQuery (2015)

Web Development with jQuery (2015)

Part II. jQuery UI

Chapter 14. Selectable

This chapter presents the jQuery UI Selectable plugin. The Selectable plugin fills a niche in UI functionality, and that niche is the occasion in which you need to make a selection by drawing a box. And this is a niche because you probably won't use this functionality very much in your applications. Making a selection by drawing a box is something you've probably done a few times in your operating system's file manager or a graphical editor like Photoshop.

Nonetheless, the Selectable plugin can be useful when the need arises, and in this chapter you see at least one practical application of this plugin: a continuation of the Mac OS X Finder clone that you started in Chapter 10, “Scrollbars.”

Introducing the Selectable Plugin

The Selectable plugin works similarly to the Sortable plugin presented in Chapter 13, “Sortable,” and all jQuery UI plugins, as you'll have recognized by now, share a clean and consistent API that is implemented similarly from plugin to plugin.

To make elements into Selectable elements, you call the selectable() method on any element. The following document, which appears as Example 14-1 in the source materials at www.wrox.com/go/webdevwithjquery, demonstrates the plugin:

<!DOCTYPE HTML>

<html xmlns='http://www.w3.org/1999/xhtml'>

<head>

<meta http-equiv='content-type'

content='application/xhtml+xml; charset=utf-8' />

<meta http-equiv='content-language' content='en-us' />

<title>Finder</title>

<script type='text/javascript' src='../jQuery.js'></script>

<script type='text/javascript' src='../jQueryUI.js'></script>

<script type='text/javascript' src='Example 14-1.js'></script>

<link type='text/css' href='Example 14-1.css' rel='stylesheet' />

</head>

<body>

<div id='finderFiles'>

<div class='finderDirectory' data-path='/Applications'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>Applications</span>

</div>

</div>

<div class='finderDirectory' data-path='/Library'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>Library</span>

</div>

</div>

<div class='finderDirectory' data-path='/Network'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>Network</span>

</div>

</div>

<div class='finderDirectory' data-path='/Sites'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>Sites</span>

</div>

</div>

<div class='finderDirectory' data-path='/System'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>System</span>

</div>

</div>

<div class='finderDirectory' data-path='/Users'>

<div class='finderIcon'><div></div></div>

<div class='finderDirectoryName'>

<span>Users</span>

</div>

</div>

</div>

</body>

</html>

The following CSS provides some styling for the finder example and jQuery UI Selectable example.

html,

body {

width: 100%;

height: 100%;

overflow: hidden;

}

body {

font: 12px "Lucida Grande", Arial, sans-serif;

background: rgb(189, 189, 189) url('images/Bottom.png') repeat-x bottom;

color: rgb(50, 50, 50);

margin: 0;

padding: 0;

}

div#finderFiles {

border-bottom: 1px solid rgb(64, 64, 64);

background: #fff;

position: absolute;

top: 0;

right: 0;

bottom: 23px;

left: 0;

overflow: auto;

}

div.finderDirectory {

float: left;

width: 150px;

height: 100px;

overflow: hidden;

}

div.finderIcon {

height: 56px;

width: 54px;

margin: 10px auto 3px auto;

}

div.finderIcon div {

background: url('images/Folder 48x48.png') no-repeat center;

width: 48px;

height: 48px;

margin: auto;

}

div.finderSelected div.finderIcon,

div.finderDirectoryDrop div.finderIcon {

background-color: rgb(196, 196, 196);

border-radius: 5px;

}

div.finderDirectoryDrop div.finderIcon div {

background-image: url('images/Open Folder 48x48.png');

}

div.finderDirectoryName {

text-align: center;

}

div.finderSelected div.finderDirectoryName span,

div.finderDirectoryDrop div.finderDirectoryName span {

background: rgb(56, 117, 215);

border-radius: 8px;

color: white;

padding: 1px 7px;

}

div.ui-selectable-helper {

position: absolute;

background: rgb(128, 128, 128);

border: 1px solid black;

opacity: 0.25;

-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=25)";

filter: alpha(opacity=25);

}

The following JavaScript makes it possible to select multiple files at once by drawing a box.

$.fn.extend({

selectFile : function()

{

this.addClass('finderSelected');

this.each(

function()

{

if ($.inArray($(this), finder.selectedFiles) == -1)

{

finder.selectedFiles.push($(this));

}

}

);

return this;

},

unselectFile : function()

{

this.removeClass('finderSelected');

var files = this;

if (finder.selectedFiles instanceof Array && finder.selectedFiles.length)

{

finder.selectedFiles = $.grep(

finder.selectedFiles,

function(file, index)

{

return $.inArray(file, files) == -1;

}

);

}

return this;

}

});

var finder = {

selectingFiles : false,

selectedFiles : [],

unselectSelected : function()

{

if (this.selectedFiles instanceof Array && this.selectedFiles.length)

{

$(this.selectedFiles).each(

function()

{

$(this).unselectFile();

}

);

}

this.selectedFiles = [];

},

ready : function()

{

$('div.finderDirectory, div.finderFile')

.mousedown(

function()

{

if (!finder.selectingFiles)

{

finder.unselectSelected();

$(this).selectFile();

}

}

)

.draggable({

helper : 'clone',

opacity : 0.5

});

$('div.finderDirectory').droppable({

accept : 'div.finderDirectory, div.finderFile',

hoverClass : 'finderDirectoryDrop',

drop : function(event, ui)

{

var path = ui.draggable.data('path');

ui.draggable.remove();

}

});

$('div#finderFiles').selectable({

appendTo : 'div#finderFiles',

filter : 'div.finderDirectory, div.finderFile',

start : function(event, ui)

{

finder.selectingFiles = true;

finder.unselectSelected();

},

stop : function(event, ui)

{

finder.selectingFiles = false;

},

selecting : function(event, ui)

{

$(ui.selecting).selectFile();

},

unselecting : function(event, ui)

{

$(ui.unselecting).unselectFile();

}

});

}

};

$(document).ready(

function()

{

finder.ready();

}

);

The preceding source code comes together to give you the document that you see in Figure 14.1.

image

Figure 14.1

While this example teaches you how to draw a selection box, you'll note that you cannot drag the selection after it is made, even though you have implemented drag-and-drop on the individual folders. Although this is possible, it is beyond the scope of this example.

In this example, you applied the Selectable plugin to the Mac OS X Finder Clone example that you worked on in Chapter 12, “Draggable and Droppable.” This example incorporates some jQuery functionality that you learned about in previous chapters to demonstrate how you apply jQuery in a realistic example.

You made a few changes to the style sheet that you created in Chapter 12. You added one additional rule that gives you the ability to customize the box that's drawn when a selection is made. jQuery UI's default selection box as depicted in the documentation looks like the one used in older operating systems, like Windows 98, which just provided a dotted box to indicate where the box is being drawn. In an application like Photoshop, where the selection is oftentimes also animated, this type of selection is referred to as marching ants because the selection box is made to resemble ants marching in a line. In this example, you changed the style of that box to look more like the selection box in Mac OS X.

div.ui-selectable-helper {

position: absolute;

background: rgb(128, 128, 128);

border: 1px solid black;

opacity: 0.25;

-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=25)";

filter: alpha(opacity=25);

}

The selection box can be customized with the same selector that you see here. jQuery UI does not provide a mechanism for customizing the style of the selection box via an option within the JavaScript API. This is by design because it is best practice to keep style separated from behavior. To customize the styling, you must use CSS. The class name ui-selectable-helper is the class name that jQuery UI applies to the selection box, which is also a <div> element, internally, so the customization that you see simply exploits that fact. The styling that the Selectable plugin provides is limited to the necessary CSS properties that actually change as you move your mouse cursor. Those properties are top, left, width, and height. The rest you must provide for yourself, and in fact, the selection box will have no styling at all until you apply it, meaning it will be completely invisible until you provide some styling. You must first position the selection box using position: absolute, and then a border or background should be applied so that you can see a selection when you create one. In this example, I've chosen to simply imitate OS X, making the styling choice easy.

The style provides a gray background and a black border; then the whole box is made semi-transparent via the standard opacity property supported by Safari, Chrome, Firefox, and IE9; Microsoft's proprietary filter property is supported by IE6 and IE7; and Microsoft's proprietary -ms-filter property is supported by IE8 in IE8 standards mode. The IE8 syntax for the -ms-filter property is the same as previous versions; it just puts quotes around the property's value and adds the vendor-specific -ms- prefix. IE9 goes on to eliminate the need for the filter or -ms-filter property because IE9 has native support for the opacity property.

Aside from those modifications, the style sheet remains mostly the same as the style sheet you made in Chapter 12. The brunt of the elbow work in this example occurs in the JavaScript.

In this example, you rewrite the example that you saw in Chapter 12 with a few jQuery plugins, and you add some functionality that deals with keeping track of selected folders. You begin by creating two plugin methods, selectFile() and unselectFile(). As you learned in Chapter 9, “Plugins,” you use $.fn to create jQuery plugins. There is also more than one way to leverage $.fn to create jQuery plugins. The method I use most often is jQuery's extend() method, which allows you to take an object and add to it. For this example, you add two new methods, which each become a new jQuery plugin.

$.fn.extend({

Within the selectFile() method, you begin by adding the class name finderSelected to each element that selectFile() is called on, which can be just one file or many files. The class name provides you with a visual cue to let you see that a file is selected by triggering the following CSS:

div.finderSelected div.finderIcon,

div.finderDirectoryDrop div.finderIcon {

background-color: rgb(196, 196, 196);

border-radius: 5px;

}

In addition to styling the folder icon, the following style is applied to the name of the file or folder:

div.finderSelected div.finderDirectoryName span,

div.finderDirectoryDrop div.finderDirectoryName span {

background: rgb(56, 117, 215);

border-radius: 8px;

color: white;

padding: 1px 7px;

}

Then, for each file object that selectFile() is called on, you see if that file object is already added to the finder.selectedFiles array. This array keeps track of every file that is selected at a given time by storing a reference to it. jQuery's inArray() method is designed to work like JavaScript's indexOf() method. The indexOf() is used to determine if a string contains another string. If the string is found, then indexOf() returns the offset position of the first occurrence of that string, where counting from zero, the first character in the string you're searching is number zero. If indexOf() returns an integer zero or greater, then the string is found within the second string and that number can be used to identify where in that string the second string exists. If indexOf() returns -1, then the string is not found. jQuery's inArray() works the same way it applies the same logic using arrays instead of strings. If a value is found within the array, the offset position of that value is returned. The array is also numbered starting from zero, so the first item within the array is number zero and each item is numbered from there. inArray() returns -1 if the value does not exist within the array, otherwise inArray()returns a number zero or greater.

selectFile : function()

{

this.addClass('finderSelected');

this.each(

function()

{

if ($.inArray($(this), finder.selectedFiles) == -1)

{

finder.selectedFiles.push($(this));

}

}

);

return this;

},

To unselect files, the first thing that you do is to remove the class name finderSelected using the removeClass() method. Then the elements passed to unselectFile(), which are made available in the this keyword, are assigned to a new variable called files. This is done to make the elements available within the anonymous function passed to the grep() method. You then verify that finder.selectedFiles is an array and contains one or more items. The grep() method is used to filter the finder.selectedFiles array. The anonymous function provided to grep() is executed once for every item in the array. If the anonymous function provided to grep() returns true, then the item remains in the array. If the anonymous function returns false, however, then the item is removed from the array. In the context of this example, if the file is among the files to be unselected, then file or files are removed from the finder.selectedFiles array via grep().

unselectFile : function()

{

this.removeClass('finderSelected');

var files = this;

if (finder.selectedFiles instanceof Array && finder.selectedFiles.length)

{

finder.selectedFiles = $.grep(

finder.selectedFiles,

function(file, index)

{

return $.inArray(file, files) == -1;

}

);

}

return this;

}

});

The unselectFile() method then returns the files that were unselected so that you can potentially chain method calls together.

You next set up a new object called finder.

var finder = {

The property finder.selectingFiles is used to keep track of whether a selection of files is presently underway using the Selectable plugin. The default value is set to false to indicate that there is no selection of files underway.

The property finder.selectedFiles contains an empty array by default. As you saw with the jQuery plugins selectFile() and unselectFile(), when one or more files are selected, a reference to each selected node is stored in the selectedFiles property.

selectingFiles : false,

selectedFiles : [],

The method unselectSelected() unselects every file node that is presently selected, and then the property is reset to an empty array. This method is simply a quick and easy way to unselect every file.

unselectSelected : function()

{

if (this.selectedFiles instanceof Array && this.selectedFiles.length)

{

$(this.selectedFiles).each(

function()

{

$(this).unselectFile();

}

);

}

this.selectedFiles = [];

},

As you have read, the ready() method is executed upon the DOMContentLoaded event.

ready : function()

{

Every directory and every file receives a mousedown event and is made draggable using jQuery UI's Draggable plugin. Every directory is also made a drop target using the Droppable plugin.

$('div.finderDirectory, div.finderFile')

Within the mousedown event, if there is no selection presently underway, which is tracked in the finder.selectingFiles property, all files are unselected, and then whichever file element is receiving the mousedown event is selected.

.mousedown(

function()

{

if (!finder.selectingFiles)

{

finder.unselectSelected();

$(this).selectFile();

}

}

)

The Draggable plugin is enabled by calling the draggable() method; the dragged element is set to clone the file where the drag originated, creating a ghost of the element being dragged. The ghost element is also set to receive opacity of 50 percent, making it semi-transparent (or semi-opaque, depending on your view).

.draggable({

helper : 'clone',

opacity : 0.5

});

Even though this example contains only directory objects, you set up the example prepared to deal with both directory and ordinary file objects. Each directory is distinguished from regular files via the class name assigned. The finderDirectory class name is given to directories, and the finderFile class name is given to regular files.

Directory objects are made droppable using the Droppable jQuery UI plugin; a call to the droppable() method enables a directory as a drop target. As you learned in Chapter 12, jQuery UI is just one way of implementing drag and drop. The more complicated HTML5 drag-and-drop API is another option, and it's the option that I recommend if you need to drag and drop between multiple browser windows. In the interest of keeping the example simple, I stuck with the simpler jQuery UI draggable and droppable plugins.

$('div.finderDirectory').droppable({

accept : 'div.finderDirectory, div.finderFile',

hoverClass : 'finderDirectoryDrop',

drop : function(event, ui)

{

var path = ui.draggable.data('path');

ui.draggable.remove();

}

});

An example of the Selectable jQuery UI plugin follows next. The contents of the <div> with id name finderFiles is made selectable. The option appendTo is provided a selector that tells the selectable() plugin where to put the <div> element that represents the selection box. The selection box is added to the <div> element with the id finderFiles.

The option filter is used to tell the selectable() plugin which elements it contains are selectable, and you do that by providing a selector to it to describe those selectable elements. The selector div.finderDirectory, div.finderFile makes the <div> elements with class names finderDirectory or finderFile selectable.

$('div#finderFiles').selectable({

appendTo : 'div#finderFiles',

filter : 'div.finderDirectory, div.finderFile',

The option start is provided a callback function that fires each time a new selection begins. As you learned in Chapter 12, each option that specifies a custom UI plugin event accepts two arguments, one for the event and another for passing additional UI plugin data. In this example, when selection begins, the property finder.selectingFiles is set to true, and this is used to prevent the mousedown event that you created earlier from also selecting files because that would conflict with the selection taking place using the selectable()plugin. In addition, any file selection that is already in place is completely cleared by calling finder.unselectSelected().

start : function(event, ui)

{

finder.selectingFiles = true;

finder.unselectSelected();

},

When selection ends, the callback function provided to the option stop is fired. This callback function sets the property finder.selectingFiles to false so that selection of individual files or directories using the mousedown event you set up previously can again take place.

stop : function(event, ui)

{

finder.selectingFiles = false;

},

While selection is happening, the callback function provided to the option selecting is continuously fired. The objects that are experiencing a selection are provided to you and described in the selector passed in the ui.selecting property. Those items are in turn selected by calling selectFile() on the individual item or collection of items.

selecting : function(event, ui)

{

$(ui.selecting).selectFile();

},

While selection is happening, as items are included in a selection, sometimes items are also excluded from a selection. When items are excluded from a selection in progress, the custom event callback function assigned to the unselecting option is fired. Like theselecting option, the unselecting option also receives data in the ui argument. A selector is provided to the ui.unselecting property, which contains the file nodes that should be unselected; each file that should be unselected is unselected using a call to unselectFile().

unselecting : function(event, ui)

{

$(ui.unselecting).unselectFile();

}

});

Although it is a niche feature that is not called for often in programming, the jQuery UI Selectable plugin provides useful functionality that has been with computing since the dawn of the graphical user interface.

NOTE Complete API documentation for the Selectable plugin is available in Appendix L.

Summary

In this chapter, you learned about the jQuery UI Selectable plugin, which provides functionality for making selections by drawing a box with your mouse cursor. You saw how the Selectable plugin can be applied to the Finder clone that you made in Chapter 12.

The Selectable plugin, like jQuery UI's other plugins, accepts an object literal of options that are specified in key, value form. The Selectable plugin lets you specify callback functions for selectable events. Callback functions provided to the options start and stop are executed when a selection begins and ends, respectively. Callback functions provided to the options selecting and unselecting are executed as items are added and removed from a selection while a selection is taking place.

Exercises

1. Which option do you use to execute the callback function when a selection begins?

2. What options do you use to execute callback functions when items are added to or removed from a selection (while a selection is taking place)?

3. When using the selecting and unselecting options, how do you access each element added to and removed from the selection?

4. What selector would you add to a style sheet to customize the look and feel of the selection box?