298 lines
8.8 KiB
JavaScript
298 lines
8.8 KiB
JavaScript
/* License: Dual-license GPL 3.0+ or MPL 1.1+
|
|
* Contributor(s): Vitaliy Filippov <vitalif@mail.ru>
|
|
*/
|
|
|
|
/*
|
|
|
|
JavaScript code to hide and show values of select fields on the query form.
|
|
I.e. hide dependent values when their controlling value is hidden.
|
|
This code uses client-side cached field_metadata hash data generated by
|
|
fieldvaluecontrol.cgi.
|
|
|
|
See Bugzilla::Field::json_visibility for field_metadata format.
|
|
|
|
Important nuances of implementation:
|
|
|
|
Non-custom product-dependent Bugzilla fields (component, version, target_milestone)
|
|
values are not uniquely identified by their name. Many values with the same name
|
|
may exist in different products.
|
|
|
|
In theory such behavior is more convenient than one implemented in Bugzilla
|
|
custom fields, where each value has a unique name and may be enabled/disabled
|
|
for different products. So we need to support it and remove duplicate names
|
|
inside selectboxes.
|
|
|
|
But simply using names instead of IDs for filtering is insufficient: if
|
|
someday we'll have a custom field (say cf_1) depending on one of such "non-unique-value-name"
|
|
fields (say milestone), then selecting a product AND a milestone will display
|
|
the list of cf_1 values which are visible for ANY of milestones with selected name
|
|
ignoring selected product.
|
|
|
|
So we need to remember "which IDs of each name" are active now and filter
|
|
dependent values accordingly. I.e. when ONE product is selected, only ONE ID
|
|
could be present for each milestone displayed in selectbox.
|
|
|
|
We also need to preserve the sort order of dependent "non-unique-value-name" values,
|
|
respecting this "list of active IDs".
|
|
|
|
*/
|
|
|
|
onDomReady(initQueryformFields);
|
|
|
|
function initQueryformFields()
|
|
{
|
|
// Initialise fields in correct order (starting with top-most controller fields)
|
|
var initialised = {};
|
|
var doInit = function(f)
|
|
{
|
|
if (f && !initialised[f])
|
|
{
|
|
initialised[f] = true;
|
|
doInit(field_metadata[f].visibility_field);
|
|
doInit(field_metadata[f].value_field);
|
|
doInit(field_metadata[f].null_field);
|
|
initQueryformField(f);
|
|
}
|
|
};
|
|
for (var i in field_metadata)
|
|
{
|
|
doInit(i);
|
|
}
|
|
reflowFieldRows();
|
|
}
|
|
|
|
function initQueryformField(i)
|
|
{
|
|
var f = document.getElementById(i);
|
|
if (f)
|
|
{
|
|
handleQueryformControlledField(f);
|
|
addListener(f, 'change', handleQueryformField_this);
|
|
}
|
|
}
|
|
|
|
var _getIdCache = {};
|
|
|
|
// Get selected IDs of names selected in selectbox sel
|
|
function getQueryformSelectedIds(sel)
|
|
{
|
|
if (_getIdCache[sel.id])
|
|
{
|
|
return _getIdCache[sel.id];
|
|
}
|
|
var opt = {};
|
|
var a;
|
|
var has_selected;
|
|
var l2 = sel.id.length+4;
|
|
// No selection is equivalent to full selection
|
|
// We must include all options explicitly, because they may be already restricted
|
|
for (var i = 0; i < sel.options.length; i++)
|
|
{
|
|
if (sel.options[i].selected)
|
|
{
|
|
has_selected = true;
|
|
break;
|
|
}
|
|
}
|
|
// Iterate over all options
|
|
for (var i = 0; i < sel.options.length; i++)
|
|
{
|
|
if (sel.options[i].selected || !has_selected)
|
|
{
|
|
// IDs of options are qf_<SELECTBOX_ID>_1_2_3_...
|
|
// where 1_2_3 is the list of IDs mapped to this option name
|
|
// which are currently visible.
|
|
if (!sel.options[i].id)
|
|
{
|
|
opt['0'] = true;
|
|
}
|
|
else
|
|
{
|
|
a = sel.options[i].id.substr(l2).split('_');
|
|
for (var j in a)
|
|
{
|
|
opt[a[j]] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_getIdCache[sel.id] = opt;
|
|
return opt;
|
|
}
|
|
|
|
function handleQueryformControlledField(controlled)
|
|
{
|
|
if (!controlled)
|
|
{
|
|
return;
|
|
}
|
|
// Show/hide field
|
|
var m = field_metadata[controlled.id];
|
|
if (m.visibility_field)
|
|
{
|
|
var sel = getQueryformSelectedIds(document.getElementById(m.visibility_field));
|
|
var vis = checkValueVisibility(sel, field_metadata[m.visibility_field].fields[controlled.id]);
|
|
var cont = document.getElementById(controlled.id+'_cont');
|
|
if (cont)
|
|
{
|
|
cont.style.display = vis ? '' : 'none';
|
|
}
|
|
if (!vis)
|
|
{
|
|
if (controlled.nodeName == 'SELECT')
|
|
{
|
|
bz_clearOptions(controlled);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (controlled.nodeName != 'SELECT')
|
|
{
|
|
return;
|
|
}
|
|
// Change options
|
|
if (m.value_field || !controlled.options.length)
|
|
{
|
|
var sel = m.value_field ? getQueryformSelectedIds(document.getElementById(m.value_field)) : null;
|
|
// Save currently selected options
|
|
var controlled_selected = getSelectedValues(controlled);
|
|
bz_clearOptions(controlled);
|
|
// Loop over all legal values and remember currently
|
|
// visible IDs inside name2id preserving their original
|
|
// order using name2id_order
|
|
var name2id = {};
|
|
var name2id_order = [];
|
|
var vis_val = m.value_field ? field_metadata[m.value_field].values[controlled.id] : null;
|
|
var item;
|
|
for (var i in m.legal)
|
|
{
|
|
if (!vis_val || checkValueVisibility(sel, vis_val[m.legal[i][0]]))
|
|
{
|
|
// Value is visible
|
|
if (!name2id[m.legal[i][1]])
|
|
{
|
|
name2id[m.legal[i][1]] = [];
|
|
name2id_order.push(m.legal[i][1]);
|
|
}
|
|
name2id[m.legal[i][1]].push(m.legal[i][0]);
|
|
}
|
|
}
|
|
// Create NULL option if the field is nullable
|
|
if (name2id_order.length > 0 && m.nullable)
|
|
{
|
|
var nullable = true;
|
|
if (m.null_field)
|
|
{
|
|
var sel = getQueryformSelectedIds(document.getElementById(m.null_field));
|
|
nullable = checkValueVisibility(sel, field_metadata[m.null_field]['null'][controlled.id]);
|
|
}
|
|
if (nullable)
|
|
{
|
|
item = bz_createOptionInSelect(controlled, '---', '---');
|
|
if (controlled_selected['---'])
|
|
{
|
|
item.selected = true;
|
|
}
|
|
}
|
|
}
|
|
// Create options
|
|
for (var i in name2id_order)
|
|
{
|
|
i = name2id_order[i];
|
|
item = bz_createOptionInSelect(controlled, i, i);
|
|
/* Save IDs of values with the same name for correct cascade selection */
|
|
item.id = 'qf_'+controlled.id+'_'+name2id[i].join('_');
|
|
if (controlled_selected[i])
|
|
{
|
|
// Restore selection
|
|
item.selected = true;
|
|
}
|
|
}
|
|
// Hide fields without options
|
|
item = document.getElementById(controlled.id+'_cont');
|
|
if (item)
|
|
{
|
|
item.style.display = controlled.options.length ? '' : 'none';
|
|
}
|
|
}
|
|
// Just toggle empty value
|
|
else if (m.nullable && m.null_field)
|
|
{
|
|
var sel = getQueryformSelectedIds(document.getElementById(m.null_field));
|
|
var nullable = checkValueVisibility(sel, field_metadata[m.null_field]['null'][controlled.id]);
|
|
var has_null = controlled.options[0].value == '---';
|
|
if (nullable && !has_null)
|
|
{
|
|
controlled.insertBefore(new Option('---', '---'), controlled.options[0]);
|
|
}
|
|
else if (!nullable && has_null)
|
|
{
|
|
controlled.removeChild(controlled.options[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleQueryformField_this(e, nonfirst)
|
|
{
|
|
if (!nonfirst)
|
|
{
|
|
_getIdCache = {};
|
|
}
|
|
var m = field_metadata[this.id];
|
|
if (!m)
|
|
{
|
|
return;
|
|
}
|
|
var f = {};
|
|
for (var i in { 'fields': 1, 'values': 1, 'null': 1 })
|
|
{
|
|
for (var j in m[i])
|
|
{
|
|
f[j] = true;
|
|
}
|
|
}
|
|
for (var i in f)
|
|
{
|
|
handleQueryformControlledField(document.getElementById(i));
|
|
}
|
|
// Cascade events
|
|
for (var i in f)
|
|
{
|
|
handleQueryformField_this.apply(document.getElementById(i), [ null, true ]);
|
|
}
|
|
if (!nonfirst)
|
|
{
|
|
reflowFieldRows();
|
|
}
|
|
}
|
|
|
|
function reflowFieldRows()
|
|
{
|
|
var per_row = 4;
|
|
var rows = [];
|
|
var fields = [];
|
|
var visible = 0;
|
|
for (var i = 0, e; e = document.getElementById('cf_row_'+i); i++)
|
|
{
|
|
rows.push(e.rows[0]);
|
|
for (var j = 0; j < e.rows[0].cells.length; j++)
|
|
{
|
|
var v = e.rows[0].cells[j].style.display != 'none' ? 1 : 0;
|
|
fields.push([ e.rows[0].cells[j], v ]);
|
|
visible += v;
|
|
}
|
|
}
|
|
for (var cur_row = 0, j = 0; cur_row < rows.length && j < fields.length; cur_row++)
|
|
{
|
|
var v = 0;
|
|
for (; j < fields.length; v += fields[j][1], j++)
|
|
{
|
|
if (v >= per_row && fields[j][1])
|
|
{
|
|
break;
|
|
}
|
|
rows[cur_row].appendChild(fields[j][0]);
|
|
}
|
|
}
|
|
}
|