File: componentselect.js

package info (click to toggle)
trac-subcomponents 1.3.1-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 88 kB
  • sloc: python: 238; makefile: 4
file content (270 lines) | stat: -rw-r--r-- 8,170 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
 * Copyright 2006, John Drinkwater <john@nextraweb.com>
 * Distributed under the terms of the MIT License.
 * Please note, this script isn't as DOM friendly as I would like,
 * but IE, as always, has problems with <select> and <option>s
 * Version 1j, from http://ezri.nextraweb.com/examples/js/haiku/rev1j/componentselect.js
 * Thanks to wkornew & tic
 */

/*
 * Rewritten & maintained by Niels Sascha Reedijk <niels.reedijk@gmail.com>
 * Copyright 2012.
*/


function getLeafsForLevel(level, prefix, forceEmptyLeafs)
{
	var retVal = new Array();
	var previous = null;

	for (var i = 0; i < gComponentList.length; i++) {
		// Check if the current item has the right prefix
		if (gComponentList[i].join('/').substring(0, prefix.length) != prefix){
			continue;
		}

		var current = gComponentList[i][level];
		if (!current){
			// This item has the right prefix, but no value. This means that
			// it is an empty leaf
			current = "";
		}

		if (current == previous)
			// this item is already in the list
			continue;

		retVal.push(current);
		previous = current;
	}

	if (retVal.length == 0 || ( retVal.length == 1 && retVal[0] == "" ))
		// There are no entries, or the only entry is an empty leaf
		return new Array();

	if (forceEmptyLeafs && retVal[0] != "")
		retVal.unshift("")

	return retVal;
}

/*
	Event handler for when the user has changed the selected component
 */
function selectedComponentChanged(e) {
	// Get the level of this select
	var level = jQuery(this).prevAll('[class=haikucomponent]').length + 1;

	// Hide the deeper subcomponents (if applicable)
	jQuery(this).nextAll('[class=haikucomponent]').hide();

	// Check if the current selected value is an empty leaf
	if (jQuery(this).val().length == 0) {
		var prefix = ""
		jQuery(this).prevAll('[class=haikucomponent]').reverse()
			.each(function() {
				if (prefix.length != 0)
					prefix += "/"
				prefix += jQuery(this).val();
			});
		// Store the path of the previous leafs and end
		jQuery(this).parent().find("select:hidden").val(prefix).change();
		return;
	}

	// Just store the new path if this is the 'highest' level
	if (level == gMaxBranches) {
		var prefix = ""
		jQuery(this).parent().find('[class=haikucomponent]').each(function() {
			if (prefix.length != 0)
				prefix += "/";
			prefix += jQuery(this).val();
		});
		jQuery(this).parent().find("select:hidden").val(prefix).change();
		return;
	}

	// Empty the next selects
	jQuery(this).nextAll('[class=haikucomponent]').empty();

	// Fill the next selects with the right options. Sometimes that only means
	// that the next select is filled, at other times, we have to go deeper,
	// for example when there are no empty leaves for a subcomponent.

	var prefix = "";
	jQuery(this).prevAll('[class=haikucomponent]').reverse().each(function() {
		prefix += jQuery(this).val();
		prefix += "/";
	});
	prefix += jQuery(this).val();

	var currentSelector = jQuery(this);
	for (var i = level; i < gMaxBranches; i++) {
		var items = getLeafsForLevel(i, prefix, e.data.forceEmptyLeafs);

		for (j = 0; j < items.length; j++)
			currentSelector.next().append(jQuery("<option/>", {
				value: items[j],
				text: items[j]
			}));

		// If there are any entries in the select to the right, show it
		// otherwise break out of the loop as we are done
		if (items.length)
			currentSelector.next().show();
		else
			break;

		// If the next selector has an empty leaf, then we are done. Otherwise
		// go deeper
		if (items[0] == "")
			break;

		prefix += "/"
		prefix += items[0]
		currentSelector = currentSelector.next()
	}

	// Update the current selected value. The prefix string has the complete
	// path of the current selection
	jQuery(this).parent().find("select:hidden").val(prefix).change();
}


