/*
 *  jquery-autocomplete-light - v3.5.0
 *  Dead simple autocompletion and widgets for jQuery
 *  http://yourlabs.org
 *
 *  Made by James Pic
 *  Under MIT License
 */
/*
Here is the list of the major difference with other autocomplete scripts:

- don't do anything but fire a signal when a choice is selected: it's
left as an exercise to the developer to implement whatever he wants when
that happens
- don't generate the autocomplete HTML, it should be generated by the server

Let's establish the vocabulary used in this script, so that we speak the
same language:

- The text input element is "input",
- The box that contains a list of choices is "box",
- Each result in the "autocomplete" is a "choice",
- With a capital A, "Autocomplete", is the class or an instance of the
class.

Here is a fantastic schema in ASCII art:

    +---------------------+ <----- Input
    | Your city name ? <---------- Placeholder
    +---------------------+
    | Paris, France       | <----- Autocomplete
    | Paris, TX, USA      |
    | Paris, TN, USA      |
    | Paris, KY, USA <------------ Choice
    | Paris, IL, USA      |
    +---------------------+

This script defines three signals:

- hilightChoice: when a choice is hilight, or that the user
navigates into a choice with the keyboard,
- dehilightChoice: when a choice was hilighed, and that the user
navigates into another choice with the keyboard or mouse,
- selectChoice: when the user clicks on a choice, or that he pressed
enter on a hilighted choice.

They all work the same, here's a trivial example:

  $('#your-autocomplete').bind(
      'selectChoice',
      function(e, choice, autocomplete) {
          alert('You selected: ' + choice.html());
      }
  );

Note that 'e' is the variable containing the event object.

Also, note that this script is composed of two main parts:

- The Autocomplete class that handles all interaction, defined as
`Autocomplete`,
- The jQuery plugin that manages Autocomplete instance, defined as
`$.fn.yourlabsAutocomplete`
*/

if (window.isOpera === undefined) {
    var isOpera = (navigator.userAgent.indexOf('Opera')>=0) && parseFloat(navigator.appVersion);
}

if (window.isIE === undefined) {
    var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split('MSIE ')[1].split(';')[0]);
}

if (window.findPosX === undefined) {
    window.findPosX = function(obj) {
        var curleft = 0;
        if (obj.offsetParent) {
            while (obj.offsetParent) {
                curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
                obj = obj.offsetParent;
            }
            // IE offsetParent does not include the top-level
            if (isIE && obj.parentElement){
                curleft += obj.offsetLeft - obj.scrollLeft;
            }
        } else if (obj.x) {
            curleft += obj.x;
        }
        return curleft;
    }
}

if (window.findPosY === undefined) {
    window.findPosY = function(obj) {
        var curtop = 0;
        if (obj.offsetParent) {
            while (obj.offsetParent) {
                curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
                obj = obj.offsetParent;
            }
            // IE offsetParent does not include the top-level
            if (isIE && obj.parentElement){
                curtop += obj.offsetTop - obj.scrollTop;
            }
        } else if (obj.y) {
            curtop += obj.y;
        }
        return curtop;
    }
}

// Our class will live in the yourlabs global namespace.
if (window.yourlabs === undefined) window.yourlabs = {};

// Fix #25: Prevent accidental inclusion of autocomplete_light/static.html
if (window.yourlabs.Autocomplete !== undefined)
    console.log('WARNING ! You are loading autocomplete.js **again**.');

yourlabs.getInternetExplorerVersion = function()
// Returns the version of Internet Explorer or a -1
// (indicating the use of another browser).
{
  var rv = -1; // Return value assumes failure.
  if (navigator.appName === 'Microsoft Internet Explorer')
  {
    var ua = navigator.userAgent;
    var re  = new RegExp('MSIE ([0-9]{1,}[.0-9]{0,})');
    if (re.exec(ua) !== null)
      rv = parseFloat( RegExp.$1 );
  }
  return rv;
};

$.fn.yourlabsRegistry = function(key, value) {
    var ie = yourlabs.getInternetExplorerVersion();

    if (ie === -1 || ie > 8) {
        // If not on IE8 and friends, that's all we need to do.
        return value === undefined ? this.data(key) : this.data(key, value);
    }

    if ($.fn.yourlabsRegistry.data === undefined) {
        $.fn.yourlabsRegistry.data = {};
    }

    if ($.fn.yourlabsRegistry.guid === undefined) {
        $.fn.yourlabsRegistry.guid = function() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
                /[xy]/g,
                function(c) {
                    var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
                    return v.toString(16);
                }
            );
        };
    }

    var attributeName = 'data-yourlabs-' + key + '-registry-id';
    var id = this.attr(attributeName);

    if (id === undefined) {
        id = $.fn.yourlabsRegistry.guid();
        this.attr(attributeName, id);
    }

    if (value !== undefined) {
        $.fn.yourlabsRegistry.data[id] = value;
    }

    return $.fn.yourlabsRegistry.data[id];
};

