Tuesday, February 25, 2014

Programmatic Selection With The Selectable Widget

The jQuery UI selectable is less a widget than a utility. Technically, it's a widget in that you apply it to elements the same as you would a standard widget — like tabs or progressbar. Really, selectable is more like the other interaction widgets found in jQuery UI — sortable and resizable, for example. Interactions are designed as utilities, used to help out the larger, more fleshed out widgets. Take the dialog, it relies on both the position and the resizable utilities. So who does selectable help out? Anything we do, really, that requires a selection. The trouble is with filling in some fundamental gaps. Like programmatically-selecting items.

With the select HTML element, you can mark which options are selected. This happens before any user interaction. So why not introduce the same capability into the selectable widget? I've had to do this with select elements before, but I'd much rather use the selectable widget.

Here's an example extension that illustrates how to add a select option to selectable. The option tells the widget, by index, which items to select. Essentially doing the same thing as the user, but via an option API with code. Here is what the _setOption() method of our extension looks like.

_setOption: function( key, value ) {
            
    this._super( key, value );
            
    if ( key !== "select" ) {
        return;
    }
            
    var selectees = this.selectees,
        self = this;
        
    if ( $.isNumeric( value ) ) {
        value = [ value ];    
    }
            
    if ( !$.isArray( value ) ) {
        return;
    }
            
    $.each( value, function() {
                
        var selectee = selectees[ this ],
            offset = $( selectee ).offset(),
            e = {
                pageX: offset.left,
                pageY: offset.top,
                target: selectee,
                ctrlKey: true
            };
               
        self._mouseStart( e );
        self._mouseStop( e );
                
    });
            
}

The _setOption() method is called whenever a widget wants to set a new option value. So this is a logical extension point for adding new data to widgets. As is the case with this selectable extension — we're adding new data about which items should be selected. The nice thing about _setOption() is that it works the same way no matter if the options were specified in the constructor, or if the options were updated down the road.

The first thing our implementation of _setOption() does is call the original implementation. This ensures that we're properly setting the values of other widget options that we're not interested in changing. We also have to make sure that the key argument is what we're after — "select" — otherwise, we short-circuit and do nothing.

Next, the variables selectees and self are setup. The self variable is a reference to this selectable widget that's used in a nested scope later on. The selectees variable is the jQuery object representing the available elements for selection. We'll use this to lookup specific items by index in our loop later on. Many jQuery UI widget options are flexible enough to know what to do when you pass in different argument types. We'll offer a degree of flexibility by allowing either a number, or an array. If it's a number, we make it into a one-element array. If it's not an array, we do nothing and exit.

Now that we have an array of index values we want selected, it's time to iterate over that array and make the required selections. To do this, we'll have to create a mock event object. This object needs the position that was "clicked" and we pass these coordinates to pageX, and pageY. The target property of the mock event is the selectee that we looked up by index. The ctrlKey is required since we could be marking multiple selections.

With the event data in place, we can pass it to the _mouseStart() and _mouseStop() methods. This simulates the user making a selection, only instead of an actual mouse click, we're hiding behind widget options in our extension. The reason there's so many steps involved with setting this option is that this is the only realistic way that we can "fake" an item selection. The selectable widget uses the jQuery UI mouse utility, since the selectable has the capability to "lasso" several items by clicking, and dragging the mouse pointer. Think of the _mouseStart() and _mouseStop() and the mock event we have to construct as a roundabout way to call .click().