Example: Creating a Slider from Existing Markup

This example illustrates a few points:

  1. How to create a Slider using existing markup
  2. How to disable a Slider
  3. How to use an image sprite to create a custom Slider skin

The visualization of the Slider is based on the volume control in Mac OS X 10.5, with additional controls included for illustration. Click on the speaker icon to show the Slider.

Nulla facilisi. In vel sem. Morbi id urna in diam dignissim feugiat. Proin molestie tortor eu velit. Aliquam erat volutpat. Nullam ultrices, diam tempus vulputate egestas, eros pede varius leo, sed imperdiet lectus est ornare odio.

Phasellus wisi purus, interdum vitae, rutrum accumsan, viverra in, velit. Sed enim risus, congue non, tristique in, commodo eu, metus. Aenean tortor mi, imperdiet id, gravida eu, posuere eu, felis.

Progressive Enhancement

The Progressive Enhancement strategy recommends that your page not contain markup that will only be useful in cases where JavaScript is available. For this reason, Slider does not include an HTML_PARSER to reuse existing markup. However, it is possible to override a couple methods to accomplish the task.

The starting markup for the volume control area is as follows:

<div id="volume_control" class="volume-hide">
    <label for="volume">volume</label><input type="text" size="3" maxlength="3" name="volume" id="volume" value="50">
    <button type="button" id="volume_icon" class="level_2" title="Open volume slider"><p>Open</p></button>

    <span id="volume_slider">
        <span class="yui3-slider-rail">
            <span class="yui3-slider-thumb"><img src="../assets/slider/images/sprite.png" height="384" width="31"></span>
        </span>
    </span>

    <label for="mute"><input type="checkbox" id="mute"> mute</label>
</div>

To tell the Slider to use the existing rail and thumb elements, override the renderRail and renderThumb methods.

var volume = new Y.Slider({
    axis  : 'y',
    min   : 100, // reverse min and max to make the top
    max   : 0,   // equal 100 and the bottom 0
    value : 50,
    length: '105px'
});

// Override renderRail to just return the existing rail node
volume.renderRail = function () {
    return Y.one( "#volume_slider span.yui3-slider-rail" );
};
// Override renderThumb to just return the existing thumb node
volume.renderThumb = function () {
    return this.rail.one( "span.yui3-slider-thumb" );
};

volume.render( "#volume_slider" );

Hide and show the Slider

By default, we want the Slider to be hidden until the user clicks on the speaker icon. However, we want to support muting or changing the value of the Slider while it is hidden.

var control = Y.one('#volume_control'),
    icon    = Y.one('#volume_icon'),
    open    = false;

function showHideSlider(e) {
    control.toggleClass('volume-hide');
    open = !open;

    if (e) {
        e.preventDefault();
    }
}

icon.on('click', showHideSlider);

// Also support hiding the Slider when the user clicks outside the
// Slider element.
function handleDocumentClick(e) {
    if (open && !icon.contains(e.target) &&
            !volume.get('boundingBox').contains(e.target)) {
        showHideSlider();
    }
}

Y.one( 'doc' ).on('click', handleDocumentClick );

Mute and unmute

We want to disable the Slider and input and set the value to 0 if a user checks the mute checkbox. The value should be returned to the last assigned value when unmuted. To disable the Slider, set its disabled attribute to true.

var volInput   = Y.one('#volume'),
    mute       = Y.one('#mute'),
    beforeMute = 0;

function muteVolume(e) {
    // Set disabled to false if currently true; true if currently false
    var disabled = !volume.get('disabled');
    volume.set('disabled', disabled);

    if (disabled) {
        beforeMute = volume.getValue();
        volume.setValue(0);
        volInput.set('disabled','disabled');
    } else {
        volume.setValue(beforeMute);
        volInput.set('disabled','');
    }
}

mute.on('click', muteVolume);

Skinning and CSS

Sprite of all custom image resources for this example

