275 lines
8.5 KiB
JavaScript
275 lines
8.5 KiB
JavaScript
|
/*
|
|||
|
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) {
|
|||
|
if (window.getSelection) {
|
|||
|
var r = window.getSelection(); //IE11
|
|||
|
} else {
|
|||
|
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 $.trim(choice.html().replace(/(<([^>]+)>)/ig,""));
|
|||
|
}
|
|||
|
|
|||
|
// 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') {
|
|||
|
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');
|
|||
|
});
|
|||
|
})
|