/*
The autocomplete class constructor:

- takes a takes a text input element as argument,
- sets attributes and methods for this instance.

The reason you want to learn about all this script is that you will then be
able to override any variable or function in it on a case-per-case basis.
However, overriding is the job of the jQuery plugin so the procedure is
described there.
*/
yourlabs.Autocomplete = function (input) {
    /*
    The text input element that should have an autocomplete.
    */
    this.input = input;

    // The value of the input. It is kept as an attribute for optimisation
    // purposes.
    this.value = '';

    /*
    It is possible to wait until a certain number of characters have been
    typed in the input before making a request to the server, to limit the
    number of requests.

    However, you may want the autocomplete to behave like a select. If you
    want that a simple click shows the autocomplete, set this to 0.
     */
    this.minimumCharacters = 2;

    /*
    In a perfect world, we would hide the autocomplete when the input looses
    focus (on blur). But in reality, if the user clicks on a choice, the
    input looses focus, and that would hide the autocomplete, *before* we
    can intercept the click on the choice.

    When the input looses focus, wait for this number of milliseconds before
    hiding the autocomplete.
     */
    this.hideAfter = 200;

    /*
    The server should have a URL that takes the input value, and responds
    with the list of choices as HTML. In most cases, an absolute URL is
    better.
     */
    this.url = false;

    /*
    Although this script will make sure that it doesn't have multiple ajax
    requests at the time, it also supports debouncing.

    Set a number of milliseconds here, it is the number of milliseconds that it
    will wait before querying the server. The higher it is, the less it will
    spam the server but the more the user will wait.
    */
    this.xhrWait = 200;

    /*
    As the server responds with plain HTML, we need a selector to find the
    choices that it contains.

    For example, if the URL returns an HTML body where every result is in a
    div of class "choice", then this should be set to '.choice'.
     */
    this.choiceSelector = '.choice';

    /*
    When the user hovers a choice, it is nice to hilight it, for
    example by changing it's background color. That's the job of CSS code.

    However, the CSS can not depend on the :hover because the user can
    hilight choices with the keyboard by pressing the up and down
    keys.

    To counter that problem, we specify a particular class that will be set
    on a choice when it's 'hilighted', and unset when it's
    'dehilighted'.
     */
    this.hilightClass = 'hilight';

    /*
    The value of the input is passed to the server via a GET variable. This
    is the name of the variable.
     */
    this.queryVariable = 'q';

    /*
    This dict will also be passed to the server as GET variables.

    If this autocomplete depends on another user defined value, then the
    other user defined value should be set in this dict.

    Consider a country select and a city autocomplete. The city autocomplete
    should only fetch city choices that are in the selected country. To
    achieve this, update the data with the value of the country select:

        $('select[name=country]').change(function() {
            $('city[name=country]').yourlabsAutocomplete().data = {
                country: $(this).val(),
            }
        });
     */
    this.data = {};

    /*
    To avoid several requests to be pending at the same time, the current
    request is aborted before a new one is sent. This attribute will hold the
    current XMLHttpRequest.
     */
    this.xhr = false;

    /*
    fetch() keeps a copy of the data sent to the server in this attribute. This
    avoids double fetching the same autocomplete.
     */
    this.lastData = {};

    // The autocomplete box HTML.
    this.box = $('<span class="yourlabs-autocomplete" ' +
        'data-input-id="' + this.input.attr('id') + '"></span>');

    /*
    We'll append the box to the container and calculate an absolute position
    every time the autocomplete is shown in the fixPosition method.

    By default, this traverses this.input's parents to find the nearest parent
    with an 'absolute' or 'fixed' position. This prevents scrolling issues. If
    we can't find a parent that would be correct to append to, default to
    <body>.
    */
    this.container = this.input.parents().filter(function() {
        return ['absolute', 'fixed'].indexOf($(this).css('position')) > -1;
    }).first();
    if (!this.container.length) this.container = $('body');
};

/*
Rather than directly setting up the autocomplete (DOM events etc ...) in
the constructor, setup is done in this method. This allows to:

- instanciate an Autocomplete,
- override attribute/methods of the instance,
- and *then* setup the instance.
 */
yourlabs.Autocomplete.prototype.initialize = function() {
    var ie = yourlabs.getInternetExplorerVersion();

    this.input
        .on('blur.autocomplete', $.proxy(this.inputBlur, this))
        .on('focus.autocomplete', $.proxy(this.inputClick, this))
        .on('keydown.autocomplete', $.proxy(this.inputKeyup, this));

    $(window).on('resize', $.proxy(function() {
        if (this.box.is(':visible')) this.fixPosition();
    }, this));

    if (ie === -1 || ie > 9) {
        this.input.on('input.autocomplete', $.proxy(this.refresh, this));
    }
    else
    {
        var events = [
            'keyup.autocomplete',
            'keypress.autocomplete',
            'cut.autocomplete',
            'paste.autocomplete'
        ]

        this.input.on(events.join(' '), function($e) {
            $.proxy(this.inputKeyup, this);
        })
    }

    /*
    Bind mouse events to fire signals. Because the same signals will be
    sent if the user uses keyboard to work with the autocomplete.
     */
    this.box
        .on('mouseenter', this.choiceSelector, $.proxy(this.boxMouseenter, this))
        .on('mouseleave', this.choiceSelector, $.proxy(this.boxMouseleave, this))
        .on('mousedown', this.choiceSelector, $.proxy(this.boxClick, this));

    /*
    Initially - empty data queried
    */
    this.data[this.queryVariable] = '';
};

// Unbind callbacks on input.
yourlabs.Autocomplete.prototype.destroy = function(input) {
    input
        .unbind('blur.autocomplete')
        .unbind('focus.autocomplete')
        .unbind('input.autocomplete')
        .unbind('keydown.autocomplete')
        .unbind('keypress.autocomplete')
        .unbind('keyup.autocomplete')
};

yourlabs.Autocomplete.prototype.inputBlur = function(e) {
    window.setTimeout($.proxy(this.hide, this), this.hideAfter);
};

yourlabs.Autocomplete.prototype.inputClick = function(e) {
    if (this.value.length >= this.minimumCharacters)
        this.show();
};

// When mouse enters the box:
yourlabs.Autocomplete.prototype.boxMouseenter = function(e) {
    // ... the first thing we want is to send the dehilight signal
    // for any hilighted choice ...
    var current = this.box.find('.' + this.hilightClass);

    this.input.trigger('dehilightChoice',
        [current, this]);

    // ... and then sent the hilight signal for the choice.
    this.input.trigger('hilightChoice',
        [$(e.currentTarget), this]);
};

// When mouse leaves the box:
yourlabs.Autocomplete.prototype.boxMouseleave = function(e) {
    // Send dehilightChoice when the mouse leaves a choice.
    this.input.trigger('dehilightChoice',
        [this.box.find('.' + this.hilightClass), this]);
};

// When mouse clicks in the box:
yourlabs.Autocomplete.prototype.boxClick = function(e) {
    var current = this.box.find('.' + this.hilightClass);

    this.input.trigger('selectChoice', [current, this]);
};

