bugzilla-4intranet/js/query-visibility.js

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]);
}
}
}