photomix/app/assets/javascripts/tag/tag.js

268 lines
9.8 KiB
JavaScript

/*
@author: remy sharp / http://remysharp.com
@url: http://remysharp.com/2007/12/28/jquery-tag-suggestion/
@usage: setGlobalTags(['javascript', 'jquery', 'java', 'json']); // applied tags to be used for all implementations
$('input.tags').tagSuggest(options);
The selector is the element that the user enters their tag list
@params:
matchClass - class applied to the suggestions, defaults to 'tagMatches'
tagContainer - the type of element uses to contain the suggestions, defaults to 'span'
tagWrap - the type of element the suggestions a wrapped in, defaults to 'span'
sort - boolean to force the sorted order of suggestions, defaults to false
url - optional url to get suggestions if setGlobalTags isn't used. Must return array of suggested tags
tags - optional array of tags specific to this instance of element matches
delay - optional sets the delay between keyup and the request - can help throttle ajax requests, defaults to zero delay
separator - optional separator string, defaults to ' ' (Brian J. Cardiff)
@license: Creative Commons License - ShareAlike http://creativecommons.org/licenses/by-sa/3.0/
@version: 1.4
@changes: fixed filtering to ajax hits
*/
(function ($) {
var globalTags = [];
// creates a public function within our private code.
// tags can either be an array of strings OR
// array of objects containing a 'tag' attribute
window.setGlobalTags = function(tags /* array */) {
globalTags = getTags(tags);
};
function getTags(tags) {
var tag, i, goodTags = [];
for (i = 0; i < tags.length; i++) {
tag = tags[i];
if (typeof tags[i] == 'object') {
tag = tags[i].tag;
}
goodTags.push(tag.toLowerCase());
}
return goodTags;
}
$.fn.tagSuggest = function (options) {
var defaults = {
'matchClass' : 'tagMatches',
'tagContainer' : 'span',
'tagWrap' : 'span',
'sort' : true,
'tags' : null,
'url' : null,
'delay' : 0,
'separator' : ' '
};
var i, tag, userTags = [], settings = $.extend({}, defaults, options);
if (settings.tags) {
userTags = getTags(settings.tags);
} else {
userTags = globalTags;
}
return this.each(function () {
var tagsElm = $(this);
var elm = this;
var matches, fromTab = false;
var suggestionsShow = false;
var workingTags = [];
var currentTag = {"position": 0, tag: ""};
var tagMatches = document.createElement(settings.tagContainer);
function showSuggestionsDelayed(el, key) {
if (settings.delay) {
if (elm.timer) clearTimeout(elm.timer);
elm.timer = setTimeout(function () {
showSuggestions(el, key);
}, settings.delay);
} else {
showSuggestions(el, key);
}
}
function showSuggestions(el, key) {
workingTags = el.value.split(settings.separator);
matches = [];
var i, html = '', chosenTags = {}, tagSelected = false;
// we're looking to complete the tag on currentTag.position (to start with)
currentTag = { position: currentTags.length-1, tag: '' };
for (i = 0; i < currentTags.length && i < workingTags.length; i++) {
if (!tagSelected &&
currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
currentTag = { position: i, tag: workingTags[i].toLowerCase() };
tagSelected = true;
}
// lookup for filtering out chosen tags
chosenTags[currentTags[i].toLowerCase()] = true;
}
if (currentTag.tag) {
// collect potential tags
if (settings.url) {
$.ajax({
'url' : settings.url,
'dataType' : 'json',
'data' : { 'tag' : currentTag.tag },
'async' : false, // wait until this is ajax hit is complete before continue
'success' : function (m) {
matches = m;
}
});
} else {
for (i = 0; i < userTags.length; i++) {
if (userTags[i].indexOf(currentTag.tag) === 0) {
matches.push(userTags[i]);
}
}
}
matches = $.grep(matches, function (v, i) {
return !chosenTags[v.toLowerCase()];
});
if (settings.sort) {
matches = matches.sort();
}
for (i = 0; i < matches.length; i++) {
html += '<' + settings.tagWrap + ' class="_tag_suggestion">' + matches[i] + '</' + settings.tagWrap + '>';
}
tagMatches.html(html);
suggestionsShow = !!(matches.length);
} else {
hideSuggestions();
}
}
function hideSuggestions() {
tagMatches.empty();
matches = [];
suggestionsShow = false;
}
function setSelection() {
var v = tagsElm.val();
// tweak for hintted elements
// http://remysharp.com/2007/01/25/jquery-tutorial-text-box-hints/
if (v == tagsElm.attr('title') && tagsElm.is('.hint')) v = '';
currentTags = v.split(settings.separator);
hideSuggestions();
}
function chooseTag(tag) {
var i, index;
for (i = 0; i < currentTags.length; i++) {
if (currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
index = i;
break;
}
}
if (index == workingTags.length - 1) tag = tag + settings.separator;
workingTags[i] = tag;
tagsElm.val(workingTags.join(settings.separator));
tagsElm.blur().focus();
setSelection();
}
function handleKeys(ev) {
fromTab = false;
var type = ev.type;
var resetSelection = false;
switch (ev.keyCode) {
case 37: // ignore cases (arrow keys)
case 38:
case 39:
case 40: {
hideSuggestions();
return true;
}
case 224:
case 17:
case 16:
case 18: {
return true;
}
case 8: {
// delete - hide selections if we're empty
if (this.value == '') {
hideSuggestions();
setSelection();
return true;
} else {
type = 'keyup'; // allow drop through
resetSelection = true;
showSuggestionsDelayed(this);
}
break;
}
case 9: // return and tab
case 13: {
if (suggestionsShow) {
// complete
chooseTag(matches[0]);
fromTab = true;
return false;
} else {
return true;
}
}
case 27: {
hideSuggestions();
setSelection();
return true;
}
case 32: {
setSelection();
return true;
}
}
if (type == 'keyup') {
switch (ev.charCode) {
case 9:
case 13: {
return true;
}
}
if (resetSelection) {
setSelection();
}
showSuggestionsDelayed(this, ev.charCode);
}
}
tagsElm.after(tagMatches).keypress(handleKeys).keyup(handleKeys).blur(function () {
if (fromTab == true || suggestionsShow) { // tweak to support tab selection for Opera & IE
fromTab = false;
tagsElm.focus();
}
});
// replace with jQuery version
tagMatches = $(tagMatches).click(function (ev) {
if (ev.target.nodeName == settings.tagWrap.toUpperCase() && $(ev.target).is('._tag_suggestion')) {
chooseTag(ev.target.innerHTML);
}
}).addClass(settings.matchClass);
// initialise
setSelection();
});
};
})(jQuery);