We'll be using the image sprite to the left to create a custom skin. In this design, to keep things simple, the Slider's container and end caps are all rendered together at the bottom of the sprite.

Slider's thumb range is constrained by the rail element, so it wouldn't be appropriate to use this image as the rail's background—the thumb would slide off the ends. Instead, the rail image is assigned as the background to the Slider's containing element #volume_slider. Then the default skin background image is removed on the rail.

/* rail image on the containing box rather than the rail element */
#volume_slider {
    background: url("assets/images/sprite.png") no-repeat 0 -259px;
    height: 116px;
    width: 31px;
    padding-top: 9px;
}

#volume_slider .yui3-slider-rail {
    background-image: none;
    width: 31px;
}

#volume_slider .yui3-slider-thumb {
    height: 17px;
    width: 31px;
    overflow: hidden;
}

#volume_slider .yui3-slider-thumb img {
    position: absolute;
    top: -225px;
}

#volume_slider .yui3-slider-disabled .yui3-slider-thumb img {
    top: -242px;
}

You can see the full CSS and JavaScript for the other controls in the Full Code Listing below.

Full Code Listing

Here is the full markup, CSS, and JavaScript for the entire example, including the volume input and mute controls, and CSS for placing the Slider and setting up the volume icon sprite positioning.

Markup

Note: be sure to add the yui3-skin-sam classname to the page's <body> element or to a parent element of the widget in order to apply the default CSS skin. See Understanding Skinning.

<div id="demo" class="yui3-skin-sam"> <!-- You need this skin class -->

    <div id="volume_control" class="volume-hide">
        <label for="volume">volume</label><input type="text" size="3" maxlength="3" name="volume" id="volume" value="50">
        <button type="button" id="volume_icon" class="level_2" title="Open volume slider"><p>Open</p></button>
        <span id="volume_slider">
            <span class="yui3-slider-rail">
                <span class="yui3-slider-thumb"><img src="../assets/slider/images/sprite.png" height="384" width="31"></span>
            </span>
        </span>
        <label for="mute"><input type="checkbox" id="mute"> mute</label>
    </div>

    <div class="demo-content">
        <p>Nulla facilisi. In vel sem. Morbi id urna in diam dignissim feugiat. Proin molestie tortor eu velit. Aliquam erat volutpat. Nullam ultrices, diam tempus vulputate egestas, eros pede varius leo, sed imperdiet lectus est ornare odio.</p>
        <p>Phasellus wisi purus, interdum vitae, rutrum accumsan, viverra in, velit. Sed enim risus, congue non, tristique in, commodo eu, metus. Aenean tortor mi, imperdiet id, gravida eu, posuere eu, felis.</p>
    </div>
</div>

JavaScript

YUI().use("slider", function (Y) {

var control    = Y.one('#volume_control'),
    volInput   = Y.one('#volume'),
    icon       = Y.one('#volume_icon'),
    mute       = Y.one('#mute'),
    open       = false,
    level      = 2,
    beforeMute = 0,
    wait,
    volume;

Y.one("#volume_slider").setStyle('left',icon.get('offsetLeft')+'px');

volume = new Y.Slider({
    axis  : 'y',
    min   : 100,
    max   : 0,
    value : 50,
    length: '105px'
});

volume.renderRail = function () {
    return Y.one( "#volume_slider span.yui3-slider-rail" );
};
volume.renderThumb = function () {
    return this.rail.one( "span.yui3-slider-thumb" );
};

volume.render( "#volume_slider" );

// Initialize event listeners
volume.after('valueChange', updateInput);
volume.after('valueChange', updateIcon);

mute.on('click', muteVolume);

volInput.on({
    keydown : handleInput,
    keyup   : updateVolume
});

icon.on('click', showHideSlider);

Y.one( 'doc' ).on('click', handleDocumentClick );

// Support functions
function updateInput(e) {
    if (e.src !== 'KEY') {
        volInput.set('value',e.newVal);
    }
}

function updateIcon(e) {
    var newLevel = e.newVal && Math.ceil(e.newVal / 34);

    if (level !== newLevel) {
        icon.replaceClass('level_'+level, 'level_'+newLevel);
        level = newLevel;
    }
}

function muteVolume(e) {
    var disabled = !volume.get('disabled');
    volume.set('disabled', disabled);

    if (disabled) {
        beforeMute = volume.getValue();
        volume.setValue(0);
        volInput.set('disabled','disabled');
    } else {
        volume.setValue(beforeMute);
        volInput.set('disabled','');
    }
}

function handleInput(e) {
    // Allow only numbers and various other control keys
    if (e.keyCode > 57) {
        e.halt();
    }
}

function updateVolume(e) {
    // delay input processing to give the user time to type
    if (wait) {
        wait.cancel();
    }

    wait = Y.later(400, null, function () {
        var value = parseInt(volInput.get('value'),10) || 0;

        if (value > 100) {
            volInput.set('value', 100);
            value = 100
        }

        volume.setValue( value );
    });
}

function showHideSlider(e) {
    control.toggleClass('volume-hide');
    open = !open;

    if (e) {
        e.preventDefault();
    }
}

function handleDocumentClick(e) {
    if (open && !icon.contains(e.target) &&
            !volume.get('boundingBox').contains(e.target)) {
        showHideSlider();
    }
}

});

