replacing jquery-ui by jquery-autocomplete

This commit is contained in:
Élie Bouttier 2014-09-06 00:51:27 -07:00
parent 7e3173c7c0
commit 6cc30ba08e
9 changed files with 839 additions and 1259 deletions

File diff suppressed because it is too large Load diff

View file

@ -146,14 +146,14 @@ class TestViews(TestCase):
user = User.objects.get(username='user1') user = User.objects.get(username='user1')
group = Group.objects.create(name='newgroup') group = Group.objects.create(name='newgroup')
with self.settings(GROUP_MANAGMENT=False): with self.settings(GROUP_MANAGMENT=False):
response = self.client.get(reverse('add-group-to-user', args=[user.id]) + '?term=new') response = self.client.get(reverse('add-group-to-user', args=[user.id]) + '?query=new')
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-group-to-user', args=[user.id])) response = self.client.get(reverse('add-group-to-user', args=[user.id]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-group-to-user', args=[user.id]) + '?term=new') response = self.client.get(reverse('add-group-to-user', args=[user.id]) + '?query=new')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
available = json.loads(response.content.decode('utf-8')) available = json.loads(response.content.decode('utf-8'))
name = available[0]['value'] name = available['suggestions'][0]['data']
response = self.client.post(reverse('add-group-to-user', args=[user.id]), { response = self.client.post(reverse('add-group-to-user', args=[user.id]), {
'group': name, 'group': name,
}) })
@ -179,10 +179,10 @@ class TestViews(TestCase):
team = Team.objects.create(name='newteam') team = Team.objects.create(name='newteam')
response = self.client.get(reverse('add-team-to-user', args=[user.id])) response = self.client.get(reverse('add-team-to-user', args=[user.id]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-team-to-user', args=[user.id]) + '?term=new') response = self.client.get(reverse('add-team-to-user', args=[user.id]) + '?query=new')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
available = json.loads(response.content.decode('utf-8')) available = json.loads(response.content.decode('utf-8'))
name = available[0]['value'] name = available['suggestions'][0]['data']
response = self.client.post(reverse('add-team-to-user', args=[user.id]), { response = self.client.post(reverse('add-team-to-user', args=[user.id]), {
'team': name, 'team': name,
}) })
@ -254,16 +254,16 @@ class TestViews(TestCase):
group = Group.objects.get(name='group1') group = Group.objects.get(name='group1')
user = User.objects.create(username='newuser') user = User.objects.create(username='newuser')
with self.settings(GROUP_MANAGMENT=False): with self.settings(GROUP_MANAGMENT=False):
response = self.client.get(reverse('add-user-to-group', args=[group.id]) + '?term=new') response = self.client.get(reverse('add-user-to-group', args=[group.id]) + '?query=new')
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
user = User.objects.get(pk=user.pk) user = User.objects.get(pk=user.pk)
self.assertFalse(group in user.groups.all()) self.assertFalse(group in user.groups.all())
response = self.client.get(reverse('add-user-to-group', args=[group.id])) response = self.client.get(reverse('add-user-to-group', args=[group.id]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-user-to-group', args=[group.id]) + '?term=new') response = self.client.get(reverse('add-user-to-group', args=[group.id]) + '?query=new')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
available = json.loads(response.content.decode('utf-8')) available = json.loads(response.content.decode('utf-8'))
name = available[0]['value'] name = available['suggestions'][0]['data']
response = self.client.post(reverse('add-user-to-group', args=[group.id]), { response = self.client.post(reverse('add-user-to-group', args=[group.id]), {
'user': name, 'user': name,
}) })
@ -336,10 +336,10 @@ class TestViews(TestCase):
user = User.objects.create(username='newuser') user = User.objects.create(username='newuser')
response = self.client.get(reverse('add-user-to-team', args=[team.id])) response = self.client.get(reverse('add-user-to-team', args=[team.id]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-user-to-team', args=[team.id]) + '?term=new') response = self.client.get(reverse('add-user-to-team', args=[team.id]) + '?query=new')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
available = json.loads(response.content.decode('utf-8')) available = json.loads(response.content.decode('utf-8'))
name = available[0]['value'] name = available['suggestions'][0]['data']
response = self.client.post(reverse('add-user-to-team', args=[team.id]), { response = self.client.post(reverse('add-user-to-team', args=[team.id]), {
'user': name, 'user': name,
}) })
@ -360,10 +360,10 @@ class TestViews(TestCase):
group = Group.objects.create(name='newgroup') group = Group.objects.create(name='newgroup')
response = self.client.get(reverse('add-group-to-team', args=[team.id])) response = self.client.get(reverse('add-group-to-team', args=[team.id]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('add-group-to-team', args=[team.id]) + '?term=new') response = self.client.get(reverse('add-group-to-team', args=[team.id]) + '?query=new')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
available = json.loads(response.content.decode('utf-8')) available = json.loads(response.content.decode('utf-8'))
name = available[0]['value'] name = available['suggestions'][0]['data']
response = self.client.post(reverse('add-group-to-team', args=[team.id]), { response = self.client.post(reverse('add-group-to-team', args=[team.id]), {
'group': name, 'group': name,
}) })

View file

@ -174,7 +174,7 @@ def user_add_group(request, user):
request.session['user-tab'] = 'group' request.session['user-tab'] = 'group'
return redirect('show-user', user.id) return redirect('show-user', user.id)
else: else:
term = request.GET.get('term') term = request.GET.get('query')
if not term: if not term:
raise Http404() raise Http404()
groups = Group.objects \ groups = Group.objects \
@ -183,10 +183,12 @@ def user_add_group(request, user):
response = [] response = []
for group in groups: for group in groups:
response += [{ response += [{
'label': group.name,
'value': group.name, 'value': group.name,
'data': group.name,
}] }]
return JsonResponse(response, safe=False) return JsonResponse({
'suggestions': response,
}, safe=False)
@project_perm_required('manage_accounts') @project_perm_required('manage_accounts')
@ -225,7 +227,7 @@ def user_add_team(request, user):
request.session['user-tab'] = 'team' request.session['user-tab'] = 'team'
return redirect('show-user', user.id) return redirect('show-user', user.id)
else: else:
term = request.GET.get('term') term = request.GET.get('query')
if not term: if not term:
raise Http404() raise Http404()
teams = Team.objects \ teams = Team.objects \
@ -234,10 +236,12 @@ def user_add_team(request, user):
response = [] response = []
for team in teams: for team in teams:
response += [{ response += [{
'label': team.name,
'value': team.name, 'value': team.name,
'data': team.name,
}] }]
return JsonResponse(response, safe=False) return JsonResponse({
'suggestions': response,
}, safe=False)
@project_perm_required('manage_accounts') @project_perm_required('manage_accounts')
@ -330,7 +334,7 @@ def group_add_user(request, group):
messages.error(request, 'User not found.') messages.error(request, 'User not found.')
return redirect('show-group', group.id) return redirect('show-group', group.id)
else: else:
term = request.GET.get('term') term = request.GET.get('query')
if not term: if not term:
raise Http404() raise Http404()
query = Q(username__icontains=term) \ query = Q(username__icontains=term) \
@ -340,10 +344,12 @@ def group_add_user(request, group):
response = [] response = []
for user in users: for user in users:
response += [{ response += [{
'label': user.username_and_fullname, 'value': user.username_and_fullname,
'value': user.username, 'data': user.username,
}] }]
return JsonResponse(response, safe=False) return JsonResponse({
'suggestions': response,
}, safe=False)
@project_perm_required('manage_accounts') @project_perm_required('manage_accounts')
@ -432,7 +438,7 @@ def team_add_user(request, team):
request.session['team-tab'] = 'user' request.session['team-tab'] = 'user'
return redirect('show-team', team.id) return redirect('show-team', team.id)
else: else:
term = request.GET.get('term') term = request.GET.get('query')
if not term: if not term:
raise Http404() raise Http404()
query = Q(username__icontains=term) \ query = Q(username__icontains=term) \
@ -445,10 +451,12 @@ def team_add_user(request, team):
response = [] response = []
for user in users: for user in users:
response += [{ response += [{
'label': user.username_and_fullname, 'value': user.username_and_fullname,
'value': user.username, 'data': user.username,
}] }]
return JsonResponse(response, safe=False) return JsonResponse({
'suggestions': response,
}, safe=False)
@project_perm_required('manage_accounts') @project_perm_required('manage_accounts')
@ -483,7 +491,7 @@ def team_add_group(request, team):
request.session['team-tab'] = 'group' request.session['team-tab'] = 'group'
return redirect('show-team', team.id) return redirect('show-team', team.id)
else: else:
term = request.GET.get('term') term = request.GET.get('query')
if not term: if not term:
raise Http404() raise Http404()
groups = Group.objects \ groups = Group.objects \
@ -492,10 +500,12 @@ def team_add_group(request, team):
response = [] response = []
for group in groups: for group in groups:
response += [{ response += [{
'label': group.name,
'value': group.name, 'value': group.name,
'data': group.name,
}] }]
return JsonResponse(response, safe=False) return JsonResponse({
'suggestions': response,
}, safe=False)
@project_perm_required('manage_accounts') @project_perm_required('manage_accounts')

View file

@ -0,0 +1,4 @@
.autocomplete-suggestions { border: 1px solid #999; background: #FFF; overflow: auto; }
.autocomplete-suggestion { padding: 2px 5px; white-space: nowrap; overflow: hidden; }
.autocomplete-selected { background: #F0F0F0; }
.autocomplete-suggestions strong { font-weight: normal; color: #3399FF; }

View file

@ -0,0 +1,735 @@
/**
* Ajax Autocomplete for jQuery, version 1.2.7
* (c) 2013 Tomas Kirda
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
*
*/
/*jslint browser: true, white: true, plusplus: true */
/*global define, window, document, jQuery */
// Expose plugin as an AMD module if AMD loader is present:
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
'use strict';
var
utils = (function () {
return {
escapeRegExChars: function (value) {
return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
createNode: function (html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
}
};
}()),
keys = {
ESC: 27,
TAB: 9,
RETURN: 13,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
};
function Autocomplete(el, options) {
var noop = function () { },
that = this,
defaults = {
autoSelectFirst: false,
appendTo: 'body',
serviceUrl: null,
lookup: null,
onSelect: null,
width: 'auto',
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
params: {},
formatResult: Autocomplete.formatResult,
delimiter: null,
zIndex: 9999,
type: 'GET',
noCache: false,
onSearchStart: noop,
onSearchComplete: noop,
containerClass: 'autocomplete-suggestions',
tabDisabled: false,
dataType: 'text',
currentRequest: null,
lookupFilter: function (suggestion, originalQuery, queryLowerCase) {
return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
},
paramName: 'query',
transformResult: function (response) {
return typeof response === 'string' ? $.parseJSON(response) : response;
}
};
// Shared variables:
that.element = el;
that.el = $(el);
that.suggestions = [];
that.badQueries = [];
that.selectedIndex = -1;
that.currentValue = that.element.value;
that.intervalId = 0;
that.cachedResponse = [];
that.onChangeInterval = null;
that.onChange = null;
that.isLocal = false;
that.suggestionsContainer = null;
that.options = $.extend({}, defaults, options);
that.classes = {
selected: 'autocomplete-selected',
suggestion: 'autocomplete-suggestion'
};
that.hint = null;
that.hintValue = '';
that.selection = null;
// Initialize and set options:
that.initialize();
that.setOptions(options);
}
Autocomplete.utils = utils;
$.Autocomplete = Autocomplete;
Autocomplete.formatResult = function (suggestion, currentValue) {
var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
};
Autocomplete.prototype = {
killerFn: null,
initialize: function () {
var that = this,
suggestionSelector = '.' + that.classes.suggestion,
selected = that.classes.selected,
options = that.options,
container;
// Remove autocomplete attribute to prevent native suggestions:
that.element.setAttribute('autocomplete', 'off');
that.killerFn = function (e) {
if ($(e.target).closest('.' + that.options.containerClass).length === 0) {
that.killSuggestions();
that.disableKillerFn();
}
};
that.suggestionsContainer = Autocomplete.utils.createNode('<div class="' + options.containerClass + '" style="position: absolute; display: none;"></div>');
container = $(that.suggestionsContainer);
container.appendTo(options.appendTo);
// Only set width if it was provided:
if (options.width !== 'auto') {
container.width(options.width);
}
// Listen for mouse over event on suggestions list:
container.on('mouseover.autocomplete', suggestionSelector, function () {
that.activate($(this).data('index'));
});
// Deselect active element when mouse leaves suggestions container:
container.on('mouseout.autocomplete', function () {
that.selectedIndex = -1;
container.children('.' + selected).removeClass(selected);
});
// Listen for click event on suggestions list:
container.on('click.autocomplete', suggestionSelector, function () {
that.select($(this).data('index'));
});
that.fixPosition();
that.fixPositionCapture = function () {
if (that.visible) {
that.fixPosition();
}
};
$(window).on('resize', that.fixPositionCapture);
that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
that.el.on('blur.autocomplete', function () { that.onBlur(); });
that.el.on('focus.autocomplete', function () { that.fixPosition(); });
that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
},
onBlur: function () {
this.enableKillerFn();
},
setOptions: function (suppliedOptions) {
var that = this,
options = that.options;
$.extend(options, suppliedOptions);
that.isLocal = $.isArray(options.lookup);
if (that.isLocal) {
options.lookup = that.verifySuggestionsFormat(options.lookup);
}
// Adjust height, width and z-index:
$(that.suggestionsContainer).css({
'max-height': options.maxHeight + 'px',
'width': options.width + 'px',
'z-index': options.zIndex
});
},
clearCache: function () {
this.cachedResponse = [];
this.badQueries = [];
},
clear: function () {
this.clearCache();
this.currentValue = '';
this.suggestions = [];
},
disable: function () {
this.disabled = true;
},
enable: function () {
this.disabled = false;
},
fixPosition: function () {
var that = this,
offset;
// Don't adjsut position if custom container has been specified:
if (that.options.appendTo !== 'body') {
return;
}
offset = that.el.offset();
$(that.suggestionsContainer).css({
top: (offset.top + that.el.outerHeight()) + 'px',
left: offset.left + 'px'
});
},
enableKillerFn: function () {
var that = this;
$(document).on('click.autocomplete', that.killerFn);
},
disableKillerFn: function () {
var that = this;
$(document).off('click.autocomplete', that.killerFn);
},
killSuggestions: function () {
var that = this;
that.stopKillSuggestions();
that.intervalId = window.setInterval(function () {
that.hide();
that.stopKillSuggestions();
}, 300);
},
stopKillSuggestions: function () {
window.clearInterval(this.intervalId);
},
isCursorAtEnd: function () {
var that = this,
valLength = that.el.val().length,
selectionStart = that.element.selectionStart,
range;
if (typeof selectionStart === 'number') {
return selectionStart === valLength;
}
if (document.selection) {
range = document.selection.createRange();
range.moveStart('character', -valLength);
return valLength === range.text.length;
}
return true;
},
onKeyPress: function (e) {
var that = this;
// If suggestions are hidden and user presses arrow down, display suggestions:
if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
that.suggest();
return;
}
if (that.disabled || !that.visible) {
return;
}
switch (e.which) {
case keys.ESC:
that.el.val(that.currentValue);
that.hide();
break;
case keys.RIGHT:
if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
that.selectHint();
break;
}
return;
case keys.TAB:
if (that.hint && that.options.onHint) {
that.selectHint();
return;
}
// Fall through to RETURN
case keys.RETURN:
if (that.selectedIndex === -1) {
that.hide();
return;
}
that.select(that.selectedIndex);
if (e.which === keys.TAB && that.options.tabDisabled === false) {
return;
}
break;
case keys.UP:
that.moveUp();
break;
case keys.DOWN:
that.moveDown();
break;
default:
return;
}
// Cancel event if function did not return:
e.stopImmediatePropagation();
e.preventDefault();
},
onKeyUp: function (e) {
var that = this;
if (that.disabled) {
return;
}
switch (e.which) {
case keys.UP:
case keys.DOWN:
return;
}
clearInterval(that.onChangeInterval);
if (that.currentValue !== that.el.val()) {
that.findBestHint();
if (that.options.deferRequestBy > 0) {
// Defer lookup in case when value changes very quickly:
that.onChangeInterval = setInterval(function () {
that.onValueChange();
}, that.options.deferRequestBy);
} else {
that.onValueChange();
}
}
},
onValueChange: function () {
var that = this,
q;
if (that.selection) {
that.selection = null;
(that.options.onInvalidateSelection || $.noop)();
}
clearInterval(that.onChangeInterval);
that.currentValue = that.el.val();
q = that.getQuery(that.currentValue);
that.selectedIndex = -1;
if (q.length < that.options.minChars) {
that.hide();
} else {
that.getSuggestions(q);
}
},
getQuery: function (value) {
var delimiter = this.options.delimiter,
parts;
if (!delimiter) {
return $.trim(value);
}
parts = value.split(delimiter);
return $.trim(parts[parts.length - 1]);
},
getSuggestionsLocal: function (query) {
var that = this,
queryLowerCase = query.toLowerCase(),
filter = that.options.lookupFilter;
return {
suggestions: $.grep(that.options.lookup, function (suggestion) {
return filter(suggestion, query, queryLowerCase);
})
};
},
getSuggestions: function (q) {
var response,
that = this,
options = that.options,
serviceUrl = options.serviceUrl;
response = that.isLocal ? that.getSuggestionsLocal(q) : that.cachedResponse[q];
if (response && $.isArray(response.suggestions)) {
that.suggestions = response.suggestions;
that.suggest();
} else if (!that.isBadQuery(q)) {
options.params[options.paramName] = q;
if (options.onSearchStart.call(that.element, options.params) === false) {
return;
}
if ($.isFunction(options.serviceUrl)) {
serviceUrl = options.serviceUrl.call(that.element, q);
}
if(this.currentRequest != null) {
this.currentRequest.abort();
}
this.currentRequest = $.ajax({
url: serviceUrl,
data: options.ignoreParams ? null : options.params,
type: options.type,
dataType: options.dataType
}).done(function (data) {
that.processResponse(data, q);
options.onSearchComplete.call(that.element, q);
});
}
},
isBadQuery: function (q) {
var badQueries = this.badQueries,
i = badQueries.length;
while (i--) {
if (q.indexOf(badQueries[i]) === 0) {
return true;
}
}
return false;
},
hide: function () {
var that = this;
that.visible = false;
that.selectedIndex = -1;
$(that.suggestionsContainer).hide();
that.signalHint(null);
},
suggest: function () {
if (this.suggestions.length === 0) {
this.hide();
return;
}
var that = this,
formatResult = that.options.formatResult,
value = that.getQuery(that.currentValue),
className = that.classes.suggestion,
classSelected = that.classes.selected,
container = $(that.suggestionsContainer),
html = '',
width;
// Build suggestions inner HTML:
$.each(that.suggestions, function (i, suggestion) {
html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>';
});
// If width is auto, adjust width before displaying suggestions,
// because if instance was created before input had width, it will be zero.
// Also it adjusts if input width has changed.
// -2px to account for suggestions border.
if (that.options.width === 'auto') {
width = that.el.outerWidth() - 2;
container.width(width > 0 ? width : 300);
}
container.html(html).show();
that.visible = true;
// Select first value by default:
if (that.options.autoSelectFirst) {
that.selectedIndex = 0;
container.children().first().addClass(classSelected);
}
that.findBestHint();
},
findBestHint: function () {
var that = this,
value = that.el.val().toLowerCase(),
bestMatch = null;
if (!value) {
return;
}
$.each(that.suggestions, function (i, suggestion) {
var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
if (foundMatch) {
bestMatch = suggestion;
}
return !foundMatch;
});
that.signalHint(bestMatch);
},
signalHint: function (suggestion) {
var hintValue = '',
that = this;
if (suggestion) {
hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
}
if (that.hintValue !== hintValue) {
that.hintValue = hintValue;
that.hint = suggestion;
(this.options.onHint || $.noop)(hintValue);
}
},
verifySuggestionsFormat: function (suggestions) {
// If suggestions is string array, convert them to supported format:
if (suggestions.length && typeof suggestions[0] === 'string') {
return $.map(suggestions, function (value) {
return { value: value, data: null };
});
}
return suggestions;
},
processResponse: function (response, originalQuery) {
var that = this,
options = that.options,
result = options.transformResult(response, originalQuery);
result.suggestions = that.verifySuggestionsFormat(result.suggestions);
// Cache results if cache is not disabled:
if (!options.noCache) {
that.cachedResponse[result[options.paramName]] = result;
if (result.suggestions.length === 0) {
that.badQueries.push(result[options.paramName]);
}
}
// Display suggestions only if returned query matches current value:
if (originalQuery === that.getQuery(that.currentValue)) {
that.suggestions = result.suggestions;
that.suggest();
}
},
activate: function (index) {
var that = this,
activeItem,
selected = that.classes.selected,
container = $(that.suggestionsContainer),
children = container.children();
container.children('.' + selected).removeClass(selected);
that.selectedIndex = index;
if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
activeItem = children.get(that.selectedIndex);
$(activeItem).addClass(selected);
return activeItem;
}
return null;
},
selectHint: function () {
var that = this,
i = $.inArray(that.hint, that.suggestions);
that.select(i);
},
select: function (i) {
var that = this;
that.hide();
that.onSelect(i);
},
moveUp: function () {
var that = this;
if (that.selectedIndex === -1) {
return;
}
if (that.selectedIndex === 0) {
$(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
that.selectedIndex = -1;
that.el.val(that.currentValue);
that.findBestHint();
return;
}
that.adjustScroll(that.selectedIndex - 1);
},
moveDown: function () {
var that = this;
if (that.selectedIndex === (that.suggestions.length - 1)) {
return;
}
that.adjustScroll(that.selectedIndex + 1);
},
adjustScroll: function (index) {
var that = this,
activeItem = that.activate(index),
offsetTop,
upperBound,
lowerBound,
heightDelta = 25;
if (!activeItem) {
return;
}
offsetTop = activeItem.offsetTop;
upperBound = $(that.suggestionsContainer).scrollTop();
lowerBound = upperBound + that.options.maxHeight - heightDelta;
if (offsetTop < upperBound) {
$(that.suggestionsContainer).scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
$(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
}
that.el.val(that.getValue(that.suggestions[index].value));
that.signalHint(null);
},
onSelect: function (index) {
var that = this,
onSelectCallback = that.options.onSelect,
suggestion = that.suggestions[index];
that.currentValue = that.getValue(suggestion.value);
that.el.val(that.currentValue);
that.signalHint(null);
that.suggestions = [];
that.selection = suggestion;
if ($.isFunction(onSelectCallback)) {
onSelectCallback.call(that.element, suggestion);
}
},
getValue: function (value) {
var that = this,
delimiter = that.options.delimiter,
currentValue,
parts;
if (!delimiter) {
return value;
}
currentValue = that.currentValue;
parts = currentValue.split(delimiter);
if (parts.length === 1) {
return value;
}
return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
},
dispose: function () {
var that = this;
that.el.off('.autocomplete').removeData('autocomplete');
that.disableKillerFn();
$(window).off('resize', that.fixPositionCapture);
$(that.suggestionsContainer).remove();
}
};
// Create chainable jQuery plugin:
$.fn.autocomplete = function (options, args) {
var dataKey = 'autocomplete';
// If function invoked without argument return
// instance of the first matched element:
if (arguments.length === 0) {
return this.first().data(dataKey);
}
return this.each(function () {
var inputElement = $(this),
instance = inputElement.data(dataKey);
if (typeof options === 'string') {
if (instance && typeof instance[options] === 'function') {
instance[options](args);
}
} else {
// If instance already exists, destroy it:
if (instance && instance.dispose) {
instance.dispose();
}
instance = new Autocomplete(this, options);
inputElement.data(dataKey, instance);
}
});
};
}));

25
static/js/jquery.autocomplete.min.js vendored Normal file
View file

@ -0,0 +1,25 @@
/**
* Ajax Autocomplete for jQuery, version 1.2.7
* (c) 2013 Tomas Kirda
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
*
*/
(function(e){"function"===typeof define&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){function g(a,b){var c=function(){},c={autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:g.formatResult,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:c,onSearchComplete:c,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",lookupFilter:function(a,b,c){return-1!==
a.value.toLowerCase().indexOf(c)},paramName:"query",transformResult:function(a){return"string"===typeof a?e.parseJSON(a):a}};this.element=a;this.el=e(a);this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.currentValue=this.element.value;this.intervalId=0;this.cachedResponse=[];this.onChange=this.onChangeInterval=null;this.isLocal=this.ignoreValueChange=!1;this.suggestionsContainer=null;this.options=e.extend({},c,b);this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"};
this.initialize();this.setOptions(b)}var h={extend:function(a,b){return e.extend(a,b)},createNode:function(a){var b=document.createElement("div");b.innerHTML=a;return b.firstChild}};g.utils=h;e.Autocomplete=g;g.formatResult=function(a,b){var c="("+b.replace(RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\)","g"),"\\$1")+")";return a.value.replace(RegExp(c,"gi"),"<strong>$1</strong>")};g.prototype={killerFn:null,initialize:function(){var a=this,b="."+a.classes.suggestion,c=a.classes.selected,
d=a.options,f;a.element.setAttribute("autocomplete","off");a.killerFn=function(b){0===e(b.target).closest("."+a.options.containerClass).length&&(a.killSuggestions(),a.disableKillerFn())};if(!d.width||"auto"===d.width)d.width=a.el.outerWidth();a.suggestionsContainer=g.utils.createNode('<div class="'+d.containerClass+'" style="position: absolute; display: none;"></div>');f=e(a.suggestionsContainer);f.appendTo(d.appendTo).width(d.width);f.on("mouseover.autocomplete",b,function(){a.activate(e(this).data("index"))});
f.on("mouseout.autocomplete",function(){a.selectedIndex=-1;f.children("."+c).removeClass(c)});f.on("click.autocomplete",b,function(){a.select(e(this).data("index"),!1)});a.fixPosition();if(window.opera)a.el.on("keypress.autocomplete",function(b){a.onKeyPress(b)});else a.el.on("keydown.autocomplete",function(b){a.onKeyPress(b)});a.el.on("keyup.autocomplete",function(b){a.onKeyUp(b)});a.el.on("blur.autocomplete",function(){a.onBlur()});a.el.on("focus.autocomplete",function(){a.fixPosition()})},onBlur:function(){this.enableKillerFn()},
setOptions:function(a){var b=this.options;h.extend(b,a);if(this.isLocal=e.isArray(b.lookup))b.lookup=this.verifySuggestionsFormat(b.lookup);e(this.suggestionsContainer).css({"max-height":b.maxHeight+"px",width:b.width+"px","z-index":b.zIndex})},clearCache:function(){this.cachedResponse=[];this.badQueries=[]},clear:function(){this.clearCache();this.currentValue=null;this.suggestions=[]},disable:function(){this.disabled=!0},enable:function(){this.disabled=!1},fixPosition:function(){var a;"body"===this.options.appendTo&&
(a=this.el.offset(),e(this.suggestionsContainer).css({top:a.top+this.el.outerHeight()+"px",left:a.left+"px"}))},enableKillerFn:function(){e(document).on("click.autocomplete",this.killerFn)},disableKillerFn:function(){e(document).off("click.autocomplete",this.killerFn)},killSuggestions:function(){var a=this;a.stopKillSuggestions();a.intervalId=window.setInterval(function(){a.hide();a.stopKillSuggestions()},300)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},onKeyPress:function(a){if(!this.disabled&&
!this.visible&&40===a.keyCode&&this.currentValue)this.suggest();else if(!this.disabled&&this.visible){switch(a.keyCode){case 27:this.el.val(this.currentValue);this.hide();break;case 9:case 13:if(-1===this.selectedIndex){this.hide();return}this.select(this.selectedIndex,13===a.keyCode);if(9===a.keyCode&&!1===this.options.tabDisabled)return;break;case 38:this.moveUp();break;case 40:this.moveDown();break;default:return}a.stopImmediatePropagation();a.preventDefault()}},onKeyUp:function(a){var b=this;
if(!b.disabled){switch(a.keyCode){case 38:case 40:return}clearInterval(b.onChangeInterval);if(b.currentValue!==b.el.val())if(0<b.options.deferRequestBy)b.onChangeInterval=setInterval(function(){b.onValueChange()},b.options.deferRequestBy);else b.onValueChange()}},onValueChange:function(){var a;clearInterval(this.onChangeInterval);this.currentValue=this.element.value;a=this.getQuery(this.currentValue);this.selectedIndex=-1;this.ignoreValueChange?this.ignoreValueChange=!1:a.length<this.options.minChars?
this.hide():this.getSuggestions(a)},getQuery:function(a){var b=this.options.delimiter;if(!b)return e.trim(a);a=a.split(b);return e.trim(a[a.length-1])},getSuggestionsLocal:function(a){var b=a.toLowerCase(),c=this.options.lookupFilter;return{suggestions:e.grep(this.options.lookup,function(d){return c(d,a,b)})}},getSuggestions:function(a){var b,c=this,d=c.options,f=d.serviceUrl;(b=c.isLocal?c.getSuggestionsLocal(a):c.cachedResponse[a])&&e.isArray(b.suggestions)?(c.suggestions=b.suggestions,c.suggest()):
c.isBadQuery(a)||(d.params[d.paramName]=a,!1!==d.onSearchStart.call(c.element,d.params)&&(e.isFunction(d.serviceUrl)&&(f=d.serviceUrl.call(c.element,a)),e.ajax({url:f,data:d.ignoreParams?null:d.params,type:d.type,dataType:d.dataType}).done(function(b){c.processResponse(b,a);d.onSearchComplete.call(c.element,a)})))},isBadQuery:function(a){for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){this.visible=!1;this.selectedIndex=-1;e(this.suggestionsContainer).hide()},
suggest:function(){if(0===this.suggestions.length)this.hide();else{var a=this.options.formatResult,b=this.getQuery(this.currentValue),c=this.classes.suggestion,d=this.classes.selected,f=e(this.suggestionsContainer),g="";e.each(this.suggestions,function(d,e){g+='<div class="'+c+'" data-index="'+d+'">'+a(e,b)+"</div>"});f.html(g).show();this.visible=!0;this.options.autoSelectFirst&&(this.selectedIndex=0,f.children().first().addClass(d))}},verifySuggestionsFormat:function(a){return a.length&&"string"===
typeof a[0]?e.map(a,function(a){return{value:a,data:null}}):a},processResponse:function(a,b){var c=this.options,d=c.transformResult(a,b);d.suggestions=this.verifySuggestionsFormat(d.suggestions);c.noCache||(this.cachedResponse[d[c.paramName]]=d,0===d.suggestions.length&&this.badQueries.push(d[c.paramName]));b===this.getQuery(this.currentValue)&&(this.suggestions=d.suggestions,this.suggest())},activate:function(a){var b=this.classes.selected,c=e(this.suggestionsContainer),d=c.children();c.children("."+
b).removeClass(b);this.selectedIndex=a;return-1!==this.selectedIndex&&d.length>this.selectedIndex?(a=d.get(this.selectedIndex),e(a).addClass(b),a):null},select:function(a,b){var c=this.suggestions[a];c&&(this.el.val(c),this.ignoreValueChange=b,this.hide(),this.onSelect(a))},moveUp:function(){-1!==this.selectedIndex&&(0===this.selectedIndex?(e(this.suggestionsContainer).children().first().removeClass(this.classes.selected),this.selectedIndex=-1,this.el.val(this.currentValue)):this.adjustScroll(this.selectedIndex-
1))},moveDown:function(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)},adjustScroll:function(a){var b=this.activate(a),c,d;b&&(b=b.offsetTop,c=e(this.suggestionsContainer).scrollTop(),d=c+this.options.maxHeight-25,b<c?e(this.suggestionsContainer).scrollTop(b):b>d&&e(this.suggestionsContainer).scrollTop(b-this.options.maxHeight+25),this.el.val(this.getValue(this.suggestions[a].value)))},onSelect:function(a){var b=this.options.onSelect;a=this.suggestions[a];
this.el.val(this.getValue(a.value));e.isFunction(b)&&b.call(this.element,a)},getValue:function(a){var b=this.options.delimiter,c;if(!b)return a;c=this.currentValue;b=c.split(b);return 1===b.length?a:c.substr(0,c.length-b[b.length-1].length)+a},dispose:function(){this.el.off(".autocomplete").removeData("autocomplete");this.disableKillerFn();e(this.suggestionsContainer).remove()}};e.fn.autocomplete=function(a,b){return 0===arguments.length?this.first().data("autocomplete"):this.each(function(){var c=
e(this),d=c.data("autocomplete");if("string"===typeof a){if(d&&"function"===typeof d[a])d[a](b)}else d&&d.dispose&&d.dispose(),d=new g(this,a),c.data("autocomplete",d)})}});

View file

@ -3,6 +3,11 @@
{% load staticfiles %} {% load staticfiles %}
{% load humanize %} {% load humanize %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load accounts_tags %}
{% block css %}
<link rel="stylesheet" href="{% static 'css/autocomplete.css' %}">
{% endblock %}
{% block grouptab %} class="active"{% endblock %} {% block grouptab %} class="active"{% endblock %}
@ -68,9 +73,13 @@
{% block js_end %} {% block js_end %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/jquery.autocomplete.min.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
$('input[name="user"]').autocomplete({ $('input[name="user"]').autocomplete({
source: "{% url 'add-user-to-group' group.id %}" serviceUrl: "{% url 'add-user-to-group' group.id %}",
onSelect: function (data) {
$('input[name="user"]').val(data.data);
}
}); });
</script> </script>
<script src="{% static 'js/accounts.js' %}"></script> <script src="{% static 'js/accounts.js' %}"></script>

View file

@ -4,6 +4,10 @@
{% load humanize %} {% load humanize %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block css %}
<link rel="stylesheet" href="{% static 'css/autocomplete.css' %}">
{% endblock %}
{% block teamtab %} class="active"{% endblock %} {% block teamtab %} class="active"{% endblock %}
{% block moretabs %} {% block moretabs %}
@ -93,12 +97,19 @@
{% block js_end %} {% block js_end %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/jquery.autocomplete.min.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
$('input[name="user"]').autocomplete({ $('input[name="user"]').autocomplete({
source: "{% url 'add-user-to-team' team.id %}" serviceUrl: "{% url 'add-user-to-team' team.id %}",
onSelect: function (data) {
$('input[name="user"]').val(data.data);
}
}); });
$('input[name="group"]').autocomplete({ $('input[name="group"]').autocomplete({
source: "{% url 'add-group-to-team' team.id %}" serviceUrl: "{% url 'add-group-to-team' team.id %}",
onSelect: function (data) {
$('input[name="group"]').val(data.data);
}
}); });
</script> </script>
<script src="{% static 'js/accounts.js' %}"></script> <script src="{% static 'js/accounts.js' %}"></script>

View file

@ -4,6 +4,10 @@
{% load humanize %} {% load humanize %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block css %}
<link rel="stylesheet" href="{% static 'css/autocomplete.css' %}">
{% endblock %}
{% block usertab %} class="active"{% endblock %} {% block usertab %} class="active"{% endblock %}
{% block moretabs %} {% block moretabs %}
@ -108,12 +112,19 @@
{% block js_end %} {% block js_end %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/jquery.autocomplete.min.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
$('input[name="group"]').autocomplete({ $('input[name="group"]').autocomplete({
source: "{% url 'add-group-to-user' user.id %}" serviceUrl: "{% url 'add-group-to-user' user.id %}",
onSelect: function (data) {
$('input[name="group"]').val(data.data);
}
}); });
$('input[name="team"]').autocomplete({ $('input[name="team"]').autocomplete({
source: "{% url 'add-team-to-user' user.id %}" serviceUrl: "{% url 'add-team-to-user' user.id %}",
onSelect: function (data) {
$('input[name="team"]').val(data.data);
}
}); });
</script> </script>
<script src="{% static 'js/accounts.js' %}"></script> <script src="{% static 'js/accounts.js' %}"></script>