/*
	This function hides the <select> box and adds as many <select>s as the
	first/selected component has parts. The new elements have an event that
	triggers on change to add or remove <select> boxes.
	element: the <select> field that is to be visually replaced
	forceEmptyLeafs: add leafs that contain '', so users can pick super components
	                 used in the Query
*/
function convertComponentSelect(element, forceEmptyLeafs)
{
	var e = jQuery(element);
	var parent = jQuery(element).parent();

	gComponentCount ++;

	// Populate the global component list if it has not been populated before
	if ( gComponentList.length == 0 ) {
		var i = 0;
		e.find('option').each(function() {
			gComponentList[i] = jQuery(this).text().split('/');
			gMaxBranches = (gMaxBranches < gComponentList[i].length ? gComponentList[i].length : gMaxBranches);
			i++;
		});

		gComponentList.sort(); // so Trac can be lazy
	}

	// create some replacement dropdowns
	for (var i = 1; i <= gMaxBranches; i++) {
		parent.append(jQuery(document.createElement('select'))
			.attr('id', 'component-selector' + gComponentCount + '-' + i)
			.attr('class', 'haikucomponent')
			.change({forceEmptyLeafs: forceEmptyLeafs}, selectedComponentChanged));
	}

	// Store the current selected item
	var currentItems = e.val().split('/');
	var currentSelectors = parent.find('[class=haikucomponent]');
	var prefix = "";

	// Populate choice(s)
	// Note: always use currentItems.length + 1 because we want to check
	// whether there are more subselections possible
	for (var i = 0; i < currentItems.length + 1; i++) {
		var items = getLeafsForLevel(i, prefix, forceEmptyLeafs);

		for (j = 0; j < items.length; j++) {
			jQuery(currentSelectors[i]).append(jQuery("<option/>", {
				value: items[j],
				text: items[j]
			}));
		}

		if (items.length == 0) {
			break;
		}

		jQuery(currentSelectors[i]).val(currentItems[i]);

		// Add the current selected item to the prefix to prepare for the next
		// level
		if (prefix.length != 0)
			prefix += "/";
		if (currentItems[i] == "")
			// In case we have an 'emtpy' value, we only need to go once to get
			// the toplevel
			break;
		prefix += currentItems[i];
	}

	// Hide the unused inputs
	jQuery(currentSelectors[currentItems.length]).nextAll().hide();

	// Hide the highest input if there are no options
	if (jQuery(currentSelectors[currentItems.length]).children().length == 0)
		jQuery(currentSelectors[currentItems.length]).hide();

	// Hide the original component selector
	e.hide();
}

window.gComponentList = new Array( );
window.gMaxBranches = 0;
window.gComponentCount = 0;

/*
  We hook into the query page with this attached to the filter <select>
  We should be called after the component has been created if the browser
  follows the DOM way of thinking
 */
function convertQueryComponent() {
	jQuery('tr.component td.filter select').each(function () {
		if (this.name.match(/[0-9]+_component/g) )
			convertComponentSelect(this, true)
	});
}

function convertBatchModifyComponent() {
	jQuery('#batchmod_component td.batchmod_property select').each(function () {
		if (this.name == "batchmod_value_component")
			convertComponentSelect(this, false);
	});
}

function initialiseComponents() {
	// Query page: add filters
	if (jQuery('[id^=add_filter_]').length)
		jQuery('[id^=add_filter_]').change(convertQueryComponent);

	// Query page: batch modify
	if (jQuery('#add_batchmod_field').length)
		jQuery('#add_batchmod_field').change(convertBatchModifyComponent);

	// Query page: existing filters
	jQuery('tr.component td.filter select').each( function () {
		convertComponentSelect(this, true)
	});

	// Ticket/Newticket page: component field
	// Original comment: Opera picks up .names in getElementById(), hence it being at the end now
	if ( jQuery( '#field-component' ).length )
		convertComponentSelect( jQuery( '#field-component' )[0], false ); // For the new ticket page

	// Legacy: is this (still) necessary?
	// now we need to query any radio groups for Mozilla breakage: http://www.quirksmode.org/js/tests/moz_radios.html
	var brokMoz = jQuery('input[type="radio"]');
	if ( brokMoz.length > 0 ) {
		for (var i = 0; i < brokMoz.length; i++)
			{ if (brokMoz[i]) brokMoz[i].checked = brokMoz[i].defaultChecked; }
	}

	// Add the reverse function to jQuery, used by convertComponentSelect()
	jQuery.fn.reverse = [].reverse;
}

jQuery(document).ready(initialiseComponents);