Popover automatic placement

by Alessandro Vernet

HTML

<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css">
<script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<div class="container">
    <h3>Requirements</h3>
    
    We want to use Bootstrap 2.3.2 <a href="http://getbootstrap.com/2.3.2/javascript.html#popovers">popovers</a> to show a help message that relates to a specific form control. The popover should show on the right or the left of the control, whichever side has the most space. The popover is shown on users clicking on a help icon displayed next to the label. The popover hides only when users explicitely close it, but clicking on the <i>x</i> to the right of the popover title. At most, one popover should be shown; i.e. if the popover for control A is shown, and users open the help for control B, the popover for A is closed before the popover for B is shown.
    
    <h3>Controls</h3>
    
    <form class="xforms-form">
        <div class="row">
            <div class="span6">
                <span class="xforms-control">
                    <label class="xforms-label">
                        <i class="icon-question-sign xforms-help"></i> 
                        First name
                    </label>
                    <input></input>
                </span>
            </div>
            <div class="span6">
                <span class="xforms-control">
                    <label class="xforms-label">
                        <i class="icon-question-sign xforms-help"></i> 
                        First name
                    </label>
                    <input></input>
                </span>
            </div>
        </div>
        <div class="row">
            <div class="span12">
                <span class="xforms-control">
                    <label class="xforms-label">
                        <i class="icon-question-sign...

CSS

/* For testing */
input, textarea { box-sizing: border-box; width: 100% }
textarea { height: 200px }
.container { margin-top: 100px; margin-bottom: 300px }

/* Part of the "real" CSS */

.popover .close {
    /* Positioning of the close 'x', using padding so this also extends the clickable area */
    padding: 5px 10px;
}

.popover .popover-content {
    /* So we have scrollbars when necessary */
    overflow: auto;
    /* So the height we set in JS is correct */
    box-sizing: border-box;
}

JavaScript

$('body').on('click', '.xforms-help', function() {
    var controlEl = $(this).closest('.xforms-control');
    showHelp(controlEl, "First name", testHelpMessage); 
});

// Hide any help popover when users press escape
$(document).keyup(function(e) {
    if (e.keyCode == 27) { hideHelp(); }
});

function hideHelp() {
    _.each($('form.xforms-form'), function(form) {
        var popoverContainer = $(form).children('.xforms-help-popover');
        var previousPopover = popoverContainer.children('.popover');
        if (previousPopover.is('*')) {
            var previousEl = previousPopover.data('xforms-for');
            previousEl.popover('destroy');
        }
    });
}

function showHelp(controlEl, labelText, helpText) {

    // We want the arrow to point to the form field, not somewhere between the label and the field, 
    // hence here we look for the first element which is not an LHHA
    var firstContentEl = (function() {
        var lhhaClasses = '.xforms-label, .xforms-help, .xforms-hint, .xforms-alert';
        return controlEl.children().not(lhhaClasses).first();
    })();

    // Position and size of element relative to which we position the popover
    var scrollTop = $(document).scrollTop(); // Will this work if we're in a scrollable area?
    var elOffset  = firstContentEl.offset();
    var elWidth   = firstContentEl.outerWidth();
    var elHeight  = firstContentEl.outerHeight();
    
    // Is there most space on the left or the right?
    var placement = (function() {
        var RequiredSpace = 300;
        var space = {
            left:   elOffset.left,
            right:  $(window).width() - (elOffset.left + elWidth),
            top:    elOffset.top - scrollTop,
            bottom: $(window).height() - (elOffset.top - scrollTop + elHeight)
        }
        return (space.right >= RequiredSpace || space.left >= RequiredSpace)
             ?  space.right >= space.left ? 'right' : 'left'
             : (space.top   >= RequiredSpace ||...