jQuery UI Cookbook
上QQ阅读APP看书,第一时间看更新

Filling space with buttons automatically

The width of any given button widget is controlled by what goes inside it. What this amounts to is either the primary or secondary icons, or neither, plus the text. The actual rendered width of the button itself isn't concretely specified, but instead is determined by the browser. Of course, this is a desirable feature of any widget—relying on the browser to compute dimensions. This approach scales well when there are lots of widgets in the UI to consider, and when there are lots of browser resolution configurations to consider.

There are, however, a few cases where the automatic width set forth by the browser isn't desirable. Think about several buttons in the same context, perhaps a div element. In all likelihood, these buttons will not render as having the same width, when this is in fact a desired property. Just because one button in the group has slightly more or slightly less text doesn't mean that we don't want them to share a consistent width.

Getting ready

The goal here is to treat the widest button within a group of buttons as the target width. The siblings of the group get notified when a new button is added, potentially creating a new target width if it is the widest. Let's illustrate the problem further by looking at the default button functionality and what it means in terms of width.

Here is the HTML we'll use to create the button widgets.

<div>
    <button style="display: block;">Button 1</button>
    <button style="display: block;">Button 2</button>
    <button style="display: block;">Button with longer text</button>
</div>

We're explicitly marking each button as a block-level element so we can easily contrast the widths. Notice, too, that the buttons are all siblings.

The following JavaScript turns each button element into a button widget.

$(function() {
    $( "button" ).button();
});

As you can see, the first two buttons are of the same length while the last button uses more text and is the widest.

How to do it...

Let's now extend the button widget with some new behavior that allows the developer to synchronize the width of each button within a group. The modified JavaScript code to extend the button widget looks like this:

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        matchWidth: false
    },

    _create: function() {

        this._super( "create" );

        if ( !this.options.matchWidth ) {
            return;
        }

        this.element.siblings( ":" + this.widgetFullName )
                    .addBack()
                    .button( "refresh" );

    },

    refresh: function() {

        this._super( "refresh" );

        if ( !this.options.matchWidth ) {
            return;
        }
    
        var widths = this.element
                         .siblings( ":" + this.widgetFullName )
                         .addBack()
                         .children( ".ui-button-text" )
                         .map(function() {
                            return $( this ).width();
                         }),
            maxWidth = Math.max.apply( Math, widths ),
            buttonText = this.element.children( ".ui-button-text" );

        if ( buttonText.width() < maxWidth ) {
            buttonText.width( maxWidth );
        }

    }

});

})( jQuery );

$(function() {
    $( "button" ).button( { matchWidth: true } );
});

Here you can see that the buttons communicate with one another to establish the correct width for each sibling within the group. In other words, the first two buttons altered their widths as a result of the last button being added to the group.

How it works...

The extension to the button widget we've just added creates a new matchWidth option, which, if true, will change the width of this button to match that of the widest in this group if necessary.

Our extension of the _create() method calls the default _create() button implementation, and then we tell all our siblings to refresh(). We include this button in the list of siblings by using addBack()—the reason being, we might have to adjust our own width if there is already someone bigger than us. Alternatively, if we're now the widest sibling, we have to tell everyone so that they can adjust their widths.

The refresh() method calls the base refresh() implementation, then figures out whether the width of this button should be updated or not. The first step is to generate a width array for all siblings in the group, including ourselves. With an array of widths, we can pass it to Math.max() to get the maximum width. If the current width of this button is less than the widest button in the group, we adjust to the new width.

Notice that we're not actually collecting or changing the width of the button element itself, but rather, the span element within. This span has the ui-button-text class, and is the element of variable width we're interested in. If we took the other route of simply measuring the button's width, we could end up with some messy margin issues that leave us in a state worse than we were in to begin with.

There's more...

You'll notice in the previous example that the text of the resized buttons remained centered. We could, if so inclined, introduce a small CSS adjustment when making button width changes that would keep the button text aligned.

(function( $, undefined ) {

$.widget( "ab.button", $.ui.button, {

    options: {
        matchWidth: false
    },

    _create: function() {

        this._super( "create" );

        if ( !this.options.matchWidth ) {
            return;
        }

        this.element.siblings( ":" + this.widgetFullName )
                    .addBack()
                    .button( "refresh" );

    },

    _destroy: function() {
        this._super();
        this.element.css( "text-align", "" );
    },

    refresh: function() {

        this._super( "refresh" );

        if ( !this.options.matchWidth ) {
            return;
        }
    
        var widths = this.element
                         .siblings( ":" + this.widgetFullName )
                         .addBack()
                         .children( ".ui-button-text" )
                         .map(function() {
                            return $( this ).width();
                         }),
            maxWidth = Math.max.apply( Math, widths ),
            buttonText = this.element.children( ".ui-button-text" );

        if ( buttonText.width() < maxWidth ) {
            buttonText.width( maxWidth );
            this.element.css( "text-align", "left" );
        }

    }

});

})( jQuery );

$(function() {
    $( "button" ).button( { matchWidth: true } );
});

Notice that within the _refresh() method, we're now stating that the text-align CSS property is left. Additionally, we have to add a new _destroy() method to clean up this property when the button is destroyed. The end result is the same as our previous example, except now the button text is aligned.