Bug 91840 - Autocomplete for users
git-svn-id: svn://svn.office.custis.ru/3rdparty/bugzilla.org/trunk@1489 6955db30-a419-402b-8a0d-67ecbb4d7f56master
parent
b431d1a537
commit
67b6abc243
|
@ -74,9 +74,9 @@ sub ready_list
|
|||
my $self = shift;
|
||||
$self->{r} ||= [
|
||||
map { {
|
||||
login => $_->[0]->login,
|
||||
identity => $_->[1] . ': ' . $_->[0]->identity,
|
||||
visible => 1
|
||||
email => $_->[0]->login,
|
||||
real_name => $_->[1] . ': ' . $_->[0]->name,
|
||||
visible => 1,
|
||||
} }
|
||||
sort { ($roleindex{$b->[1]} <=> $roleindex{$a->[1]})
|
||||
|| ($a->[0]->login cmp $b->[0]->login) }
|
||||
|
|
|
@ -130,7 +130,6 @@ sub create {
|
|||
return { id => $self->type('int', $user->id) };
|
||||
}
|
||||
|
||||
|
||||
# function to return user information by passing either user ids or
|
||||
# login names or both together:
|
||||
# $call = $rpc->call( 'User.get', { ids => [1,2,3],
|
||||
|
@ -138,6 +137,17 @@ sub create {
|
|||
sub get {
|
||||
my ($self, $params) = validate(@_, 'names', 'ids');
|
||||
|
||||
# Make them arrays if they aren't
|
||||
if ($params->{names} && !ref $params->{names}) {
|
||||
$params->{names} = [ $params->{names} ];
|
||||
}
|
||||
if ($params->{ids} && !ref $params->{ids}) {
|
||||
$params->{ids} = [ $params->{ids} ];
|
||||
}
|
||||
if ($params->{match} && !ref $params->{match}) {
|
||||
$params->{match} = [ $params->{match} ];
|
||||
}
|
||||
|
||||
my @user_objects;
|
||||
@user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
|
||||
if $params->{names};
|
||||
|
@ -145,7 +155,7 @@ sub get {
|
|||
# start filtering to remove duplicate user ids
|
||||
my %unique_users = map { $_->id => $_ } @user_objects;
|
||||
@user_objects = values %unique_users;
|
||||
|
||||
|
||||
my @users;
|
||||
|
||||
# If the user is not logged in: Return an error if they passed any user ids.
|
||||
|
@ -187,7 +197,7 @@ sub get {
|
|||
userid => $obj->id});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# User Matching
|
||||
my $limit;
|
||||
if ($params->{'maxusermatches'}) {
|
||||
|
@ -202,9 +212,9 @@ sub get {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
my $in_group = $self->_filter_users_by_group(
|
||||
\@user_objects, $params);
|
||||
\@user_objects, $params);
|
||||
if (Bugzilla->user->in_group('editusers')) {
|
||||
@users =
|
||||
map {filter $params, {
|
||||
|
@ -216,8 +226,7 @@ sub get {
|
|||
email_enabled => $self->type('boolean', $_->email_enabled),
|
||||
login_denied_text => $self->type('string', $_->disabledtext),
|
||||
}} @$in_group;
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
@users =
|
||||
map {filter $params, {
|
||||
|
|
BIN
images/dn.gif
BIN
images/dn.gif
Binary file not shown.
Before Width: | Height: | Size: 91 B |
Binary file not shown.
Before Width: | Height: | Size: 79 B |
54
js/field.js
54
js/field.js
|
@ -50,7 +50,7 @@ function showEditableField(e, ContainerInputArray)
|
|||
}
|
||||
addClass(ContainerInputArray[0], 'bz_default_hidden');
|
||||
removeClass(inputArea, 'bz_default_hidden');
|
||||
if (inputArea.tagName.toLowerCase() == "input")
|
||||
if (inputArea.nodeName.toLowerCase() == "input")
|
||||
inputs.push(inputArea);
|
||||
else
|
||||
inputs = inputArea.getElementsByTagName('input');
|
||||
|
@ -265,3 +265,55 @@ function _value_id(field_name, id)
|
|||
{
|
||||
return 'v' + id + '_' + field_name;
|
||||
}
|
||||
|
||||
// Data loader for user autocomplete
|
||||
function userAutocomplete(hint, emptyOptions)
|
||||
{
|
||||
if (!hint.input.value)
|
||||
{
|
||||
if (emptyOptions)
|
||||
hint.replaceItems(emptyOptions);
|
||||
else
|
||||
hint.replaceItems(null);
|
||||
return;
|
||||
}
|
||||
var x;
|
||||
if (window.XMLHttpRequest)
|
||||
x = new XMLHttpRequest();
|
||||
else
|
||||
{
|
||||
try { return new ActiveXObject("Msxml2.XMLHTTP"); }
|
||||
catch (e) { return new ActiveXObject("Microsoft.XMLHTTP"); }
|
||||
}
|
||||
x.onreadystatechange = function()
|
||||
{
|
||||
if (x.readyState == 4)
|
||||
{
|
||||
var r = {};
|
||||
try { eval('r = '+x.responseText+';'); } catch (e) { return; }
|
||||
if (r.status == 'ok')
|
||||
{
|
||||
var data = [];
|
||||
for (var i = 0; i < r.users.length; i++)
|
||||
data.push([
|
||||
'<span class="hintRealname">' + htmlspecialchars(r.users[i].real_name) +
|
||||
'</span><br /><span class="hintEmail">' + htmlspecialchars(r.users[i].email) + '</span>',
|
||||
r.users[i].email
|
||||
]);
|
||||
// FIXME 3 letters: remove hardcode, also in Bugzilla::User::match()
|
||||
if (data.length == 0 && hint.input.value.length < 3)
|
||||
hint.emptyText = 'Type at least 3 letters';
|
||||
else
|
||||
hint.emptyText = 'No users found';
|
||||
hint.replaceItems(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
var u = window.location.href.replace(/[^\/]+$/, '');
|
||||
u += 'xml.cgi?method=User.get&output=json&maxusermatches=20';
|
||||
var l = hint.input.value.split(/[\s,]*,[\s,]*/);
|
||||
for (var i = 0; i < l.length; i++)
|
||||
u += '&match='+encodeURI(l[i]);
|
||||
x.open('GET', u);
|
||||
x.send(null);
|
||||
}
|
||||
|
|
89
js/global.js
89
js/global.js
|
@ -116,92 +116,3 @@ function set_language(value)
|
|||
});
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/* template/en/global/menuforusers.html.tmpl */
|
||||
|
||||
function menuforusers_initcombo(id, multi)
|
||||
{
|
||||
var sel = document.getElementById(id+"_s");
|
||||
var ed = document.getElementById(id);
|
||||
if (!sel || !ed)
|
||||
return;
|
||||
var p = sel.parentNode;
|
||||
/* We must move <select> out of its parent element temporarily,
|
||||
because it can be invisible in some cases (i.e. New Bug form),
|
||||
and have offsetWidth=offsetHeight=0 */
|
||||
document.body.appendChild(sel);
|
||||
var w = (sel.offsetWidth-sel.offsetHeight+2);
|
||||
if (w > 400)
|
||||
{
|
||||
w = 400;
|
||||
sel.style.width = (w+sel.offsetHeight-2)+'px';
|
||||
}
|
||||
ed.style.width = w+'px';
|
||||
p.appendChild(sel);
|
||||
ed.style.borderWidth = 0;
|
||||
menuforusers_tocombo(id);
|
||||
if (multi)
|
||||
{
|
||||
addListener(document.body, "click", function(ev) {
|
||||
t = eventTarget(ev);
|
||||
if (t.id != id && t.id != id+'_b' && t.id != id+'_s' && t.parentNode.id != id+'_s')
|
||||
menuforusers_showmulti(id, false)
|
||||
});
|
||||
addListener(document.getElementById(id+'_b'), "click", function(ev) { menuforusers_showmulti(id); });
|
||||
}
|
||||
}
|
||||
|
||||
function menuforusers_tocombo(id, multi)
|
||||
{
|
||||
var sel = document.getElementById(id+"_s");
|
||||
var ed = document.getElementById(id);
|
||||
if (!sel || !ed)
|
||||
return;
|
||||
var nv = [];
|
||||
var v = ed.value.split(/[\s,]+/);
|
||||
var i, j;
|
||||
for (i = 0; i < v.length; i++)
|
||||
{
|
||||
for (j = 0; j < sel.options.length; j++)
|
||||
{
|
||||
if (sel.options[j].value.toLowerCase().indexOf(v[i].toLowerCase()) >= 0 ||
|
||||
sel.options[j].text.toLowerCase().indexOf(v[i].toLowerCase()) >= 0)
|
||||
{
|
||||
sel.options[j].selected = true;
|
||||
nv.push(sel.options[j].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j >= sel.options.length)
|
||||
nv.push(v[i]);
|
||||
if (!multi)
|
||||
break;
|
||||
}
|
||||
ed.value = nv.join(', ');
|
||||
}
|
||||
|
||||
function menuforusers_fromcombo(id, multi)
|
||||
{
|
||||
var sel = document.getElementById(id+"_s");
|
||||
var ed = document.getElementById(id);
|
||||
if (!sel || !ed)
|
||||
return;
|
||||
v = [];
|
||||
for (var i = 0; i < sel.options.length; i++)
|
||||
if (sel.options[i].selected)
|
||||
v.push(sel.options[i].value);
|
||||
ed.value = v.join(', ');
|
||||
}
|
||||
|
||||
function menuforusers_showmulti(id, wha)
|
||||
{
|
||||
var sel = document.getElementById(id+"_s");
|
||||
var btn = document.getElementById(id+"_b");
|
||||
if (!sel || !btn)
|
||||
return;
|
||||
var show = sel.style.visibility == 'hidden' && !sel.disabled;
|
||||
if (typeof(wha) != 'undefined')
|
||||
show = wha;
|
||||
sel.style.visibility = show ? '' : 'hidden';
|
||||
btn.src = 'images/dn' + (show ? 'push' : '') + '.gif';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,482 @@
|
|||
/* Simple autocomplete for text inputs, with the support for multiple selection.
|
||||
Homepage: http://yourcmc.ru/wiki/SimpleAutocomplete
|
||||
(c) Vitaliy Filippov 2011
|
||||
Usage:
|
||||
Include hinter.css, hinter.js on your page. Then write:
|
||||
var hint = new SimpleAutocomplete(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML);
|
||||
Parameters:
|
||||
input
|
||||
The input, either id or DOM element reference (the input must have an id anyway).
|
||||
dataLoader(hint, value)
|
||||
Callback which should load autocomplete options and then call
|
||||
hint.replaceItems([ [ name, value ], [ name, value ], ... ])
|
||||
'hint' parameter will be this autocompleter object, and the guess
|
||||
should be done based on 'value' parameter (string).
|
||||
Optional parameters:
|
||||
multipleDelimiter
|
||||
Pass a delimiter string (for example ',' or ';') to enable multiple selection.
|
||||
Item values cannot have leading or trailing whitespace. Input value will consist
|
||||
of selected item values separated by this delimiter plus single space.
|
||||
dataLoader should handle it's 'value' parameter accordingly in this case,
|
||||
because it will be just the raw value of the input, probably with incomplete
|
||||
item or items, typed by the user.
|
||||
onChangeListener(hint, index)
|
||||
Callback which is called when input value is changed using this dropdown.
|
||||
index is the number of element which selection is changed, starting with 0.
|
||||
It must be used instead of normal 'onchange' event.
|
||||
maxHeight
|
||||
Maximum hint dropdown height in pixels
|
||||
emptyText
|
||||
Text to show when dataLoader returns no options.
|
||||
If emptyText === false, the hint will be hidden instead of showing text.
|
||||
allowHTML
|
||||
If true, HTML code will be allowed in option names.
|
||||
*/
|
||||
|
||||
var SimpleAutocomplete = function(input, dataLoader, multipleDelimiter, onChangeListener, maxHeight, emptyText, allowHTML)
|
||||
{
|
||||
if (typeof(input) == 'string')
|
||||
input = document.getElementById(input);
|
||||
if (emptyText === undefined)
|
||||
emptyText = 'No items found';
|
||||
|
||||
// Parameters
|
||||
var self = this;
|
||||
self.input = input;
|
||||
self.multipleDelimiter = multipleDelimiter;
|
||||
self.dataLoader = dataLoader;
|
||||
self.onChangeListener = onChangeListener;
|
||||
self.maxHeight = maxHeight;
|
||||
self.emptyText = emptyText;
|
||||
self.allowHTML = allowHTML;
|
||||
|
||||
// Variables
|
||||
self.items = [];
|
||||
self.skipHideCounter = 0;
|
||||
self.selectedIndex = -1;
|
||||
self.id = input.id;
|
||||
self.disabled = false;
|
||||
|
||||
// Initialiser
|
||||
var init = function()
|
||||
{
|
||||
var e = self.input;
|
||||
var p = getOffset(e);
|
||||
|
||||
// Create hint layer
|
||||
var t = self.hintLayer = document.createElement('div');
|
||||
t.className = 'hintLayer';
|
||||
t.style.display = 'none';
|
||||
t.style.position = 'absolute';
|
||||
t.style.top = (p.top+e.offsetHeight) + 'px';
|
||||
t.style.zIndex = 1000;
|
||||
t.style.left = p.left + 'px';
|
||||
if (self.maxHeight)
|
||||
{
|
||||
t.style.overflowY = 'scroll';
|
||||
try { t.style.overflow = '-moz-scrollbars-vertical'; } catch(exc) {}
|
||||
t.style.maxHeight = self.maxHeight+'px';
|
||||
if (!t.style.maxHeight)
|
||||
self.scriptMaxHeight = true;
|
||||
}
|
||||
document.body.appendChild(t);
|
||||
|
||||
// Remember instance
|
||||
e.SimpleAutocomplete_input = self;
|
||||
t.SimpleAutocomplete_layer = self;
|
||||
SimpleAutocomplete.SimpleAutocompletes.push(self);
|
||||
|
||||
// Set event listeners
|
||||
var msie = navigator.userAgent.match('MSIE') && !navigator.userAgent.match('Opera');
|
||||
if (msie)
|
||||
addListener(e, 'keydown', self.onKeyPress);
|
||||
else
|
||||
{
|
||||
addListener(e, 'keydown', self.onKeyDown);
|
||||
addListener(e, 'keypress', self.onKeyPress);
|
||||
}
|
||||
addListener(e, 'keyup', self.onKeyUp);
|
||||
addListener(e, 'change', self.onChange);
|
||||
addListener(e, 'focus', self.onInputFocus);
|
||||
addListener(e, 'blur', self.onInputBlur);
|
||||
self.onChange();
|
||||
};
|
||||
|
||||
// obj = [ [ name, value, disabled ], [ name, value ], ... ]
|
||||
self.replaceItems = function(items)
|
||||
{
|
||||
self.hintLayer.innerHTML = '';
|
||||
self.hintLayer.scrollTop = 0;
|
||||
self.items = [];
|
||||
if (!items || items.length == 0)
|
||||
{
|
||||
if (self.emptyText)
|
||||
{
|
||||
var d = document.createElement('div');
|
||||
d.className = 'hintEmptyText';
|
||||
d.innerHTML = self.emptyText;
|
||||
self.hintLayer.appendChild(d);
|
||||
}
|
||||
else
|
||||
self.disable();
|
||||
return;
|
||||
}
|
||||
self.enable();
|
||||
var h = {};
|
||||
if (self.multipleDelimiter)
|
||||
{
|
||||
var old = self.input.value.split(self.multipleDelimiter);
|
||||
for (var i = 0; i < old.length; i++)
|
||||
h[old[i].trim()] = true;
|
||||
}
|
||||
for (var i in items)
|
||||
self.hintLayer.appendChild(self.makeItem(items[i][0], items[i][1], h[items[i][1]]));
|
||||
if (self.maxHeight)
|
||||
{
|
||||
self.hintLayer.style.height =
|
||||
(self.hintLayer.scrollHeight > self.maxHeight
|
||||
? self.maxHeight : self.hintLayer.scrollHeight) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
// Create a drop-down list item, include checkbox if self.multipleDelimiter is true
|
||||
self.makeItem = function(name, value, checked)
|
||||
{
|
||||
var d = document.createElement('div');
|
||||
d.id = self.id+'_item_'+self.items.length;
|
||||
d.className = 'hintItem';
|
||||
d.title = value;
|
||||
if (self.allowHTML)
|
||||
d.innerHTML = name;
|
||||
if (self.multipleDelimiter)
|
||||
{
|
||||
var c = document.createElement('input');
|
||||
c.type = 'checkbox';
|
||||
c.id = self.id+'_check_'+self.items.length;
|
||||
c.checked = checked && true;
|
||||
c.value = value;
|
||||
if (d.childNodes.length)
|
||||
d.insertBefore(c, d.firstChild);
|
||||
else
|
||||
d.appendChild(c);
|
||||
addListener(c, 'click', self.preventCheck);
|
||||
}
|
||||
if (!self.allowHTML)
|
||||
d.appendChild(document.createTextNode(name));
|
||||
addListener(d, 'mouseover', self.onItemMouseOver);
|
||||
addListener(d, 'mousedown', self.onItemClick);
|
||||
self.items.push([name, value, checked]);
|
||||
return d;
|
||||
};
|
||||
|
||||
// Prevent default action on checkbox
|
||||
self.preventCheck = function(ev)
|
||||
{
|
||||
ev = ev||window.event;
|
||||
return stopEvent(ev, false, true);
|
||||
};
|
||||
|
||||
// Handle item mouse over
|
||||
self.onItemMouseOver = function()
|
||||
{
|
||||
return self.highlightItem(this);
|
||||
};
|
||||
|
||||
// Handle item clicks
|
||||
self.onItemClick = function(ev)
|
||||
{
|
||||
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
|
||||
return true;
|
||||
};
|
||||
|
||||
// Move highlight forward or back by 'by' items (integer)
|
||||
self.moveHighlight = function(by)
|
||||
{
|
||||
var n = self.selectedIndex+by;
|
||||
if (n < 0)
|
||||
n = 0;
|
||||
var elem = document.getElementById(self.id+'_item_'+n);
|
||||
if (!elem)
|
||||
return true;
|
||||
return self.highlightItem(elem);
|
||||
};
|
||||
|
||||
// Make item 'elem' active (highlighted)
|
||||
self.highlightItem = function(elem)
|
||||
{
|
||||
if (self.selectedIndex >= 0)
|
||||
{
|
||||
var c = self.getItem();
|
||||
if (c)
|
||||
c.className = 'hintItem';
|
||||
}
|
||||
self.selectedIndex = parseInt(elem.id.substr(self.id.length+6));
|
||||
elem.className = 'hintActiveItem';
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get index'th item, or current when index is null
|
||||
self.getItem = function(index)
|
||||
{
|
||||
if (index == null)
|
||||
index = self.selectedIndex;
|
||||
if (index < 0)
|
||||
return null;
|
||||
return document.getElementById(self.id+'_item_'+self.selectedIndex);
|
||||
};
|
||||
|
||||
// Select index'th item - change the input value and hide the hint if not a multi-select
|
||||
self.selectItem = function(index)
|
||||
{
|
||||
if (!self.multipleDelimiter)
|
||||
{
|
||||
self.input.value = self.items[index][1];
|
||||
self.hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById(self.id+'_check_'+index).checked = self.items[index][2] = !self.items[index][2];
|
||||
var old = self.input.value.split(self.multipleDelimiter);
|
||||
for (var i = 0; i < old.length; i++)
|
||||
old[i] = old[i].trim();
|
||||
if (!self.items[index][2])
|
||||
{
|
||||
for (var i = old.length-1; i >= 0; i--)
|
||||
if (old[i] == self.items[index][1])
|
||||
old.splice(i, 1);
|
||||
self.input.value = old.join(self.multipleDelimiter+' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
var h = {};
|
||||
for (var i = 0; i < self.items.length; i++)
|
||||
if (self.items[i][2])
|
||||
h[self.items[i][1]] = true;
|
||||
var nl = [];
|
||||
for (var i = 0; i < old.length; i++)
|
||||
{
|
||||
if (h[old[i]])
|
||||
{
|
||||
delete h[old[i]];
|
||||
nl.push(old[i]);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < self.items.length; i++)
|
||||
if (self.items[i][2] && h[self.items[i][1]])
|
||||
nl.push(self.items[i][1]);
|
||||
self.input.value = nl.join(self.multipleDelimiter+' ');
|
||||
}
|
||||
}
|
||||
if (self.onChangeListener)
|
||||
self.onChangeListener(self, index);
|
||||
};
|
||||
|
||||
// Handle user input, load new items
|
||||
self.onChange = function()
|
||||
{
|
||||
var v = self.input.value.trim();
|
||||
if (v != self.curValue)
|
||||
{
|
||||
self.curValue = v;
|
||||
self.dataLoader(self, v);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Handle Enter key presses, cancel handling of arrow keys
|
||||
self.onKeyUp = function(ev)
|
||||
{
|
||||
ev = ev||window.event;
|
||||
if (ev.keyCode != 10 && ev.keyCode != 13)
|
||||
self.show();
|
||||
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 10 || ev.keyCode == 13)
|
||||
return stopEvent(ev, true, true);
|
||||
self.onChange();
|
||||
return true;
|
||||
};
|
||||
|
||||
// Cancel handling of Enter key
|
||||
self.onKeyDown = function(ev)
|
||||
{
|
||||
ev = ev||window.event;
|
||||
if (ev.keyCode == 10 || ev.keyCode == 13)
|
||||
return stopEvent(ev, true, true);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Handle arrow keys and Enter
|
||||
self.onKeyPress = function(ev)
|
||||
{
|
||||
ev = ev||window.event;
|
||||
if (ev.keyCode == 38) // up
|
||||
self.moveHighlight(-1);
|
||||
else if (ev.keyCode == 40) // down
|
||||
self.moveHighlight(1);
|
||||
else if (ev.keyCode == 10 || ev.keyCode == 13) // enter
|
||||
{
|
||||
if (self.selectedIndex >= 0)
|
||||
self.selectItem(self.selectedIndex);
|
||||
return stopEvent(ev, true, true);
|
||||
}
|
||||
else
|
||||
return true;
|
||||
// scrolling
|
||||
if (self.selectedIndex >= 0)
|
||||
{
|
||||
var c = self.getItem();
|
||||
var t = self.hintLayer;
|
||||
var ct = getOffset(c).top + t.scrollTop - t.style.top.substr(0, t.style.top.length-2);
|
||||
var ch = c.scrollHeight;
|
||||
if (ct+ch-t.offsetHeight > t.scrollTop)
|
||||
t.scrollTop = ct+ch-t.offsetHeight;
|
||||
else if (ct < t.scrollTop)
|
||||
t.scrollTop = ct;
|
||||
}
|
||||
return stopEvent(ev, true, true);
|
||||
};
|
||||
|
||||
// Called when input receives focus
|
||||
self.onInputFocus = function()
|
||||
{
|
||||
self.show();
|
||||
self.hasFocus = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Called when input loses focus
|
||||
self.onInputBlur = function()
|
||||
{
|
||||
self.hide();
|
||||
self.hasFocus = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Hide hinter
|
||||
self.hide = function()
|
||||
{
|
||||
if (!self.skipHideCounter)
|
||||
self.hintLayer.style.display = 'none';
|
||||
else
|
||||
self.skipHideCounter = 0;
|
||||
};
|
||||
|
||||
// Show hinter
|
||||
self.show = function()
|
||||
{
|
||||
var p = getOffset(self.input);
|
||||
self.hintLayer.style.top = (p.top+self.input.offsetHeight) + 'px';
|
||||
self.hintLayer.style.left = p.left + 'px';
|
||||
if (!self.disabled)
|
||||
self.hintLayer.style.display = '';
|
||||
};
|
||||
|
||||
// Disable hinter, for the case when there is no items and no empty text
|
||||
self.disable = function()
|
||||
{
|
||||
self.disabled = true;
|
||||
self.hide();
|
||||
};
|
||||
|
||||
// Enable hinter
|
||||
self.enable = function()
|
||||
{
|
||||
var show = self.disabled;
|
||||
self.disabled = false;
|
||||
if (show)
|
||||
self.show();
|
||||
}
|
||||
|
||||
// *** Call initialise ***
|
||||
init();
|
||||
};
|
||||
|
||||
// Global variable
|
||||
SimpleAutocomplete.SimpleAutocompletes = [];
|
||||
|
||||
// Global mousedown handler, hides dropdowns when clicked outside
|
||||
SimpleAutocomplete.GlobalMouseDown = function(ev)
|
||||
{
|
||||
var target = ev.target || ev.srcElement;
|
||||
var esh;
|
||||
while (target)
|
||||
{
|
||||
esh = target.SimpleAutocomplete_input;
|
||||
if (esh)
|
||||
break;
|
||||
else if (target.SimpleAutocomplete_layer)
|
||||
{
|
||||
if (target.SimpleAutocomplete_layer.hasFocus)
|
||||
target.SimpleAutocomplete_layer.skipHideCounter++;
|
||||
return true;
|
||||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
for (var i in SimpleAutocomplete.SimpleAutocompletes)
|
||||
if (SimpleAutocomplete.SimpleAutocompletes[i] != esh)
|
||||
SimpleAutocomplete.SimpleAutocompletes[i].hide();
|
||||
return true;
|
||||
};
|
||||
|
||||
//// UTILITY FUNCTIONS ////
|
||||
// You can delete this section if you already have them somewhere in your scripts //
|
||||
|
||||
// Cancel event bubbling and/or default action
|
||||
var stopEvent = function(ev, cancelBubble, preventDefault)
|
||||
{
|
||||
if (cancelBubble)
|
||||
{
|
||||
if (ev.stopPropagation)
|
||||
ev.stopPropagation();
|
||||
else
|
||||
ev.cancelBubble = true;
|
||||
}
|
||||
if (preventDefault && ev.preventDefault)
|
||||
ev.preventDefault();
|
||||
ev.returnValue = !preventDefault;
|
||||
return !preventDefault;
|
||||
};
|
||||
|
||||
// Get element position, relative to the top-left corner of page
|
||||
var getOffset = function(elem)
|
||||
{
|
||||
if (elem.getBoundingClientRect)
|
||||
return getOffsetRect(elem);
|
||||
else
|
||||
return getOffsetSum(elem);
|
||||
};
|
||||
|
||||
// Get element position using getBoundingClientRect()
|
||||
var getOffsetRect = function(elem)
|
||||
{
|
||||
var box = elem.getBoundingClientRect();
|
||||
|
||||
var body = document.body;
|
||||
var docElem = document.documentElement;
|
||||
|
||||
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
|
||||
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
|
||||
var clientTop = docElem.clientTop || body.clientTop || 0;
|
||||
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
|
||||
var top = box.top + scrollTop - clientTop;
|
||||
var left = box.left + scrollLeft - clientLeft;
|
||||
|
||||
return { top: Math.round(top), left: Math.round(left) };
|
||||
};
|
||||
|
||||
// Get element position using sum of offsetTop/offsetLeft
|
||||
var getOffsetSum = function(elem)
|
||||
{
|
||||
var top = 0, left = 0;
|
||||
while(elem)
|
||||
{
|
||||
top = top + parseInt(elem.offsetTop);
|
||||
left = left + parseInt(elem.offsetLeft);
|
||||
elem = elem.offsetParent;
|
||||
}
|
||||
return { top: top, left: left };
|
||||
};
|
||||
|
||||
//// END UTILITY FUNCTIONS ////
|
||||
|
||||
// Set global mousedown listener
|
||||
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });
|
10
js/util.js
10
js/util.js
|
@ -23,6 +23,16 @@ function findPos(obj)
|
|||
return [ obj.x, obj.y ];
|
||||
}
|
||||
|
||||
// Escape special HTML/XML characters
|
||||
function htmlspecialchars(s)
|
||||
{
|
||||
s = s.replace(/&/g, '&'); //&
|
||||
s = s.replace(/</g, '<'); //<
|
||||
s = s.replace(/>/g, '>'); //>
|
||||
s = s.replace(/"/g, '"'); //"
|
||||
return s;
|
||||
}
|
||||
|
||||
// Checks if a specified value 'val' is in the specified array 'arr'
|
||||
function bz_isValueInArray(arr, val)
|
||||
{
|
||||
|
|
|
@ -546,3 +546,44 @@ fieldset.chart { margin-bottom: 8px; padding: 0 8px 4px; }
|
|||
.chart_not_c { border: 1px inset gray; background-color: white; font-weight: bold; width: 3.5em; }
|
||||
div.chart._neg { border: 1px solid black; background-image: url(../../images/neg.gif); margin: 1px; padding: 2px 1px 0 1px; }
|
||||
fieldset.chart._neg { background-image: url(../../images/neg.gif); }
|
||||
|
||||
/* SimpleAutocomplete styles */
|
||||
.hintLayer {
|
||||
border: 1px solid gray;
|
||||
color: gray;
|
||||
width: 300pt;
|
||||
background-color: white;
|
||||
font-size: 80%;
|
||||
max-height: 300pt;
|
||||
overflow-y: scroll;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
}
|
||||
.hintEmptyText {
|
||||
padding: 3px;
|
||||
}
|
||||
.hintItem {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
padding: 1px 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.hintActiveItem {
|
||||
color: white;
|
||||
background-color: #008;
|
||||
cursor: pointer;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
.hintItem input, .hintActiveItem input {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
.hintRealName {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.hintEmail {
|
||||
color: #808080;
|
||||
}
|
||||
.hintActiveItem .hintEmail {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
|
|
@ -26,55 +26,22 @@
|
|||
# custom_userlist: optional, specify a limited list of users to use
|
||||
#%]
|
||||
|
||||
<div style="position: relative">
|
||||
[% SET id="userselect_" _ name IF !id %]
|
||||
[% SET emptyok=emptyok ? 1 : 0 %]
|
||||
[% SET custom_userlist=user.get_userlist UNLESS custom_userlist %]
|
||||
[% UNLESS multiple %]
|
||||
<div class="selectfree_ie" style="position:absolute;top:1px;left:1px;z-index:20;overflow:hidden">
|
||||
<input
|
||||
name="[% name | html %]"
|
||||
value="[% value | html %]"
|
||||
autocomplete="off"
|
||||
[% IF disabled %] disabled="[% disabled | html %]" [% END %]
|
||||
[% IF accesskey %] accesskey="[% accesskey | html %]" [% END %]
|
||||
id="[% id | html %]"
|
||||
onchange="menuforusers_tocombo('[% id | js %]', 0); [% onchange | html %]" />[% IF multiple %]<img style="margin-bottom:-4px" src="images/dn.gif" alt="dropdown list" title="dropdown list" onclick="menuforusers_showmulti('[% id | js %]')" />[% END %]
|
||||
[%# http://anton-egorov.blogspot.com/2007/11/ie-select-z-index.html %]
|
||||
<!--[if lte IE 6.5]><iframe style="width: 800px; height: 200px;"></iframe><![endif]-->
|
||||
</div>
|
||||
<select style="top: 0; left: 0; position: relative; z-index: 10"
|
||||
id="[% id | html %]_s"
|
||||
[% IF disabled %] disabled="[% disabled | html %]" [% END %]
|
||||
onchange="menuforusers_fromcombo('[% id | js %]', 0)"
|
||||
>
|
||||
[% IF emptyok %]<option value=""></option>[% END %]
|
||||
[% FOREACH tmpuser = custom_userlist %]
|
||||
<option value="[% tmpuser.login | html %]">[% tmpuser.identity | html %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
[% ELSE %]
|
||||
<span style="border: 1px solid gray; display: inline-block">
|
||||
<input
|
||||
name="[% name | html %]"
|
||||
value="[% value | html %]"
|
||||
[% IF disabled %] disabled="[% disabled | html %]" [% END %]
|
||||
[% IF accesskey %] accesskey="[% accesskey | html %]" [% END %]
|
||||
id="[% id | html %]" style="height: 19px; padding: 0; margin: 0"
|
||||
onchange="menuforusers_tocombo('[% id | js %]', 1); [% onchange | html %]" /><img id="[% id | html %]_b" style="margin-bottom:-4px" src="images/dn.gif" alt="dropdown list" title="dropdown list" />
|
||||
<div class="selectfree_ie" style="position:relative:z-index:30;overflow:hidden">
|
||||
<select id="[% id | html %]_s"
|
||||
[% IF disabled %] disabled="[% disabled | html %]" [% END %]
|
||||
onchange="menuforusers_fromcombo('[% id | js %]', 1)" multiple="multiple" size="[% custom_userlist.size+emptyok < 4 ? custom_userlist.size+emptyok : 4 %]"
|
||||
style="position:absolute;visibility:hidden;z-index:30">
|
||||
[% IF emptyok %]<option value=""></option>[% END %]
|
||||
[% FOREACH tmpuser = custom_userlist %]
|
||||
<option value="[% tmpuser.login | html %]">[% tmpuser.identity | html %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
</div>
|
||||
</span>
|
||||
[% END %]
|
||||
id="[% id | html %]" />
|
||||
<script language="JavaScript">
|
||||
addListener(window, "load", function() { menuforusers_initcombo("[% id | js %]", [% multiple ? 1 : 0 %]) }, null);
|
||||
addListener(window, 'load', function() {
|
||||
[% cl = []; FOREACH tmpuser = custom_userlist; cl.push([ tmpuser.real_name, tmpuser.email ]); END %]
|
||||
var emptyOptions = [% json(cl) %];
|
||||
new SimpleAutocomplete("[% id | js %]",
|
||||
function(h) { userAutocomplete(h, emptyOptions); },
|
||||
[% multiple ? '","' : 'null' %], null, null, null, true);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
%]
|
||||
|
||||
[% param_descs = {
|
||||
usemenuforusers => "If this option is set, $terms.Bugzilla will offer you a list " _
|
||||
"to select from (instead of a text entry field) where a user " _
|
||||
"needs to be selected. This option should not be enabled on " _
|
||||
"sites where there are a large number of users.",
|
||||
usemenuforusers => "If this option is set, a selectbox with all registered users will be shown " _
|
||||
"always instead of a text entry field.<br /><span style='color:red'>WARNING:</span> " _
|
||||
"As $terms.Bugzilla now has an autocomplete feature for users, you most probably " _
|
||||
"don't need this option and you definitely should not turn it on sites with more than 10 users.",
|
||||
|
||||
maxusermatches => "Search for no more than this many matches.<br> " _
|
||||
"If set to '1', no users will be displayed on ambiguous matches. " _
|
||||
|
|
|
@ -37,9 +37,6 @@
|
|||
# atomlink: Atom link URL, May contain HTML
|
||||
#%]
|
||||
|
||||
[% SET svn_rev = "$Revision$" %]
|
||||
[% SET svn_rev = svn_rev.split(' ').1 %]
|
||||
|
||||
[% IF message %]
|
||||
[% PROCESS global/messages.html.tmpl %]
|
||||
[% END %]
|
||||
|
@ -83,7 +80,7 @@
|
|||
[% IF user.settings.skin.value != 'standard' %]
|
||||
[% user_skin = user.settings.skin.value %]
|
||||
[% END %]
|
||||
[% style_urls.unshift('skins/standard/global.css?rev=' _ svn_rev) %]
|
||||
[% style_urls.unshift('skins/standard/global.css') %]
|
||||
|
||||
[%# CSS cascade, part 1: Standard Bugzilla stylesheet set (persistent).
|
||||
# Always present.
|
||||
|
@ -195,8 +192,9 @@
|
|||
type="text/css" />
|
||||
<![endif]-->
|
||||
|
||||
<script src="js/util.js?rev=[% svn_rev %]" type="text/javascript"></script>
|
||||
<script src="js/global.js?rev=[% svn_rev %]" type="text/javascript"></script>
|
||||
<script src="js/util.js" type="text/javascript"></script>
|
||||
<script src="js/global.js" type="text/javascript"></script>
|
||||
<script src="js/hinter.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
[%# The language selector needs javascript to set its cookie,
|
||||
|
|
|
@ -67,13 +67,18 @@
|
|||
<input
|
||||
name="[% name FILTER html %]"
|
||||
value="[% value FILTER html %]"
|
||||
autocomplete="off"
|
||||
[% IF tabindex %] tabindex="[% tabindex FILTER html %]" [% END %]
|
||||
[% IF onchange %] onchange="[% onchange FILTER html %]" [% END %]
|
||||
[% IF disabled %] disabled="[% disabled FILTER html %]" [% END %]
|
||||
[% IF accesskey %] accesskey="[% accesskey FILTER html %]" [% END %]
|
||||
[% IF size %] size="[% size FILTER html %]" [% END %]
|
||||
[% IF id %] id="[% id FILTER html %]" [% END %]
|
||||
>
|
||||
[% IF id %] id="[% id FILTER html %]" [% ELSE %] id="[% name | html %]"[% END %]
|
||||
/>
|
||||
<script language="JavaScript">
|
||||
addListener(window, 'load', function() {
|
||||
[%# FIXME: remove hardcoded i18n message, also from js/field.js::userAutocomplete() %]
|
||||
new SimpleAutocomplete("[% (id || name) | js %]", userAutocomplete, null, null, null, 'No users found', true);
|
||||
});
|
||||
</script>
|
||||
[% END %]
|
||||
|
||||
|
||||
|
|
27
xml.cgi
27
xml.cgi
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/perl -wT
|
||||
# REST-XML RPC interface (input as query parameters, output as xml)
|
||||
# REST-(XML/JSON) RPC interface (input as query parameters, output as xml or json)
|
||||
# Catches all errors and reports them correctly in output!
|
||||
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
||||
# Contributor(s): Vitaliy Filippov <vitalif@mail.ru>
|
||||
|
||||
# USAGE: xml.cgi?method={{method}}&output=(xml|json)&...
|
||||
# FIXME: rename to rest.cgi
|
||||
|
||||
use strict;
|
||||
|
||||
use lib qw(. lib);
|
||||
|
@ -14,7 +17,7 @@ use Bugzilla::WebService::Server::XMLSimple;
|
|||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
my $args = $cgi->Vars;
|
||||
my $args = { %{ $cgi->Vars } }; # throw away the tied hash
|
||||
my $method = $args->{method};
|
||||
|
||||
sub addmsg
|
||||
|
@ -115,9 +118,19 @@ else
|
|||
}
|
||||
}
|
||||
# Send response
|
||||
Bugzilla->send_header(-type => 'text/xml'.(Bugzilla->params->{utf8} ? '; charset=utf-8' : ''));
|
||||
print '<?xml version="1.0"'.(Bugzilla->params->{utf8} ? ' encoding="UTF-8"' : '').' ?>';
|
||||
print '<response>';
|
||||
print xml_dump_simple($result);
|
||||
print '</response>';
|
||||
if (!$args->{output} || lc $args->{output} ne 'json')
|
||||
{
|
||||
# XML output format
|
||||
Bugzilla->send_header(-type => 'text/xml'.(Bugzilla->params->{utf8} ? '; charset=utf-8' : ''));
|
||||
print '<?xml version="1.0"'.(Bugzilla->params->{utf8} ? ' encoding="UTF-8"' : '').' ?>';
|
||||
print '<response>';
|
||||
print xml_dump_simple($result);
|
||||
print '</response>';
|
||||
}
|
||||
else
|
||||
{
|
||||
# JSON output format
|
||||
Bugzilla->send_header(-type => 'application/json'.(Bugzilla->params->{utf8} ? '; charset=utf-8' : ''));
|
||||
print bz_encode_json($result);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue