text_widget.js

#

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(){
#

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) {
        var 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();
    var word = '';
#

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 choice.html();
}
#

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 overrides = overrides ? overrides : {};

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

    if (this.data('widget') == undefined) {
#

Instanciate the widget

        var 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') {
                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-bootstrap=normal]', function() {
        /*
        Only setup autocompletes on inputs which have data-bootstrap=normal,
        if you want to initialize some autocompletes with custom code, then set
        data-boostrap=yourbootstrap or something like that.
#

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

// Solid initialization, usage::
//
//      $(document).bind('yourlabsTextWidgetReady', function() {
//          $('body').on('initialize', 'input[data-bootstrap=normal]', 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'); }); })