CSS

<style scoped>
    #demo {
        background: #fff;
        border: 1px solid #999;
        color: #000;
    }

    #demo .demo-content {
        padding: 1ex 1em;
    }

    #volume_control {
        height: 25px;
        line-height: 25px;
        background: url(../assets/slider/images/sprite.png) repeat-x 0 0;
        position: relative;
    }

    #volume_control label {
        font-weight: bold;
        margin: 0 1ex 0 1em;
        zoom: 1;
    }

    #volume {
        border: 1px inset #999;
        height: 16px;
        margin-top: 3px;
        padding: 0 3px;
        text-align: right;
        width: 2em;
    }

    /* Support open/close action for the slider */
    #demo .volume-hide #volume_slider {
        display: none;
    }

    #volume_icon {
        background: url(../assets/slider/images/sprite.png) no-repeat 0 -25px;
        border: 0 none;
        height: 25px;
        vertical-align: top;
        width: 31px;
    }

    /* move the button text offscreen left */
    #volume_icon p {
        text-indent: -9999px;
    }

    /*
     * adjust the speaker icon sprite in accordance with volume level and
     * active state
    */
    #demo .volume-hide .level_0 { background-position: 0 -25px; }
    #demo .volume-hide .level_1 { background-position: 0 -50px; }
    #demo .volume-hide .level_2 { background-position: 0 -75px; }
    #demo .volume-hide .level_3 { background-position: 0 -100px; }

    #demo .level_0,
    #demo .level_0:hover {
        background-position: 0 -125px;
    }
    #demo .level_1,
    #demo .level_1:hover {
        background-position: 0 -150px;
    }
    #demo .level_2,
    #demo .level_2:hover {
        background-position: 0 -175px;
    }
    #demo .level_3,
    #demo .level_3:hover {
        background-position: 0 -200px;
    }

    #volume_slider {
        position: absolute;
        top: 25px;
    }

    /* rail image on the containing box rather than the rail element */
    #volume_slider {
        background: url(../assets/slider/images/sprite.png) no-repeat 0 -259px;
        height: 116px;
        width: 31px;
        padding-top: 9px;
        cursor: arrow;
    }

    #volume_slider .yui3-slider-rail {
        background-image: none;
        width: 31px;
    }

    #volume_slider .yui3-slider-thumb {
        height: 17px;
        width: 31px;
        overflow: hidden;
    }

    #volume_slider .yui3-slider-thumb img {
        position: absolute;
        top: -225px;
    }

    #volume_slider .yui3-slider-disabled .yui3-slider-thumb img {
        top: -242px;
    }

    #demo_sprite {
        display: inline;
        float: left;
        margin-right: 1em;
    }
</style>