Experimental implementation of select fields as hinted editboxes
parent
2638849b6d
commit
ed5108ffe5
|
@ -719,6 +719,8 @@ sub default_value_hash_for
|
|||
->{$self->id}->{$visibility_value_id} };
|
||||
}
|
||||
|
||||
sub use_combobox { return $_[0]->name eq 'product' || $_[0]->name eq 'cf_keywords'; }
|
||||
|
||||
# Field and value dependency data, intended for use in client JavaScript
|
||||
sub json_visibility
|
||||
{
|
||||
|
@ -731,6 +733,8 @@ sub json_visibility
|
|||
default_field => $self->default_field ? $self->default_field->name : undef,
|
||||
nullable => $self->nullable ? 1 : 0,
|
||||
default_value => $self->default_value || undef,
|
||||
use_combobox => $self->use_combobox,
|
||||
multiple => $self->type == FIELD_TYPE_MULTI_SELECT,
|
||||
fields => {},
|
||||
values => {},
|
||||
defaults => {},
|
||||
|
|
|
@ -28,9 +28,165 @@ function initControlledFields()
|
|||
reflowFieldColumns();
|
||||
}
|
||||
|
||||
function fieldHintLoader(hint, value, more)
|
||||
{
|
||||
if (value == hint.input.getAttribute('value'))
|
||||
{
|
||||
value = '';
|
||||
}
|
||||
var j, l = value.length ? value.trim().split(/[\s,]*,[\s,]*/) : [];
|
||||
for (j = 0; j < l.length; j++)
|
||||
{
|
||||
l[j] = l[j].toLowerCase();
|
||||
}
|
||||
// Get current IDs as hash
|
||||
var ids = hint.input.getAttribute('data-ids');
|
||||
ids = ids.length ? ids.split(',') : [];
|
||||
var idh = {};
|
||||
for (j = 0; j < ids.length; j++)
|
||||
{
|
||||
idh[ids[j]] = true;
|
||||
}
|
||||
// Get value visibility data, if applicable
|
||||
var vv, h;
|
||||
var m = field_metadata[hint.input.id];
|
||||
if (m.value_field && document.getElementById(m.value_field))
|
||||
{
|
||||
vv = getSelectedIds(m.value_field);
|
||||
h = field_metadata[m.value_field].values[hint.input.id];
|
||||
}
|
||||
// Create null option, if applicable
|
||||
var o = [];
|
||||
var nullable = m.nullable && !m.multiple;
|
||||
if (m.null_field && nullable)
|
||||
{
|
||||
nullable = checkValueVisibility(getSelectedIds(m.null_field), field_metadata[m.null_field]['null'][hint.input.id]);
|
||||
}
|
||||
if (nullable && !l.length)
|
||||
{
|
||||
o.push([ '<span class="hintRealname">---</span>', '' ]);
|
||||
}
|
||||
// Fill options
|
||||
for (var i in m.legal)
|
||||
{
|
||||
var kw = m.legal[i];
|
||||
var vis = m.multiple && idh[kw[0]]; // always show selected values
|
||||
if (!vis && (!vv || checkValueVisibility(vv, h[kw[0]])))
|
||||
{
|
||||
for (j = 0; j < l.length; j++)
|
||||
{
|
||||
// show matching values
|
||||
if (kw[1].toLowerCase().indexOf(l[j]) >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
vis = !l.length || j < l.length;
|
||||
}
|
||||
if (vis)
|
||||
{
|
||||
o.push([
|
||||
'<span class="hintRealname">' + htmlspecialchars(kw[1]) + '</span>',
|
||||
kw[1], false, idh[kw[0]], kw[0]
|
||||
]);
|
||||
}
|
||||
}
|
||||
hint.replaceItems(o);
|
||||
}
|
||||
|
||||
function fieldHintMakeInput(field, id, name)
|
||||
{
|
||||
var a = document.getElementById('v'+item[4]+'_'+field.id);
|
||||
if (!a)
|
||||
{
|
||||
a = document.createElement('input');
|
||||
a.id = 'v'+item[4]+'_'+field.id;
|
||||
a.name = field.id;
|
||||
a.type = 'hidden';
|
||||
a.value = item[1];
|
||||
field.parentNode.insertBefore(a, field);
|
||||
}
|
||||
}
|
||||
|
||||
function fieldHintRemoveInput(field, id)
|
||||
{
|
||||
var a = document.getElementById('v'+id+'_'+field.id);
|
||||
if (a)
|
||||
{
|
||||
a.parentNode.removeChild(a);
|
||||
}
|
||||
}
|
||||
|
||||
function fieldHintChangeListener(hint, index, item)
|
||||
{
|
||||
var m = field_metadata[hint.input.id];
|
||||
var ids = hint.input.getAttribute('data-ids');
|
||||
if (m.multiple)
|
||||
{
|
||||
if (item[3])
|
||||
{
|
||||
hint.input.setAttribute('data-ids', ids+(ids.length ? ',' : '')+item[4]);
|
||||
fieldHintMakeInput(hint.input, item[4], item[1]);
|
||||
}
|
||||
else if (ids.length)
|
||||
{
|
||||
var a = ids.split(',');
|
||||
for (var i = a.length-1; i >= 0; i--)
|
||||
{
|
||||
if (a[i] == item[4])
|
||||
{
|
||||
a.splice(i, 1);
|
||||
}
|
||||
}
|
||||
hint.input.setAttribute('data-ids', a.join(','));
|
||||
fieldHintRemoveInput(hint.input, item[4]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hint.input.setAttribute('data-ids', item[4]);
|
||||
}
|
||||
handleControllerField_this.apply(hint.input);
|
||||
}
|
||||
|
||||
function initControlledField(id)
|
||||
{
|
||||
var f = document.getElementById(id);
|
||||
if (f && field_metadata[id].use_combobox)
|
||||
{
|
||||
var p = {};
|
||||
if (field_metadata[id].multiple)
|
||||
{
|
||||
p.multipleListener = fieldHintChangeListener;
|
||||
}
|
||||
else
|
||||
{
|
||||
p.onChangeListener = fieldHintChangeListener;
|
||||
}
|
||||
var s = new SimpleAutocomplete(f, fieldHintLoader, p);
|
||||
if (field_metadata[id].multiple)
|
||||
{
|
||||
s.show = function()
|
||||
{
|
||||
if (SimpleAutocomplete.prototype.show.apply(s))
|
||||
{
|
||||
f.value = '';
|
||||
s.onChange(true);
|
||||
}
|
||||
};
|
||||
s.hide = function()
|
||||
{
|
||||
if (SimpleAutocomplete.prototype.hide.apply(s))
|
||||
{
|
||||
var ids = f.getAttribute('data-ids');
|
||||
ids = ids.length ? ids.split(',') : [];
|
||||
for (var i = 0; i < ids.length; i++)
|
||||
ids[i] = document.getElementById('v'+ids[i]+'_'+f.id).value;
|
||||
f.value = ids.join(', ');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if (f && document.forms['Create'] && field_metadata[id].default_value)
|
||||
{
|
||||
// Check if anything is selected initially on the entry form
|
||||
|
@ -124,6 +280,51 @@ function setFieldValue(f, v)
|
|||
f.selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
else if (field_metadata[f.id].use_combobox)
|
||||
{
|
||||
// new values
|
||||
var a = v.length ? v.split(',') : [];
|
||||
var h = {};
|
||||
var nh = 0;
|
||||
var nn = [];
|
||||
for (var i in a)
|
||||
{
|
||||
h[a[i]] = true;
|
||||
nh++;
|
||||
}
|
||||
a = f.getAttribute('data-ids');
|
||||
a = a.length ? a.split(',') : [];
|
||||
var ids = {};
|
||||
for (var i in a)
|
||||
{
|
||||
ids[a[i]] = true;
|
||||
if (!h[a[i]])
|
||||
{
|
||||
fieldHintRemoveInput(f, a[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
nn.push(document.getElementById('v'+a[i]+'_'+f.id).value);
|
||||
delete h[a[i]];
|
||||
nh--;
|
||||
}
|
||||
}
|
||||
if (nh)
|
||||
{
|
||||
var l = field_metadata[f.id].legal;
|
||||
// FIXME: Not good to iterate over all values to find names for given IDs...
|
||||
for (var i in l)
|
||||
{
|
||||
if (h[l[i][0]])
|
||||
{
|
||||
nn.push(l[i][1]);
|
||||
fieldHintMakeInput(f, l[i][0], l[i][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
f.setAttribute('data-ids', v);
|
||||
f.value = nn.join(', ');
|
||||
}
|
||||
else if (f.type != 'hidden')
|
||||
{
|
||||
f.value = v;
|
||||
|
@ -157,6 +358,11 @@ function handleControlledField(controlled_id, is_initial_editform)
|
|||
// the bug may include incorrect values that must not be hidden initially.
|
||||
// Also remember, but do not select the default value in this case.
|
||||
}
|
||||
else if (m.use_combobox)
|
||||
{
|
||||
controlled.SimpleAutocomplete_input.onChange();
|
||||
return;
|
||||
}
|
||||
else if (controlled.nodeName == 'SELECT')
|
||||
{
|
||||
// Change select options
|
||||
|
@ -212,13 +418,13 @@ function handleControlledField(controlled_id, is_initial_editform)
|
|||
if (m.default_field && document.getElementById(m.default_field) && 0)
|
||||
{
|
||||
var diff = false;
|
||||
if (controlled._oldDefault)
|
||||
if (controlled._oldDefault !== undefined)
|
||||
{
|
||||
// Check if the value is different from previous default
|
||||
if (controlled.nodeName == 'SELECT')
|
||||
{
|
||||
var copt = getSelectedIds(controlled);
|
||||
for (var i in controlled._oldDefault)
|
||||
for (var i in controlled._oldDefault || {})
|
||||
{
|
||||
if (copt[controlled._oldDefault[i]])
|
||||
{
|
||||
|
@ -250,6 +456,7 @@ function handleControlledField(controlled_id, is_initial_editform)
|
|||
v = field_metadata[m.default_field]['defaults'][controlled_id][i];
|
||||
}
|
||||
}
|
||||
v = v || null;
|
||||
controlled._oldDefault = v;
|
||||
if (!diff && !is_initial_editform)
|
||||
{
|
||||
|
|
11
js/field.js
11
js/field.js
|
@ -289,6 +289,17 @@ function getSelectedIds(sel)
|
|||
var lm = sel.id.length+2;
|
||||
if (sel.nodeName != 'SELECT')
|
||||
{
|
||||
if (field_metadata[sel.id].use_combobox)
|
||||
{
|
||||
// IDs stored separately
|
||||
var ids = sel.getAttribute('data-ids');
|
||||
ids = ids.length ? ids.split(',') : [];
|
||||
for (var i = 0; i < ids.length; i++)
|
||||
{
|
||||
opt[ids[i]] = true;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
if (sel.name == 'product')
|
||||
{
|
||||
// product is a special case - it is preselected as hidden field on bug creation form
|
||||
|
|
17
js/hinter.js
17
js/hinter.js
|
@ -162,6 +162,7 @@ SimpleAutocomplete.prototype.init = function()
|
|||
// items = [ [ name, value ], [ name, value ], ... ]
|
||||
SimpleAutocomplete.prototype.replaceItems = function(items, append)
|
||||
{
|
||||
this.selectedOrig = null;
|
||||
if (!append)
|
||||
{
|
||||
this.hintLayer.scrollTop = 0;
|
||||
|
@ -189,10 +190,13 @@ SimpleAutocomplete.prototype.replaceItems = function(items, append)
|
|||
for (var i in items)
|
||||
items[i][3] = h[items[i][1]];
|
||||
}
|
||||
for (var i in items)
|
||||
for (var i = 0; i < items.length; i++)
|
||||
{
|
||||
this.hintLayer.appendChild(this.makeItem(this.items.length, items[i]));
|
||||
var d = this.makeItem(this.items.length, items[i]);
|
||||
this.hintLayer.appendChild(d);
|
||||
this.items.push(items[i]);
|
||||
if (items[i][3])
|
||||
this.selectedOrig = i;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -297,7 +301,7 @@ SimpleAutocomplete.prototype.getItem = function(index)
|
|||
index = this.selectedIndex;
|
||||
if (index < 0)
|
||||
return null;
|
||||
return document.getElementById(this.id+'_item_'+this.selectedIndex);
|
||||
return document.getElementById(this.id+'_item_'+index);
|
||||
};
|
||||
|
||||
// Select index'th item - change the input value and hide the hint if not a multi-select
|
||||
|
@ -317,6 +321,7 @@ SimpleAutocomplete.prototype.selectItem = function(index)
|
|||
if (!this.multipleDelimiter && !this.multipleListener)
|
||||
{
|
||||
this.input.value = this.items[index][1];
|
||||
this.selectedOrig = index;
|
||||
this.hide();
|
||||
}
|
||||
else
|
||||
|
@ -397,6 +402,12 @@ SimpleAutocomplete.prototype.show = function()
|
|||
this.hintLayer.style.right = (sw-p.left-this.input.offsetWidth)+'px';
|
||||
this.hintLayer.style.left = '';
|
||||
}
|
||||
if (this.selectedOrig !== null)
|
||||
{
|
||||
var o = this.getItem(this.selectedOrig);
|
||||
this.highlightItem(o);
|
||||
this.hintLayer.scrollTop = o.offsetTop;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -112,6 +112,34 @@ a:hover, #header a:hover, #footer a:hover {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
.combo_btn {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #b2b2b2;
|
||||
border-radius: 0 .25em .25em 0;
|
||||
box-shadow: inset 0 -1px rgba(0, 0, 0, 0.1);
|
||||
background: #f5f5f5;
|
||||
padding: 1px 6px;
|
||||
color: #606060;
|
||||
cursor: pointer;
|
||||
}
|
||||
.combo_btn:hover {
|
||||
color: #0095dd;
|
||||
}
|
||||
.combo_btn:active {
|
||||
box-shadow: inset 0 1px rgba(0, 0, 0, 0.1);
|
||||
top: 1px;
|
||||
position: relative;
|
||||
padding: 2px 6px 0px;
|
||||
}
|
||||
.combo_btn.combo_active {
|
||||
border-color: #42a4e0;
|
||||
-webkit-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
|
||||
-moz-box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
|
||||
box-shadow: 0 0 0 2px rgba(73,173,227,0.4);
|
||||
}
|
||||
|
||||
input[type=checkbox], input[type=radio] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -169,6 +169,22 @@
|
|||
);
|
||||
</script>
|
||||
[% CASE [ constants.FIELD_TYPE_SINGLE_SELECT, constants.FIELD_TYPE_MULTI_SELECT ] %]
|
||||
[% IF field.use_combobox %]
|
||||
[% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
|
||||
[% FOREACH v = bug.get_object(field.name) %]
|
||||
<input type="hidden" id="v[% v.id | html %]_[% field.name | html %]"
|
||||
name="[% field.name | html %]" value="[% v.name | html %]" />
|
||||
[% END %]
|
||||
[% END %]
|
||||
<input id="[% field.name | html %]" data-ids="[% bug.get_ids(field.name).join(',') | html %]"
|
||||
[% IF field.type == constants.FIELD_TYPE_SINGLE_SELECT %] name="[% field.name | html %]"[% END %]
|
||||
[% IF style %] style="[% style | html %]"[% END %]
|
||||
[% IF tabindex %] tabindex="[% tabindex | html %]"[% END %]
|
||||
size="40" value="[% bug.get_string(field.name) | html %]"
|
||||
style="border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-width: 0; z-index: 10"
|
||||
onfocus="this.nextSibling.className='combo_btn combo_active'" onblur="this.nextSibling.className='combo_btn'"
|
||||
/><span class="combo_btn" onclick="this.previousSibling.focus()">▾</span>
|
||||
[% ELSE %]
|
||||
<select id="[% field.name | html %]"
|
||||
[% IF style %] style="[% style | html %]"[% END %]
|
||||
[% IF tabindex %] tabindex="[% tabindex | html %]"[% END %]
|
||||
|
@ -229,7 +245,7 @@
|
|||
[% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
|
||||
<input type="hidden" name="defined_[% field.name | html %]" />
|
||||
[% END %]
|
||||
|
||||
[% END %]
|
||||
[% CASE constants.FIELD_TYPE_TEXTAREA %]
|
||||
[% INCLUDE global/textarea.html.tmpl
|
||||
id = field.name
|
||||
|
|
Loading…
Reference in New Issue