(function($) {
function _arrayToVal( array, ele ) {
var tags_array = [];
$.each( array, function(key) {
tags_array.push( key );
});
ele.val( tags_array.join(",") );
ele.trigger("change");
}
// this doesn't act on the autocompletewithunknown widget. Instead, it's called on our input field with autocompletion
function _handleComplete(e, ui, $oldElement) {
var self = $(this).data("autocompletewithunknown");
var ele = self.element;
var curtagslist = self.cachemap[self.currentCache];
var items = ui.item.value.toLowerCase();
$.each( items.split(","), function() {
var tag = $.trim(this);
// check if the tag is empty, or if we had previously used it
if ( tag == "" ) return;
// TODO: don't put it in if the word exceeds the maximum length of a tag
if ( ! self.tagslist[tag] ) {
self.tagslist[tag] = true;
var tokentype = curtagslist && curtagslist[tag] ? self.options.curTokenClass : self.options.newTokenClass;
var $text = $("")
.addClass(self.options.tokenTextClass)
.text(tag);
var $a = $("").addClass(self.options.tokenRemoveClass).html("" +
"Remove " + tag + "");
var $li = $("
").addClass(self.options.tokenClass + " " + tokentype)
.append($text).append($a).append(" ")
.attr( "title", tokentype == self.options.curTokenClass ? "tag: " + tag : "new tag: " + tag);
if ( $oldElement )
$oldElement.replaceWith($li);
else
$li.appendTo(self.uiAutocompletelist);
}
});
$("#"+self.options.id+"_count_label").text("characters per tag:");
$(this).val("");
if ( self.options.grow ) {
// shrink
$(this).height((parseInt($(this).css('lineHeight').replace(/px$/,''), 10)||20) + 'px');
if ( self.options.maxlength )
$("#"+self.options.id+"_count").text(self.options.maxlength);
}
_arrayToVal(self.tagslist, ele)
if ( $.browser.opera ) {
var keyCode = $.ui.keyCode;
if( e.which == keyCode.ENTER || e.which == keyCode.TAB )
self.justCompleted = true;
}
// this prevents the default behavior of having the
// form field filled with the autocompleted text.
// Target the event type, to avoid issues with selecting using TAB
if ( e.type == "autocompleteselect" ) {
e.preventDefault();
return false;
}
}
$.widget("ui.autocompletewithunknown", {
options: {
id: null,
tokenClass: "token",
tokenTextClass: "token-text",
tokenRemoveClass: "token-remove",
newTokenClass: "new",
curTokenClass: "autocomplete",
numMatches: 20,
populateSource: null, // initial location to populate from
populateId: "", // initial identifier for the list to be cached
grow: false // whether to automatically grow the autocomplete textarea or not
},
_filterTags: function( array, term ) {
var self = this;
var startsWithTerm = [];
term = $.trim(term.toLowerCase());
var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term) );
var filtered = $.grep( array, function(value) {
var val = value.label || value.value || value;
if ( !self.tagslist[val] && matcher.test( val ) ) {
if ( val.indexOf(term) == 0 ) {
// ugly, but we'd like to handle terms that start with
// in a different manner
startsWithTerm.push({ value: val,
label: val.replace(term, ""+term+"")});
return false;
}
return true;
}
});
// second step, because we first need to find out how many
// total start with the term, and so need to be in the list
// those that only contain the term fill in any remaining slots
var responseArray = startsWithTerm.slice(0, self.options.numMatches);
$.each(filtered, function(index, value) {
if ( responseArray.length >= self.options.numMatches )
return false;
responseArray.push({ value: value,
label: value.replace(term, ""+term+"")});
})
return responseArray;
},
_create: function() {
var self = this;
if (!self.options.id)
self.options.id = "autocomplete_"+self.element.attr("id");
self.uiAutocomplete = self.element.wrap("").parent().attr("id", self.options.id).addClass(self.element.attr("class"));
self.uiAutocompletelist = $("
").appendTo(self.uiAutocomplete).attr( "aria-live", "assertive" );
// this is just frontend; will use JS to transfer contents to the original field (now hidden)
self.uiAutocompleteInput = $("")
.appendTo(self.uiAutocomplete)
.data("autocompletewithunknown", self)
.focus(function(e) {
$(this).closest(".autocomplete-container").addClass("focus");
})
.blur(function(e) {
$(this).closest(".autocomplete-container").removeClass("focus");
});
if ( self.options.grow ) {
self.uiAutocomplete.closest(".row").parent().find(".tagselector-controls")
.append("
characters per tag:50
");
$(self.uiAutocompleteInput).vertigro(self.options.maxlength, self.options.id + "_count");
}
self.element.hide();
self.tagslist = {};
self.cache = {};
self.cachemap = {};
if ( self.options.populateSource )
self.populate(self.options.populateSource, self.options.populateId);
self.uiAutocompleteInput.autocomplete({
source: function (request, response) {
if ( self.cache[self.currentCache] != null )
return response( self._filterTags( self.cache[self.currentCache], request.term ) );
},
autoFocus: true,
select: _handleComplete
}).bind("keydown.autocompleteselect", function( event ) {
var keyCode = $.ui.keyCode;
var $input = $(this);
$("#"+self.options.id+"_count_label").text("characters left:");
switch( event.which ) {
case keyCode.ENTER:
_handleComplete.apply( $input,
[event, { item: { value: $input.val() } } ]);
self.justCompleted = true;
$input.autocomplete("close");
event.preventDefault();
return;
case keyCode.TAB:
if ($input.val()) {
_handleComplete.apply( $input,
[event, { item: { value: $input.val() } } ]);
self.justCompleted = true;
event.preventDefault();
}
return;
case keyCode.BACKSPACE:
if( ! $input.val() ) {
$("."+self.options.tokenRemoveClass + ":last", self.uiAutocomplete).focus();
event.preventDefault();
}
return;
}
}).bind("keyup.autocompleteselect", function( event ) {
var $input = $(this);
if ( $input.val().indexOf(",") > -1 )
_handleComplete.apply( $input, [event, { item: { value: $input.val() } } ]);
}).change(function(event){
// if we have the menu open, let that handle the autocomplete
var $menu = $(this).data("ui-autocomplete").menu;
if ( $menu.element.is(":visible") ) return;
_handleComplete.apply( $(this), [event, { item: { value: $(event.currentTarget).val() } } ]);
// workaround for autocompleting with TAB in opera
if ( $.browser.opera && self.justCompleted ) {
$(this).focus();
self.justCompleted = false;
}
})
.data("ui-autocomplete")._renderItem = function( ul, item ) {
return $( "" )
.data( "ui-autocomplete-item", item )
.append( "" + item.label + "" )
.appendTo( ul );
};
// so other things can reinitialize the widget with their own text
$(self.element).bind("autocomplete_inittext", function( event, new_text ) {
self.tagslist = {};
self.uiAutocompletelist.empty();
_handleComplete.apply( self.uiAutocompleteInput, [ event, { item: { value: new_text } } ] );
});
$(self.element).trigger("autocomplete_inittext", self.element.val());
// replace one text
$(self.element).bind("autocomplete_edittext", function ( event, $element, new_text ) {
_handleComplete.apply( self.uiAutocompleteInput, [ event, { item: { value: new_text } }, $element ] );
});
$(".autocomplete-container").click(function() {
self.uiAutocompleteInput.focus();
});
$("span."+self.options.tokenTextClass, self.uiAutocomplete.get(0))
.live("click", function(event) {
delete self.tagslist[$(this).text()];
_arrayToVal(self.tagslist, self.element);
var $input = $("")
.addClass(self.options.tokenTextClass)
.val($(this).text())
.width($(this).width()+5);
$(this).replaceWith($input);
$input.focus();
event.stopPropagation();
});
$("input."+self.options.tokenTextClass,self.uiAutocomplete.get(0))
.live("blur", function(event) {
$(self.element).trigger("autocomplete_edittext", [ $(this).closest("li"), $(this).val() ] );
});
$("."+self.options.tokenRemoveClass, self.uiAutocomplete.get(0)).live("click", function(e) {
var $token = $(this).closest("."+self.options.tokenClass);
delete self.tagslist[$token.children("."+self.options.tokenTextClass).text()];
_arrayToVal(self.tagslist, self.element);
$token.fadeOut(function() {$(this).remove()});
e.preventDefault();
e.stopPropagation();
}).live("focus", function(event) {
$(this).parent().addClass("focus");
}).live("blur", function(event) {
$(this).parent().removeClass("focus");
}).live("keydown", function(event) {
if ( event.which == $.ui.keyCode.BACKSPACE ) {
event.preventDefault();
var $prevToken = $(this).closest("."+self.options.tokenClass).prev("."+self.options.tokenClass);
$(this).click();
if ($prevToken.length == 1)
$prevToken.find("."+self.options.tokenRemoveClass).focus();
else
self.uiAutocompleteInput.focus();
} else if (event.which != $.ui.keyCode.TAB) {
$(this).siblings("."+self.options.tokenTextClass).click();
}
});
// workaround for autocompleting with ENTER in opera
self.justCompleted = false;
$.browser.opera && $(self.element.get(0).form).bind("submit.autocomplete", function(e) {
// this tries to make sure that we don't try to validate crossposting, if we only hit enter
// to autocomplete. Workaround for opera.
// Sort of like a lock, to mark which handler last prevented the form submission.
// TODO: refactor this out into something that we're sure works. We are at the mercy
// of the way that Opera and other browsers order the handlers.
if ( self.element.data("preventedby") == self.options.id)
self.element.data("preventedby", null)
if( self.justCompleted ) {
if ( ! self.element.data("preventedby") )
self.element.data("preventedby", self.options.id);
self.justCompleted = false;
return false;
}
});
},
// store this in an array, and in a hash, so that we can easily look up presence of tags
_cacheData: function(array, key) {
this.currentCache=key;
this.cache[this.currentCache] = array;
this.cachemap[this.currentCache] = {};
for( var i in array ) {
this.cachemap[this.currentCache][array[i]] = true;
}
},
tagstatus: function(key) {
var self = this;
if ( !self.cachemap[key] )
return;
// recheck tags status in case tags list loaded slowly
// or we switched comms
$("."+self.options.tokenClass, self.uiAutocomplete).each(function() {
var exists = self.cachemap[key][$(this).find("."+self.options.tokenTextClass).text()];
$(this).toggleClass(self.options.newTokenClass, !exists).toggleClass(self.options.curTokenClass, exists);
});
},
populate: function(source, id) {
var self = this;
// Source is a URL endpoint we need to fetch
if (typeof source === 'string' || source instanceof String) {
$.getJSON(source, function(data) {
if ( !data ) return;
self._cacheData(data.tags, id);
self.tagstatus(id);
});
} else if (Array.isArray(source)) {
// Source is a static array, just store it.
self._cacheData(source, id);
self.tagstatus(id);
}
},
clear: function () {
var self = this;
self._cacheData([], "");
self.tagstatus("");
}
});
})(jQuery);