// Return the value to pass to this.queryVariable.
yourlabs.Autocomplete.prototype.getQuery = function() {
    // Return the input's value by default.
    return this.input.val();
};

yourlabs.Autocomplete.prototype.inputKeyup = function(e) {
    if (!this.input.is(':visible'))
        // Don't handle keypresses on hidden inputs (ie. with limited choices)
        return;

    switch(e.keyCode) {
        case 40: // down arrow
        case 38: // up arrow
        case 16: // shift
        case 17: // ctrl
        case 18: // alt
            this.move(e);
            break;

        case 9: // tab
        case 13: // enter
            if (!this.box.is(':visible')) return;

            var choice = this.box.find('.' + this.hilightClass);

            if (!choice.length) {
                // Don't get in the way, let the browser submit form or focus
                // on next element.
                return;
            }

            e.preventDefault();
            e.stopPropagation();

            this.input.trigger('selectChoice', [choice, this]);
            break;

        case 27: // escape
            if (!this.box.is(':visible')) return;
            this.hide();
            break;

        default:
            this.refresh();
    }
};

// This function is in charge of ensuring that a relevant autocomplete is
// shown.
yourlabs.Autocomplete.prototype.show = function(html) {
    // First recalculate the absolute position since the autocomplete may
    // have changed position.
    this.fixPosition();

    // Is autocomplete empty ?
    var empty = $.trim(this.box.find(this.choiceSelector)).length === 0;

    // If the inner container is empty or data has changed and there is no
    // current pending request, rely on fetch(), which should show the
    // autocomplete as soon as it's done fetching.
    if ((this.hasChanged() || empty) && !this.xhr) {
        this.fetch();
        return;
    }

    // And actually, fetch() will call show() with the response
    // body as argument.
    if (html !== undefined) {
        this.box.html(html);
    }

    // Don't display empty boxes.
    if (this.box.is(':empty')) {
        if (this.box.is(':visible')) {
            this.hide();
        }
        return;
    }

    var current = this.box.find('.' + this.hilightClass);
    var first = this.box.find(this.choiceSelector + ':first');
    if (first && !current.length) {
        first.addClass(this.hilightClass);
    }

    // Show the inner and outer container only if necessary.
    if (!this.box.is(':visible')) {
        this.box.css('display', 'block');
    }
};

// This function is in charge of the opposite.
yourlabs.Autocomplete.prototype.hide = function() {
    this.box.hide();
};

// This function is in charge of hilighting the right result from keyboard
// navigation.
yourlabs.Autocomplete.prototype.move = function(e) {
    // If the autocomplete should not be displayed then return.
    if (this.value.length < this.minimumCharacters) return true;

    // The current choice if any.
    var current = this.box.find('.' + this.hilightClass);

    // Prevent default browser behaviours on TAB and RETURN if a choice is
    // hilighted.
    if ($.inArray(e.keyCode, [9,13]) > -1 && current.length) {
        e.preventDefault();
    }

    // If not KEY_UP or KEY_DOWN, then return.
    // NOTE: with Webkit, both keyCode and charCode are set to 38/40 for &/(.
    //       charCode is 0 for arrow keys.
    //       Ref: http://stackoverflow.com/a/12046935/15690
    var way;
    if (e.keyCode === 38 && !e.charCode) way = 'up';
    else if (e.keyCode === 40 && !e.charCode) way = 'down';
    else return;

    // The first and last choices. If the user presses down on the last
    // choice, then the first one will be hilighted.
    var first = this.box.find(this.choiceSelector + ':first');
    var last = this.box.find(this.choiceSelector + ':last');

    // The choice that should be hilighted after the move.
    var target;

    // The autocomplete must be shown so that the user sees what choice
    // he is hilighting.
    this.show();

    // If a choice is currently hilighted:
    if (current.length) {
        if (way === 'up') {
            // The target choice becomes the first previous choice.
            target = current.prevAll(this.choiceSelector + ':first');

            // If none, then the last choice becomes the target.
            if (!target.length) target = last;
        } else {
            // The target choice becomes the first  next** choice.
            target = current.nextAll(this.choiceSelector + ':first');

            // If none, then the first choice becomes the target.
            if (!target.length) target = first;
        }

        // Trigger dehilightChoice on the currently hilighted choice.
        this.input.trigger('dehilightChoice',
            [current, this]);
    } else {
        target = way === 'up' ? last : first;
    }

    // Avoid moving the cursor in the input.
    e.preventDefault();

    // Trigger hilightChoice on the target choice.
    this.input.trigger('hilightChoice',
        [target, this]);
};

/*
Calculate and set the outer container's absolute positionning. We're copying
the system from Django admin's JS widgets like the date calendar, which means:

- the autocomplete box is an element appended to this.co,
- 
*/
yourlabs.Autocomplete.prototype.fixPosition = function() {
    var el = this.input.get(0)

    var zIndex = this.input.parents().filter(function() {
        return $(this).css('z-index') !== 'auto' && $(this).css('z-index') !== 0;
    }).first().css('z-index');

    this.box.appendTo(this.container).css({
        position: 'absolute',
        minWidth: parseInt(this.input.outerWidth()),
        top: (findPosY(el) + this.input.outerHeight()) + 'px',
        left: findPosX(el) + 'px',
        zIndex: zIndex
    });
};

// Proxy fetch(), with some sanity checks.
yourlabs.Autocomplete.prototype.refresh = function() {
    // Set the new current value.
    this.value = this.getQuery();

    // If the input doesn't contain enought characters then abort, else fetch.
    if (this.value.length < this.minimumCharacters)
      this.hide();
    else
      this.fetch();
};

// Return true if the data for this query has changed from last query.
yourlabs.Autocomplete.prototype.hasChanged = function() {
    for(var key in this.data) {
        if (!(key in this.lastData) || this.data[key] !== this.lastData[key]) {
            return true;
        }
    }
    return false;
};

