adding all weblabels from weblabels.fsf.org
[weblabels.fsf.org.git] / www.fsf.org / 20131028 / files / widgets / js / keywordmultiselect.js
CommitLineData
5a920362 1/*jslint white:false, onevar:true, undef:true, nomen:true, eqeqeq:true, plusplus:true, bitwise:true, regexp:true, newcap:true, immed:true, strict:false, browser:true */
2/*
3// jQuery multiSelect
4//
5// Version 1.3
6//
7// Cory S.N. LaViska
8// A Beautiful Site (http://abeautifulsite.net/)
9// Visit http://abeautifulsite.net/notebook/62 for more information
10//
11// (Amended by Andy Richmond, Letters & Science Deans' Office, University of California, Davis)
12//
13// (Amended and trimmed for the Plone open source CMS by Matt Barkau, WebLion Group, PSU.edu)
14//
15// Usage: $('#control_id').multiSelect( arguments )
16//
17// Arguments:
18// noneSelected - text to display when there are no selected items in the list
19// oneOrMoreSelected - text to display when there are one or more selected items in the list
20// (note: you can use % as a placeholder for the number of items selected).
21// Use * to show a comma separated list of all selected; default = '% selected'
22//
23// Dependencies: jQuery 1.2.6 or higher (http://jquery.com/)
24//
25// Change Log:
26//
27// 1.0.1 - Updated to work with jQuery 1.2.6+ (no longer requires the dimensions plugin)
28// - Changed $(this).offset() to $(this).position(), per James' and Jono's suggestions
29//
30// 1.0.2 - Fixed issue where dropdown doesn't scroll up/down with keyboard shortcuts
31// - Changed '$' in setTimeout to use 'jQuery' to support jQuery.noConflict
32// - Renamed from jqueryMultiSelect.* to jquery.multiSelect.* per the standard recommended at
33// http://docs.jquery.com/Plugins/Authoring (does not affect API methods)
34//
35// 1.1.0 - Added the ability to update the options dynamically via javascript: optionsBoxUpdate(JSON)
36// - Added a title that displays the whole comma delimited list when using oneOrMoreSelected = *
37// - Moved some of the functions to be closured to make them private
38// - Changed the way the keyboard navigation worked to more closely match how a standard dropdown works
39// - ** by Andy Richmond **
40//
41// 1.2.1 - Fixed bug where input text overlapped dropdown arrow in IE (i.e. when using oneOrMoreSelected = *)
42// - ** by Andy Richmond **
43//
44// 1.2.2 - 06 January 2010 (per http://abeautifulsite.net/blog/2008/04/jquery-multiselect )
45// - Fixed bug where the keypress stopped showing the dropdown because in jQuery 1.3.2 they changed the way ':visible' works
46// - Fixed some other bugs in the way the keyboard interface worked
47// - Changed the main textbox to an <a> tag (with 'display: inline-block') to prevent the display text from being selected/highlighted
48// - Added the ability to jump to an option by typing the first character of that option (simular to a normal drop down)
49// - ** by Andy Richmond **
50// - Added [] to make each control submit an HTML array so $.serialize() works properly
51//
52// 1.3 - 2010 October-November
53// - Adapted for Plone open source CMS tag selector
54// - Fixed bug related to mouse hover when using arrow keys
55// - Improved many areas of keyboard support & accessibility
56// - see https://dev.plone.org/archetypes/log/Products.Archetypes/branches/plip11017-tag-selector-rmattb/Products/Archetypes/skins/archetypes/widgets/js/keywordmultiselect.js
57// - ** by Matt Barkau **
58//
59// Licensing & Terms of Use
60//
61// This plugin is dual-licensed under the GNU General Public License and the MIT License and
62// is copyright 2008 A Beautiful Site, LLC.
63//
64*/
65
66// Removed a test for jQuery, since we know it is available.
67(function($) {
68
69 // render the html for a single option
70 function renderOption(option, i, selectName) {
71 // dl, dt, & dd semantically associates selector name with values
72 // label makes the text clickable, like a multiple-select
73 var html = '<dd><label for="tag' + i + '"><input type="checkbox" name="' + selectName + '" value="' + option.value + '" id="tag' + i + '"';
74 if( option.selected ) {
75 html += ' checked="checked"';
76 }
77 html += ' />' + option.text + '</label></dd>';
78 return html;
79 }
80
81 // render the html for the options/optgroups
82 function renderOptions(options, selectName) {
83 var html = "";
84 for(var i = 0; i < options.length; i++) {
85 html += renderOption(options[i], i, selectName);
86 }
87 return html;
88 }
89
90
91// Plans to later modularize input and output handling,
92// for better testability, modularity, and accessibility.
93// // Handle mouse move input
94// // Handle mouse click input
95// // Handle key press input
96// // Detect navigation with mouse or non-tab keys
97// // Detect navigation with tab key
98// // Detect selection with mouse
99// // Detect selection with keyboard
100// // Handle navigation of options
101// // Handle selection of options
102
103
104 // Building the actual options
105 function buildOptions(options) {
106 var optionsBox = $(this);
107 var multiSelectA = optionsBox.next('.multiSelectA');
108
109 // Help text here is only relevant when there are many tags,
110 // so putting that in documentation, rather than here.
111 // "Hover and type the first letter to skip through tags."
112 $("#existingTagsHelp").text('');
113
114 // generate the html for the new options
115 html = renderOptions(options, optionsBox.attr('name'));
116 optionsBox.html(html);
117
118 // Format selected options
119 optionsBox.each( function() {
120 $(this).find('INPUT:checked').parent('LABEL').addClass('checked');
121 });
122
123 // Initialize selected options list
124 updateSelected.call(optionsBox);
125 var allOptions = optionsBox.find('LABEL');
126
127 // --- Navigation with Mouse ---
128 // Handle mouse hover of option, both
129 // entering an option, *and*
130 // mouse moving within an option.
131 optionsBox.find('LABEL').mousemove( function(e) {
132 // At this point, the browser is saying that the mouse moved.
133 // Workaround Safari's errant reporting of mousemove
134 // when the mouse hasn't moved, but background has.
135 // Initialize position variables.
136 if(multiSelectA.oldPositionX === null || multiSelectA.oldPositionY === null) {
137 multiSelectA.oldPositionX = e.pageX;
138 multiSelectA.oldPositionY = e.pageY;
139 }
140 if( multiSelectA.oldPositionX != e.pageX || multiSelectA.oldPositionY != e.pageY ) {
141 // At this point, the mouse actually did move.
142 // Highlight navigated option
143 $(this).parent().parent().find('LABEL').removeClass('hover'); // remove all highlights
144 $(this).addClass('hover'); // add the new highlight
145 lastNavTabKeyCheckbox = null;
146 multiSelectA.oldPositionX = e.pageX;
147 multiSelectA.oldPositionY = e.pageY;
148 multiSelectA.focus();
149 adjustViewPort(optionsBox);
150 }
151 });
152
153 // --- Selection with Mouse ---
154 // Handle mouse click of checkbox
155 optionsBox.find('INPUT:checkbox').click( function() {
156 // set the label checked class
157 $(this).parent('LABEL').toggleClass('checked', $(this).attr('checked'));
158
159 updateSelected.call(optionsBox);
160 // Highlight selected option
161 // placeholder
162 // Refocus
163 multiSelectA.focus();
164 // If this checkbox was navigated to with the tab key before being checked,
165 // then put focus back on it.
166 if(typeof(lastNavTabKeyCheckbox) !== "undefined" && lastNavTabKeyCheckbox !== null) {
167 lastNavTabKeyCheckbox.focus();
168 lastNavTabKeyCheckbox = null;
169 }
170 });
171
172 // --- Navigation with Tab Key ---
173 // Track mouse click of option
174 optionsBox.find('LABEL').mousedown(function() {
175 // Track mouse clicks,
176 // so that tab key navigation focus on checkboxes can be maintained separately.
177 lastNavClickTag = this;
178 });
179 // Handle tab-key focus of checkbox
180 optionsBox.find('INPUT').focus(function() {
181 if(typeof(lastNavClickTag) == "undefined" || lastNavClickTag === null) {
182 // This only happens with tab key navgation.
183 // Must keep track of this, because
184 // mouse-driven nav always keeps *focus* on multiSelectA,
185 // while the active optionsBox get *hover*.
186 // Tab navigation is different - it's active option checkbox gets *focus*,
187 // rather than *hover*, since keyboard navigation never hovers.
188 // If the checkbox is tabbed to & checked , save it so that focus can be put back on it.
189 // Without this, both moused & tabbed checks return focus to multiSelectA,
190 // causing tabbed checkboxes to lose focus.
191 lastNavTabKeyCheckbox = $(this);
192 // Highlight navigated option
193 lastNavTabKeyCheckbox.parent().parent().parent().find('LABEL').removeClass('hover');
194 lastNavTabKeyCheckbox.parent('LABEL').addClass('hover');
195 }
196 lastNavClickTag = null;
197 });
198
199 // Handle keyboard press
200 multiSelectA.keydown( function(e) {
201
202 var optionsBox = $(this).prev('.optionsBox');
203
204 // --- Navigation with Arrow or Page Keys ---
205 // Down || Up
206 if( e.keyCode == 40 || e.keyCode == 38) {
207 var oldHoverIndex = allOptions.index(allOptions.filter('.hover'));
208 var newHoverIndex = -1;
209
210 // if there is no current highlighted item then highlight the first item
211 if(oldHoverIndex < 0) {
212 // Default to first item
213 optionsBox.find('LABEL:first').addClass('hover');
214 }
215 // else if we are moving down and there is a next item then move
216 else if(e.keyCode == 40 && oldHoverIndex < allOptions.length - 1) {
217 newHoverIndex = oldHoverIndex + 1;
218 }
219 // else if we are moving up and there is a prev item then move
220 else if(e.keyCode == 38 && oldHoverIndex > 0) {
221 newHoverIndex = oldHoverIndex - 1;
222 }
223
224 if(newHoverIndex >= 0) {
225 // Highlight navigated option
226 $(allOptions).removeClass('hover'); // remove old highlights
227 $(allOptions.get(newHoverIndex)).addClass('hover'); // add the new highlight
228 lastNavTabKeyCheckbox = null;
229
230 // Adjust the viewport if necessary
231 adjustViewPort(optionsBox);
232 }
233
234 return false;
235 }
236 // Page up || Page down
237 if( e.keyCode == 33 || e.keyCode == 34) {
238 var oldHoverIndex = allOptions.index(allOptions.filter('.hover'));
239 var newHoverIndex = -1;
240 var optionsPerPage = 8; // depends on css
241 // if we are moving up and there is a prev item then move
242 if(e.keyCode == 33 && oldHoverIndex > 0) {
243 newHoverIndex = oldHoverIndex - optionsPerPage;
244 if(newHoverIndex < 0) {
245 newHoverIndex = 0;
246 }
247 }
248 if(e.keyCode == 34 && oldHoverIndex < allOptions.length - 1) {
249 newHoverIndex = oldHoverIndex + optionsPerPage;
250 if(newHoverIndex > allOptions.length - 1) {
251 newHoverIndex = allOptions.length - 1;
252 }
253 }
254 // Highlight navigated option
255 $(allOptions).removeClass('hover'); // remove all highlights
256 $(allOptions.get(newHoverIndex)).addClass('hover'); // add the new highlight
257 lastNavTabKeyCheckbox = null;
258 // Adjust the viewport if necessary
259 adjustViewPort(optionsBox);
260 return false;
261 }
262
263 // --- Selection with Keyboard ---
264 // Enter, Space
265 if( e.keyCode == 13 || e.keyCode == 32 ) {
266 var selectedCheckbox = optionsBox.find('LABEL.hover INPUT:checkbox');
267 // Set the checkbox (and label class)
268 selectedCheckbox.attr('checked', !selectedCheckbox.attr('checked')).parent('LABEL').toggleClass('checked', selectedCheckbox.attr('checked'));
269 // Highlight selected option
270 // placeholder
271 // Refocus
272 // placeholder
273 updateSelected.call(optionsBox);
274 return false;
275 }
276
277 // Any other standard keyboard character (try and match the first character of an option)
278 if( e.keyCode >= 33 && e.keyCode <= 126 ) {
279 // find the next matching item after the current hovered item
280 var match = optionsBox.find('LABEL:startsWith(' + String.fromCharCode(e.keyCode) + ')');
281
282 var currentHoverIndex = match.index(match.filter('LABEL.hover'));
283
284 // filter the set to any items after the current hovered item
285 var afterHoverMatch = match.filter(function (index) {
286 return index > currentHoverIndex;
287 });
288
289 // if there were no item after the current hovered item then try using the full search results (filtered to the first one)
290 match = (afterHoverMatch.length >= 1 ? afterHoverMatch : match).filter("LABEL:first");
291
292 if(match.length == 1) {
293 // if we found a match then move the hover
294 // Highlight navigated option
295 $(allOptions).removeClass('hover'); // remove all highlights
296 match.addClass('hover'); // add the new highlight
297 lastNavTabKeyCheckbox = null;
298
299 adjustViewPort(optionsBox);
300 }
301 }
302 // Prevent enter key from submitting form
303 if (e.keyCode == 13) {
304 return false;
305 }
306 });
307 }
308
309 // Scroll the viewport div if necessary
310 function adjustViewPort(optionsBox) {
311 // check for and move scrollbar down, content up
312 var hoverTop = optionsBox.find('LABEL.hover').position().top;
313 var hoverHeight = optionsBox.find('LABEL.hover').outerHeight();
314 var selectionBottom = hoverTop + hoverHeight;
315 // The integer 18 is a manual approximation for typical scale,
316 // since there's extra padding at the top of the div.optionsBox
317 // which is not showing up anywhere quantitatively.
318 // Could use improvement.
319 var optionsHeight = optionsBox.outerHeight() + 18;
320 var optionsScrollTop = optionsBox.scrollTop();
321 if(selectionBottom > optionsHeight) {
322 optionsBox.scrollTop(optionsScrollTop + selectionBottom - optionsHeight);
323 }
324
325 // check for and move scrollbar up, content down
326 var hoveredTop = optionsBox.find('LABEL.hover').position().top;
327 var optionsTop = optionsBox.position().top;
328 optionsScrollTop = optionsBox.scrollTop();
329 if(hoveredTop < optionsTop) {
330 optionsBox.scrollTop(optionsScrollTop + hoveredTop - optionsTop);
331 }
332 }
333
334 // Update heading with the total number of selected items
335 function updateSelected() {
336 var optionsBox = $(this);
337 var multiSelectA = optionsBox.next('.multiSelectA');
338 var i = 0;
339 var display = '';
340 optionsBox.find('INPUT:checkbox').not('.selectAll, .optGroup').each( function() {
341 if ($(this).attr('checked')) {
342 i++;
343 display = display +
344 '<p class="selectedTag"><span class="selectedTag">' +
345 $(this).parent().text() +
346 '</span></p>';
347 }
348 else {
349 selectAll = false;
350 }
351 });
352
353 if( i === 0 ) {
354 $("#selectedTagsHeading").html( $("#noTagsSelected").text() );
355 $("#selectedTags").text('');
356 } else {
357 $("#selectedTags").html( display );
358 $("#selectedTagsHeading").html( $("#oneOrMoreTagsSelected").text().replace('%', i) );
359 }
360 }
361
362 $.extend($.fn, {
363 multiSelect: function() {
364
365 // Initialize each optionsBox
366 $(this).each( function() {
367 var select = $(this);
368 var html = '';
369 // Overflow-y: auto enables the scrollbar, like a multiple-select
370 html += '<div class="optionsBox" tabindex="9999" style="overflow-y: auto;"></div>';
371 // Anchor originally used for dropdown.
372 // Will try to remove after refactoring to be more modular and testable with QUnit,
373 // although this element may need to stay to hold focus for mouse & arrow key navigation.
374 html += '<a href="javascript:;" class="multiSelectA" title="enable tag selector: tag selector is currently enabled"></a>';
375 // display:block makes the blank area right of the text clickable, like a multiple-select
376 html += '<style type="text/css">.ArchetypesKeywordWidget label {display: block;}</style>';
377 $(select).after(html);
378
379 var optionsBox = $(select).next('.optionsBox');
380 var multiSelectA = optionsBox.next('.multiSelectA');
381
382 // Serialize the select options into json options
383 var options = [];
384 $(select).children().each( function() {
385 if( $(this).val() !== '' ) {
386 options.push({ text: $(this).html(), value: $(this).val(), selected: $(this).attr('selected') });
387 }
388 });
389
390 // Eliminate the original form element
391 $(select).remove();
392
393 // Add the id & name that was on the original select element to the new div
394 optionsBox.attr("id", $(select).attr("id"));
395 optionsBox.attr("name", $(select).attr("name"));
396
397 // Build the dropdown options
398 buildOptions.call(optionsBox, options);
399
400 });
401 }
402
403 });
404
405 // add a new ":startsWith" search filter
406 $.expr[":"].startsWith = function(el, i, m) {
407 var search = m[3];
408 if (!search) {
409 return false;
410 }
411 return eval("/^[/s]*" + search + "/i").test($(el).text());
412 };
413
414})(jQuery);