Experimental implementation of select fields as hinted editboxes

hinted-selects
Vitaliy Filippov 2014-09-04 18:46:35 +04:00
parent 2638849b6d
commit ed5108ffe5
6 changed files with 283 additions and 6 deletions

View File

@ -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 => {},

View File

@ -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)
{

View File

@ -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

View File

@ -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;
}
};

View File

@ -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;
}

View File

@ -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()">&#x25be;</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