// Manage requests to this.url.
yourlabs.Autocomplete.prototype.fetch = function() {
    // Add the current value to the data dict.
    this.data[this.queryVariable] = this.value;

    // Ensure that this request is different from the previous one
    if (!this.hasChanged()) {
        // Else show the same box again.
        this.show();
        return;
    }

    this.lastData = {};
    for(var key in this.data) {
        this.lastData[key] = this.data[key];
    }

    // Abort any current request.
    if (this.xhr) this.xhr.abort();

    // Abort any request that we planned to make.
    if (this.timeoutId) clearTimeout(this.timeoutId);

    // Make an asynchronous GET request to this.url in this.xhrWait ms
    this.timeoutId = setTimeout($.proxy(this.makeXhr, this), this.xhrWait);
};

// Wrapped ajax call to use with setTimeout in fetch().
yourlabs.Autocomplete.prototype.makeXhr = function() {
    this.input.addClass('xhr-pending');

    this.xhr = $.ajax(this.url, {
        type: 'GET',
        data: this.data,
        complete: $.proxy(this.fetchComplete, this)
    });
};

// Callback for the ajax response.
yourlabs.Autocomplete.prototype.fetchComplete = function(jqXHR, textStatus) {
    this.input.removeClass('xhr-pending');

    if (this.xhr === jqXHR) this.xhr = false;
    if (textStatus === 'abort') return;
    this.show(jqXHR.responseText);
};

/*
The jQuery plugin that manages Autocomplete instances across the various
inputs. It is named 'yourlabsAutocomplete' rather than just 'autocomplete'
to live happily with other plugins that may define an autocomplete() jQuery
plugin.

It takes an array as argument, the array may contain any attribute or
function that should override the Autocomplete builtin. For example:

  $('input#your-autocomplete').yourlabsAutocomplete({
      url: '/some/url/',
      hide: function() {
          this.outerContainer
      },
  })

Also, it implements a simple identity map, which means that:

  // First call for an input instanciates the Autocomplete instance
  $('input#your-autocomplete').yourlabsAutocomplete({
      url: '/some/url/',
  });

  // Other calls return the previously created Autocomplete instance
  $('input#your-autocomplete').yourlabsAutocomplete().data = {
      newData: $('#foo').val(),
  }

To destroy an autocomplete, call yourlabsAutocomplete('destroy').
*/
$.fn.yourlabsAutocomplete = function(overrides) {
    if (this.length < 1) {
        // avoid crashing when called on a non existing element
        return;
    }

    overrides = overrides ? overrides : {};
    var autocomplete = this.yourlabsRegistry('autocomplete');

    if (overrides === 'destroy') {
        if (autocomplete) {
            autocomplete.destroy(this);
            this.removeData('autocomplete');
        }
        return;
    }

    // Disable the browser's autocomplete features on that input.
    this.attr('autocomplete', 'off');

    // If no Autocomplete instance is defined for this id, make one.
    if (autocomplete === undefined) {
        // Instanciate Autocomplete.
        autocomplete = new yourlabs.Autocomplete(this);

        // Extend the instance with data-autocomplete-* overrides
        for (var key in this.data()) {
            if (!key) continue;
            if (key.substr(0, 12) !== 'autocomplete' || key === 'autocomplete')
                continue;
            var newKey = key.replace('autocomplete', '');
            newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
            autocomplete[newKey] = this.data(key);
        }

        // Extend the instance with overrides.
        autocomplete = $.extend(autocomplete, overrides);

        if (!autocomplete.url) {
            alert('Autocomplete needs a url !');
            return;
        }

        this.yourlabsRegistry('autocomplete', autocomplete);

        // All set, call initialize().
        autocomplete.initialize();
    }

    // Return the Autocomplete instance for this id from the registry.
    return autocomplete;
};

// Binding some default behaviors.
$(document).ready(function() {
    function removeHilightClass(e, choice, autocomplete) {
        choice.removeClass(autocomplete.hilightClass);
    }
    $(document).bind('hilightChoice', function(e, choice, autocomplete) {
        choice.addClass(autocomplete.hilightClass);
    });
    $(document).bind('dehilightChoice', removeHilightClass);
    $(document).bind('selectChoice', removeHilightClass);
    $(document).bind('selectChoice', function(e, choice, autocomplete) {
        autocomplete.hide();
    });
});

