You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openNG/public_html/static/js/jquery-autocomplete.js

342 lines
9.5 KiB
JavaScript

/* This is a workaround for the issue with the autocompleter where an input blur is called before
* a click event on an autocompleter item is processed. Normally the list would disappear (as a
* result of the blur) before the entry was selected, thereby making it impossible to use the mouse
* in an autocomplete list. This works around it by preventing a blur disappear when an entry is in
* the process of being clicked. Reference: http://stackoverflow.com/a/18873685/1332715
* Tested and working in Opera 12.15.1748, Chrome 30.0.1599.66, Firefox 24.0, Safari 6.0.5.
* Reported not working in Chrome 31.0.1650.16 beta. */
var autocompleter_clicking = false;
function AutoCompleter(type) {
this.type = type;
this.template = $(".autocompleter-template[data-template=" + type + "]");
}
AutoCompleter.prototype.spawn = function(source) {
var instance = new AutoCompleterInstance(this.template.clone().removeClass("autocompleter-template").addClass("autocompleter-" + this.type).appendTo("body"), source);
return instance;
}
function AutoCompleterInstance(element, source) {
this.element = element;
this.current_selection = 0;
this.source = source;
}
AutoCompleterInstance.prototype.attachBelow = function(element) {
var left = element.offset().left;
var top = element.offset().top + element.outerHeight();
this.target = element;
this.element.css({left: left, top: top, display: "block"}).show();
this.show();
$(element).data("attached-autocomplete", this);
this.element.data("autocomplete-object", this);
this.element.disableSelection();
}
AutoCompleterInstance.prototype.remove = function() {
this.unhookKeyEvents(this.target);
this.unhookMouseEvents(this.target);
this.hide();
this.target.data("attached-autocomplete", "");
this.element.remove();
}
AutoCompleterInstance.prototype.hookKeyEvents = function(element) {
element.on("keyup.autocomplete", this._handleKeyUp.bind(this));
element.on("keydown.autocomplete", this._handleKeyDown.bind(this));
element.on("input.autocomplete", this._handleInput.bind(this));
}
AutoCompleterInstance.prototype.unhookKeyEvents = function(element) {
element.off("keyup.autocomplete");
element.off("keydown.autocomplete");
element.off("input.autocomplete");
}
AutoCompleterInstance.prototype.hookMouseEvents = function(element) {
this.element.on("mouseover.autocomplete", ".entry", this._handleMouseOver);
this.element.on("mouseup.autocomplete", ".entry", this._handleMouseClick);
/* Workaround for blur problem */
this.element.on("mousedown.autocomplete", ".entry", function(){ autocompleter_clicking = true; });
this.element.on("mouseup.autocomplete", ".entry", function(){ autocompleter_clicking = false; });
}
AutoCompleterInstance.prototype.unhookMouseEvents = function(element) {
this.element.off("mouseover.autocomplete", ".entry");
this.element.on("mousedown.autocomplete", ".entry");
this.element.on("mouseup.autocomplete", ".entry");
}
AutoCompleterInstance.prototype._handleKeyUp = function(event) {
switch(event.keyCode)
{
case 9: // Tab
/* This is now handled in the keydown event, to be able to
* implement passthrough behaviour when no autocompletion
* occurs... */
break;
case 13: // Enter/Return
/* We need slightly different behaviour for the Enter key;
* even if no autocompletion has happened, we still want to
* block potential accidental form submits. */
if(this.visible == true && this.total_items > 0)
{
this._selectCurrent();
event.stopPropagation();
event.preventDefault();
}
break;
}
}
AutoCompleterInstance.prototype._handleKeyDown = function(event) {
switch(event.keyCode)
{
case 9: // Tab
if(this.visible == true && this.total_items > 0)
{
if(this._selectCurrent())
{
event.stopPropagation();
event.preventDefault();
}
}
break;
case 13: // Enter/Return
/* We don't want this to do anything. */
if(this.visible == true && this.total_items > 0)
{
event.stopPropagation();
event.preventDefault();
}
break;
case 38: // Arrow Up
this._movePrevious();
event.stopPropagation();
event.preventDefault();
break;
case 40: // Arrow Down
this._moveNext();
event.stopPropagation();
event.preventDefault();
break;
case 27: // Escape
this.remove();
break;
}
}
AutoCompleterInstance.prototype._handleInput = function(event) {
clearTimeout(this.update_timer);
this.update_timer = setTimeout(this._updateItems.bind(this), 350);
}
AutoCompleterInstance.prototype._handleMouseOver = function(event) {
var selected = $(this).data("position");
var autocompleter = $(this).closest(".autocompleter").data("autocomplete-object");
autocompleter.current_selection = selected;
autocompleter._updateSelection();
}
AutoCompleterInstance.prototype._handleMouseClick = function(event) {
var autocompleter = $(this).closest(".autocompleter").data("autocomplete-object");
/* We ignore the actual represented entry; we just want to treat the currently highlighted entry
* as the one the user wants to select. In certain cases this is helpful for smooth UX, as it allows
* changing the selection while the mouse button is held - this may occur when the user changes his
* mind at the last moment. */
autocompleter._selectCurrent();
}
AutoCompleterInstance.prototype._updateSelection = function() {
this.element.find(".entry").removeClass("selected").eq(this.current_selection).addClass("selected");
}
AutoCompleterInstance.prototype._movePrevious = function() {
/* We check validity afterwards to prevent race conditions (mouse vs. keyboard). */
this.current_selection -= 1;
if(this.current_selection < 0)
{
this.current_selection = 0;
}
this._updateSelection();
}
AutoCompleterInstance.prototype._moveNext = function() {
this.current_selection += 1;
if(this.current_selection > this.total_items - 1)
{
this.current_selection = this.total_items - 1;
}
this._updateSelection();
}
AutoCompleterInstance.prototype._selectCurrent = function() {
var item = this.source.getItem(this.current_selection);
/* We only want to autocomplete if the currently selected item still matches
* the original query; this is to deal with race conditions where the user
* has changed his query to no longer be accurate, but the new results have
* not yet been fetched. */
if(this.source.matchesItem(this.current_selection, this.target.val()))
{
if(typeof this.callback !== "undefined")
{
this.callback.call(this, item);
}
else
{
this.target.val(item.value);
}
var has_autocompleted = true;
}
else
{
var has_autocompleted = false;
}
this.remove();
return has_autocompleted;
}
AutoCompleterInstance.prototype._updateItems = function() {
var query = this.target.val();
if(query == "")
{
this.hide();
}
else
{
this.show();
}
this.element.find(".noresults, .results").hide();
this.element.find(".loading").show();
this.source.updateItems(query, this.continueUpdate.bind(this));
}
AutoCompleterInstance.prototype.continueUpdate = function() {
this.total_items = this.source.getItemCount();
if(this.total_items > 0)
{
this.element.find(".entry").slice(1).remove();
var base_element = this.element.find(".entry").eq(0);
var items = this.source.getAll();
for(i in items)
{
var item = items[i];
if(i == 0)
{
var current_element = base_element.addClass("selected").data("position", i);
}
else
{
var current_element = base_element.clone().appendTo(base_element.parent()).removeClass("selected").data("position", i);
}
current_element.find(".autocompleter-field").each(function(){
$(this).html(item[$(this).data("field")]);
});
}
this.element.find(".results").show();
this.element.find(".noresults").hide();
}
else
{
this.element.find(".results").hide();
this.element.find(".noresults").show();
}
this.element.find(".loading").hide();
}
AutoCompleterInstance.prototype.hide = function() {
this.visible = false;
this.element.hide();
$(this.target).css({"border-bottom-left-radius": "", "border-bottom-right-radius": ""});
}
AutoCompleterInstance.prototype.show = function() {
this.visible = true;
this.element.show();
$(this.target).css({"border-bottom-left-radius": 0, "border-bottom-right-radius": 0});
}
;(function($) {
$.fn.disableSelection = function() {
return this
.attr('unselectable', 'on')
.css('user-select', 'none')
.on('selectstart', false);
};
$.fn.enableSelection = function() {
return this
.attr('unselectable', 'off')
.css('user-select', 'text')
.off('selectstart');
};
$.fn.autoComplete = function(autocompleter, source, selector, callback) {
if(typeof selector === "string")
{
var persistent = true;
}
else
{
var persistent = false;
callback = selector; // Shift arguments
}
var event = function(){
if(!$(this).data("attached-autocomplete"))
{
var instance = autocompleter.spawn(new source($(this)));
instance.callback = callback;
instance.attachBelow($(this));
instance.hookKeyEvents($(this));
instance.hookMouseEvents($(this));
instance._updateItems();
$(this).attr("autocomplete", "off");
}
};
var removal_hook = function(){
var autocompleter = $(this).data("attached-autocomplete");
if(autocompleter_clicking == false && autocompleter)
{
autocompleter.remove();
}
}
if(persistent === true)
{
this.on("input.autocomplete_hook", selector, event);
this.on("blur.autocomplete_hook", selector, removal_hook);
}
else
{
this.on("input.autocomplete_hook", event);
this.on("blur.autocomplete_hook", removal_hook);
}
return this;
};
}(jQuery));