$(document).ready(function() {
    /* Credit: django.contrib.admin (BSD) */

    var showAddAnotherPopup = function(triggeringLink) {
        var name = triggeringLink.attr( 'id' ).replace(/^add_/, '');
        name = id_to_windowname(name);
        href = triggeringLink.attr( 'href' );

        if (href.indexOf('?') === -1) {
            href += '?';
        }

        href += '&winName=' + name;

        var height = 500;
        var width = 800;
        var left = (screen.width/2)-(width/2);
        var top = (screen.height/2)-(height/2);
        var win = window.open(href, name, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, copyhistory=no, width='+width+', height='+height+', top='+top+', left='+left)

        function removeOverlay() {
            if (win.closed) {
                $('#yourlabs_overlay').remove();
            } else {
                setTimeout(removeOverlay, 500);
            }
        }

        $('body').append('<div id="yourlabs_overlay"></div');
        $('#yourlabs_overlay').click(function() { 
            win.close(); 
            $(this).remove();
        });

        setTimeout(removeOverlay, 1500);
        
        win.focus();

        return false;
    }

    var dismissAddAnotherPopup = function(win, newId, newRepr) {
        // newId and newRepr are expected to have previously been escaped by
        newId = html_unescape(newId);
        newRepr = html_unescape(newRepr);
        var name = windowname_to_id(win.name);
        var elem = document.getElementById(name);

        if (elem) {
            if ($(elem).is('select')) {
                var o = new Option(newRepr, newId);
                elem.options[elem.options.length] = o;
                o.selected = true;
            }
        } else {
            alert('Could not get input id for win ' + name);
        }

        win.close();
    }

    window.dismissAddAnotherPopup = dismissAddAnotherPopup

    var html_unescape = function(text) {
    // Unescape a string that was escaped using django.utils.html.escape.
        text = text.replace(/</g, '');
        text = text.replace(/"/g, '"');
        text = text.replace(/'/g, '\'');
        text = text.replace(/&/g, '&');
        return text;
    }

    // IE doesn't accept periods or dashes in the window name, but the element IDs
    // we use to generate popup window names may contain them, therefore we map them
    // to allowed characters in a reversible way so that we can locate the correct
    // element when the popup window is dismissed.
    var id_to_windowname = function(text) {
        text = text.replace(/\./g, '__dot__');
        text = text.replace(/\-/g, '__dash__');
        text = text.replace(/\[/g, '__braceleft__');
        text = text.replace(/\]/g, '__braceright__');
        return text;
    } 

    var windowname_to_id = function(text) {
        text = text.replace(/__dot__/g, '.');
        text = text.replace(/__dash__/g, '-');
        text = text.replace(/__braceleft__/g, '[');
        text = text.replace(/__braceright__/g, ']');
        return text;
    }

    $( '.autocomplete-add-another' ).show().click(function(e) {
        e.preventDefault(  );
        showAddAnotherPopup( $( this ) );
    });
});

if (window.yourlabs === undefined) window.yourlabs = {};

yourlabs.RemoteAutocompleteWidget = {
    /*
    The default deck getValue() implementation just returns the PK from the
    choice HTML. RemoteAutocompleteWidget.getValue's implementation checks for
    a url too. If a url is found, it will post to that url and expect the pk to
    be in the response.

    This is how autocomplete-light supports proposing values that are not there
    in the database until user selection.
    */
    getValue: function(choice) {
        var value = choice.data('value');

        if (typeof(value) === 'string' && isNaN(value) && value.match(/^https?:/)) {
            $.ajax(this.autocompleteOptions.url, {
                async: false,
                type: 'post',
                data: {
                    'value': value
                },
                success: function(text, jqXHR, textStatus) {
                    value = text;
                }
            });

            choice.data('value', value);
        }

        return value;
    }
}

$(document).bind('yourlabsWidgetReady', function() {
    // Instanciate decks with RemoteAutocompleteWidget as override for all widgets with
    // autocomplete 'remote'.
    $('body').on('initialize', '.autocomplete-light-widget[data-bootstrap=rest_model]', function() {
        $(this).yourlabsWidget(yourlabs.RemoteAutocompleteWidget);
    });
});

/*
This script enables TextWidget, a widget for CharField that supports
autocomplete for comma-separated values.

It's organization is not final, there are a couple of things that are also used
in widget.js that will be re-factored probably in a script called lib.js.

The API however, is consistent with widget.js, and is not meant to change.

For now, the script is composed of these parts:

- a handful of jQuery extensions to ease treatment of comma separated values in
  an input,
- yourlabs.TextWidget is stripped version of yourlabs.Widget, to handle the
  behavior of a comma separated autocompleted input,
- yourlabsTextWidget jQuery extension which role is to manage TextWidget instances,
- yourlabsTextWidget initialization system, which supports dynamically added
  autocompletes (ie. admin inlines)
*/

jQuery.fn.getSelectionStart = function(){
    var r;
    // Written by jQuery4U
    // http://www.jquery4u.com/snippets/6-jquery-cursor-functions/#.UDPQ9xXtFw8
    if(this.lengh === 0) return -1;
    input = this[0];

    var pos = input.value.length;

    if (input.createTextRange) {
        if (window.getSelection) {
            r = window.getSelection(); //IE11
        } else {
            r = document.selection.createRange().duplicate();
            r.moveEnd('character', input.value.length);
        }
        if (r.text === '')
            pos = input.value.length;
        pos = input.value.lastIndexOf(r.text);
    } else if (typeof(input.selectionStart) !== undefined)
    pos = input.selectionStart;

    return pos;
}

jQuery.fn.getCursorPosition = function(){
    // Written by jQuery4U
    if (this.lengh === 0) return -1;
    return $(this).getSelectionStart();
}

// Return the word on which the cursor is on.
//
// Consider the pipe "|" as an ASCII representation of the cursor, with such an
// input value::
//
//     foo, bar|, baz
//
// getCursorWord would return 'bar'.
jQuery.fn.getCursorWord = function() {
    var value = $(this).val();
    var positions = $(this).getCursorWordPositions();
    return value.substring(positions[0], positions[1]);
}

// Return the offsets of the word on which the cursor is on.
//
// Consider the pipe "|" as an ASCII representation of the cursor, with such an
// input value::
//
//     foo, bar|, baz
//
// getCursorWord would return [6, 8].
jQuery.fn.getCursorWordPositions = function() {
    var position = $(this).getCursorPosition();
    var value = $(this).val();

    // find start of word
    for(var start=position - 1; start >= 0; start--) {
        if (value[start] === ',') {
            break;
        }
    }
    start = start < 0 ? 0 : start;

    // find end of word
    for(var end=position; end <= value.length - 1; end++) {
        if (value[end] === ',') {
            break;
        }
    }

    while(value[start] === ',' || value[start] === ' ') start++;
    while(value[end] === ',' || value[end] === ' ') end--;

    return [start, end + 1];
}

// TextWidget ties an input with an autocomplete.
yourlabs.TextWidget = function(input) {
    this.input = input;
    this.autocompleteOptions = {
        getQuery: function() {
            return this.input.getCursorWord();
        }
    }
}

// The widget is in charge of managing its Autocomplete.
yourlabs.TextWidget.prototype.initializeAutocomplete = function() {
    this.autocomplete = this.input.yourlabsAutocomplete(
        this.autocompleteOptions);

    // Add a class to ease css selection of autocompletes for widgets
    this.autocomplete.box.addClass(
        'autocomplete-light-text-widget');
};

// Bind Autocomplete.selectChoice signal to TextWidget.selectChoice()
yourlabs.TextWidget.prototype.bindSelectChoice = function() {
    this.input.bind('selectChoice', function(e, choice) {
        if (!choice.length)
            return // placeholder: create choice here

        $(this).yourlabsTextWidget().selectChoice(choice);
    });
};

// Called when a choice is selected from the Autocomplete.
yourlabs.TextWidget.prototype.selectChoice = function(choice) {
    var inputValue = this.input.val();
    var choiceValue = this.getValue(choice);
    var positions = this.input.getCursorWordPositions();

    var newValue = inputValue.substring(0, positions[0]);
    newValue += choiceValue;
    newValue += inputValue.substring(positions[1]);

    this.input.val(newValue);
    this.input.focus();
}

// Return the value of an HTML choice, used to fill the input.
yourlabs.TextWidget.prototype.getValue = function(choice) {
    return $.trim(choice.text());
}

// Initialize the widget.
yourlabs.TextWidget.prototype.initialize = function() {
    this.initializeAutocomplete();
    this.bindSelectChoice();
}

// Destroy the widget. Takes a widget element because a cloned widget element
// will be dirty, ie. have wrong .input and .widget properties.
yourlabs.TextWidget.prototype.destroy = function(input) {
    input
        .unbind('selectChoice')
        .yourlabsAutocomplete('destroy');
}

// TextWidget factory, registry and destroyer, as jQuery extension.
$.fn.yourlabsTextWidget = function(overrides) {
    var widget;
    overrides = overrides ? overrides : {};

    if (overrides === 'destroy') {
        widget = this.data('widget');
        if (widget) {
            widget.destroy(this);
            this.removeData('widget');
        }
        return
    }

    if (this.data('widget') === undefined) {
        // Instanciate the widget
        widget = new yourlabs.TextWidget(this);

        // Pares data-*
        var data = this.data();
        var dataOverrides = {
            autocompleteOptions: {
                // workaround a display bug
                minimumCharacters: 0,
                getQuery: function() {
                    // Override getQuery since we need the autocomplete to filter
                    // choices based on the word the cursor is on, rather than the full
                    // input value.
                    return this.input.getCursorWord();
                }
            }
        };
        for (var key in data) {
            if (!key) continue;

            if (key.substr(0, 12) === 'autocomplete') {
                if (key === 'autocomplete') continue;

                var newKey = key.replace('autocomplete', '');
                newKey = newKey.replace(newKey[0], newKey[0].toLowerCase())
                dataOverrides.autocompleteOptions[newKey] = data[key];
            } else {
                dataOverrides[key] = data[key];
            }
        }

        // Allow attribute overrides
        widget = $.extend(widget, dataOverrides);

        // Allow javascript object overrides
        widget = $.extend(widget, overrides);

        this.data('widget', widget);

        // Setup for usage
        widget.initialize();

        // Widget is ready
        widget.input.attr('data-widget-ready', 1);
        widget.input.trigger('widget-ready');
    }

    return this.data('widget');
}

$(document).ready(function() {
    $('body').on('initialize', 'input[data-widget-bootstrap=text]', function() {
        /*
        Only setup autocompletes on inputs which have
        data-widget-bootstrap=text, if you want to initialize some
        autocompletes with custom code, then set
        data-widget-boostrap=yourbootstrap or something like that.
        */
        $(this).yourlabsTextWidget();
    });

    // Solid initialization, usage::
    //
    //      $(document).bind('yourlabsTextWidgetReady', function() {
    //          $('body').on('initialize', 'input[data-widget-bootstrap=text]', function() {
    //              $(this).yourlabsTextWidget({
    //                  yourCustomArgs: // ...
    //              })
    //          });
    //      });
    $(document).trigger('yourlabsTextWidgetReady');

    $('.autocomplete-light-text-widget:not([id*="__prefix__"])').each(function() {
        $(this).trigger('initialize');
    });

    $(document).bind('DOMNodeInserted', function(e) {
        var widget = $(e.target).find('.autocomplete-light-text-widget');

        if (!widget.length) {
            widget = $(e.target).is('.autocomplete-light-text-widget') ? $(e.target) : false;

            if (!widget) {
                return;
            }
        }

        // Ignore inserted autocomplete box elements.
        if (widget.is('.yourlabs-autocomplete')) {
            return;
        }

        // Ensure that the newly added widget is clean, in case it was cloned.
        widget.yourlabsWidget('destroy');
        widget.find('input').yourlabsAutocomplete('destroy');

        widget.trigger('initialize');
    });
})

/*
Widget complements Autocomplete by enabling autocompletes to be used as
value holders. It looks very much like Autocomplete in its design. Thus, it
is recommended to read the source of Autocomplete first.

Widget behaves like the autocomplete in facebook profile page, which all
users should be able to use.

Behind the scenes, Widget maintains a normal hidden select which makes it
simple to play with on the server side like on the client side. If a value
is added and selected in the select element, then it is added to the deck,
and vice-versa.

It needs some elements, and established vocabulary:

- ".autocomplete-light-widget" element wraps all the HTML necessary for the
 widget,
- ".deck" contains the list of selected choice(s) as HTML,
- "input" should be the text input that has the Autocomplete,
- "select" a (optionnaly multiple) select
- ".remove" a (preferabely hidden) element that contains a value removal
 indicator, like an "X" sign or a trashcan icon, it is used to prefix every
 children of the deck
- ".choice-template" a (preferabely hidden) element that contains the template
 for choices which are added directly in the select, as they should be
 copied in the deck,

To avoid complexity, this script relies on extra HTML attributes, and
particularely one called 'data-value'. Learn more about data attributes:
http://dev.w3.org/html5/spec/global-attributes.html#embedding-custom-non-visible-data-with-the-data-attributes

When a choice is selected from the Autocomplete, its element is cloned
and appended to the deck - "deck" contains "choices". It is important that the
choice elements of the autocomplete all contain a data-value attribute.
The value of data-value is used to fill the selected options in the hidden
select field.

If choices may not all have a data-value attribute, then you can
override Widget.getValue() to implement your own logic.
*/

// Our class will live in the yourlabs global namespace.
if (window.yourlabs === undefined) window.yourlabs = {};

$.ajaxSettings.traditional = true

/*
Instanciate a Widget.
*/
yourlabs.Widget = function(widget) {
    // These attributes where described above.
    this.widget = widget;
    this.input = this.widget.find('input[data-autocomplete-url]');
    this.select = this.widget.find('select');
    this.deck = this.widget.find('.deck');
    this.choiceTemplate = this.widget.find('.choice-template .choice');

    // The number of choices that the user may select with this widget. Set 0
    // for no limit. In the case of a foreign key you want to set it to 1.
    this.maximumValues = 0;
    
    // Clear input when choice made? 1 for yes, 0 for no
    this.clearInputOnSelectChoice = '1';
}

// When a choice is selected from the autocomplete of this widget,
// getValue() is called to add and select the option in the select.
yourlabs.Widget.prototype.getValue = function(choice) {
    return choice.attr('data-value');
};

// The widget is in charge of managing its Autocomplete.
yourlabs.Widget.prototype.initializeAutocomplete = function() {
    this.autocomplete = this.input.yourlabsAutocomplete()

    // Add a class to ease css selection of autocompletes for widgets
    this.autocomplete.box.addClass('autocomplete-light-widget');
};

// Bind Autocomplete.selectChoice signal to Widget.selectChoice()
yourlabs.Widget.prototype.bindSelectChoice = function() {
    this.input.bind('selectChoice', function(e, choice) {
        if (!choice.length)
            return // placeholder: create choice here

        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        widget.selectChoice(choice);

        widget.widget.trigger('widgetSelectChoice', [choice, widget]);
    });
};

// Called when a choice is selected from the Autocomplete.
yourlabs.Widget.prototype.selectChoice = function(choice) {
    // Get the value for this choice.
    var value = this.getValue(choice);

    if (!value) {
        if (window.console) console.log('yourlabs.Widget.getValue failed');
        return;
    }

    this.freeDeck();
    this.addToDeck(choice, value);
    this.addToSelect(choice, value);
    
    var index = $(':input:visible').index(this.input);
    this.resetDisplay();

    if (this.input.is(':visible')) {
        this.input.focus();
    } else {
        var next = $(':input:visible:eq('+ index +')');
        next.focus();
    }

    if (this.clearInputOnSelectChoice === '1')
        this.input.val('');
}

// Unselect a value if the maximum number of selected values has been
// reached.
yourlabs.Widget.prototype.freeDeck = function() {
    var slots = this.maximumValues - this.deck.children().length;

    if (this.maximumValues && slots < 1) {
        // We'll remove the first choice which is supposed to be the oldest
        var choice = $(this.deck.children()[0]);

        this.deselectChoice(choice);
    }
}

// Empty the search input and hide it if maximumValues has been reached.
yourlabs.Widget.prototype.resetDisplay = function() {
    var selected = this.select.find('option:selected').length;

    if (this.maximumValues && selected === this.maximumValues) {
        this.input.hide();
    } else {
        this.input.show();
    }

    this.deck.show();

    // Also fix the position if the autocomplete is shown.
    if (this.autocomplete.box.is(':visible')) this.autocomplete.fixPosition();
}

yourlabs.Widget.prototype.deckChoiceHtml = function(choice, value) {
    var deckChoice = choice.clone();

    this.addRemove(deckChoice);

    return deckChoice;
}

yourlabs.Widget.prototype.optionChoice = function(option) {
    var optionChoice = this.choiceTemplate.clone();
    
    var target = optionChoice.find('.append-option-html');

    if (target.length) {
        target.append(option.html());
    } else {
        optionChoice.html(option.html());
    }

    return optionChoice;
}

yourlabs.Widget.prototype.addRemove = function(choices) {
    var removeTemplate = this.widget.find('.remove:last')
        .clone().css('display', 'inline-block');

    var target = choices.find('.prepend-remove');

    if (target.length) {
        target.prepend(removeTemplate);
    } else {
        // Add the remove icon to each choice
        choices.prepend(removeTemplate);
    } 
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToDeck = function(choice, value) {
    var existing_choice = this.deck.find('[data-value="'+value+'"]');

    // Avoid duplicating choices in the deck.
    if (!existing_choice.length) {
        var deckChoice = this.deckChoiceHtml(choice);

        // In case getValue() actually **created** the value, for example
        // with a post request.
        deckChoice.attr('data-value', value);

        this.deck.append(deckChoice);
    }
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToSelect = function(choice, value) {
    var option = this.select.find('option[value="'+value+'"]');

    if (! option.length) {
        this.select.append(
            '<option selected="selected" value="'+ value +'"></option>');
        option = this.select.find('option[value="'+value+'"]');
    }

    option.attr('selected', 'selected');

    this.select.trigger('change');
    this.updateAutocompleteExclude();
}

// Called when the user clicks .remove in a deck choice.
yourlabs.Widget.prototype.deselectChoice = function(choice) {
    var value = this.getValue(choice);

    this.select.find('option[value="'+value+'"]').remove();
    this.select.trigger('change');

    choice.remove();

    if (this.deck.children().length === 0) {
        this.deck.hide();
    }

    this.updateAutocompleteExclude();
    this.resetDisplay();

    this.widget.trigger('widgetDeselectChoice', [choice, this]);
};

yourlabs.Widget.prototype.updateAutocompleteExclude = function() {
    var widget = this;
    var choices = this.deck.find(this.autocomplete.choiceSelector);

    this.autocomplete.data.exclude = $.map(choices, function(choice) { 
        return widget.getValue($(choice)); 
    });
}

yourlabs.Widget.prototype.initialize = function() {
    this.initializeAutocomplete();

    // Working around firefox tempering form values after reload
    var widget = this;
    this.deck.find(this.autocomplete.choiceSelector).each(function() {
        var value = widget.getValue($(this));
        var option = widget.select.find('option[value="'+value+'"]');
        if (!option.attr('selected')) option.attr('selected', true);
    });

    var choices = this.deck.find(
        this.input.yourlabsAutocomplete().choiceSelector);

    this.addRemove(choices);
    this.resetDisplay();

    this.bindSelectChoice();
    this.clearBoth()
}

// Add an empty div with clear:both after the widget's container.
// This is meant to support django-responsive-admin templates.
yourlabs.Widget.prototype.clearBoth = function() {
    this.widget.parent().append('<div style="clear: both"></div>');
}

// Destroy the widget. Takes a widget element because a cloned widget element
// will be dirty, ie. have wrong .input and .widget properties.
yourlabs.Widget.prototype.destroy = function(widget) {
    widget.find('input')
        .unbind('selectChoice')
        .yourlabsAutocomplete('destroy');
}

// Get or create or destroy a widget instance.
//
// On first call, yourlabsWidget() will instanciate a widget applying all
// passed overrides.
// 
// On later calls, yourlabsWidget() will return the previously created widget
// instance, which is stored in widget.data('widget').
//
// Calling yourlabsWidget('destroy') will destroy the widget. Useful if the
// element was blindly cloned with .clone(true) for example.
$.fn.yourlabsWidget = function(overrides) {
    overrides = overrides ? overrides : {};

    var widget = this.yourlabsRegistry('widget');

    if (overrides === 'destroy') {
        if (widget) {
            widget.destroy(this);
            this.removeData('widget');
        }
        return
    }

    if (widget === undefined) {
        // Instanciate the widget
        widget = new yourlabs.Widget(this);

        // Extend the instance with data-widget-* overrides
        for (var key in this.data()) {
            if (!key) continue;
            if (key.substr(0, 6) !== 'widget' || key === 'widget') continue;
            var newKey = key.replace('widget', '');
            newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
            widget[newKey] = this.data(key);
        }

        // Allow javascript object overrides
        widget = $.extend(widget, overrides);

        $(this).yourlabsRegistry('widget', widget);

        // Setup for usage
        widget.initialize();

        // Widget is ready
        widget.widget.attr('data-widget-ready', 1);
        widget.widget.trigger('widget-ready');
    }

    return widget;
}

$(document).ready(function() {
    $('body').on('initialize', '.autocomplete-light-widget[data-widget-bootstrap=normal]', function() {
        /*
        Only setup widgets which have data-widget-bootstrap=normal, if you want to
        initialize some Widgets with custom code, then set
        data-widget-boostrap=yourbootstrap or something like that.
        */
        $(this).yourlabsWidget();
    });

    // Call Widget.deselectChoice when .remove is clicked
    $('body').on('click', '.autocomplete-light-widget .deck .remove', function() {
        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        var selector = widget.input.yourlabsAutocomplete().choiceSelector;
        var choice = $(this).parents(selector);

        widget.deselectChoice(choice);
    });

    // Solid initialization, usage:
    //
    //
    //      $(document).bind('yourlabsWidgetReady', function() {
    //          $('.your.autocomplete-light-widget').on('initialize', function() {
    //              $(this).yourlabsWidget({
    //                  yourCustomArgs: // ...
    //              })
    //          });
    //      });
    $(document).trigger('yourlabsWidgetReady');

    $('.autocomplete-light-widget:not([id*="__prefix__"])').each(function() {
        $(this).trigger('initialize');
    });

    $(document).bind('widgetDeselectChoice', function(e, choice, widget) {
        /*
        Unset the .change-related link when an item is selected.

        For django 1.8 admin support.
        */
        var next = widget.widget.next();
        var template = next.attr('data-href-template');

        if (template && next.is('.change-related') && next.attr('href')) {
            next.removeAttr('href');
        }
    });

    $(document).bind('widgetSelectChoice', function(e, choice, widget) {
        /*
        Set the .change-related link when an item is selected.

        For django 1.8 admin support.
        */
        var next = widget.widget.next();
        var template = next.attr('data-href-template');

        if (template && next.is('.change-related')) {
            next.attr('href', template.replace('__fk__', widget.getValue(choice)));
        }
    });

    $(document).bind('DOMNodeInserted', function(e) {
        /*
        Support values added directly in the select via js (ie. choices created in
        modal or popup).

        For this, we listen to DOMNodeInserted and intercept insert of <option> nodes.
        
        The reason for that is that change is not triggered when options are
        added like this:

            $('select#id-dependencies').append(
                '<option value="9999" selected="selected">blabla</option>')
        */
        var widget;

        if ($(e.target).is('option')) { // added an option ?
            widget = $(e.target).parents('.autocomplete-light-widget');

            if (!widget.length) {
                return;
            }

            widget = widget.yourlabsWidget();
            var option = $(e.target);
            var value = option.attr('value');
            var choice = widget.deck.find('[data-value="'+value+'"]');

            if (!choice.length) {
                var deckChoice = widget.optionChoice(option);

                deckChoice.attr('data-value', value);

                widget.selectChoice(deckChoice);
            }
        } else { // added a widget ?
            var notReady = '.autocomplete-light-widget:not([data-widget-ready])'
            widget = $(e.target).find(notReady);

            if (!widget.length) {
                return;
            }

            // Ignore inserted autocomplete box elements.
            if (widget.is('.yourlabs-autocomplete')) {
                return;
            }

            // Ensure that the newly added widget is clean, in case it was
            // cloned with data.
            widget.yourlabsWidget('destroy');
            widget.find('input').yourlabsAutocomplete('destroy');

            // added a widget: initialize the widget.
            widget.trigger('initialize');
        }
    });
    
    var ie = yourlabs.getInternetExplorerVersion();
    if (ie !== -1 && ie < 9) {
        observe = [
            '.autocomplete-light-widget:not([data-yourlabs-skip])',
            '.autocomplete-light-widget option:not([data-yourlabs-skip])'
        ].join();
        $(observe).attr('data-yourlabs-skip', 1);

        var ieDOMNodeInserted = function() {
            // http://msdn.microsoft.com/en-us/library/ms536957
            $(observe).each(function() {
                $(document).trigger(jQuery.Event('DOMNodeInserted', {target: $(this)}));
                $(this).attr('data-yourlabs-skip', 1);
            });

            setTimeout(ieDOMNodeInserted, 500);
        }
        setTimeout(ieDOMNodeInserted, 500);
    }

});
