-- Indentation fixes
[civicrm-core.git] / js / jquery / jquery.crmasmselect.js
CommitLineData
6a488035
TO
1/*
2 * Alternate Select Multiple (asmSelect) 1.0.4a beta - jQuery Plugin
3 * http://www.ryancramer.com/projects/asmselect/
e1f05567 4 *
6a488035 5 * Copyright (c) 2009 by Ryan Cramer - http://www.ryancramer.com
e1f05567 6 *
6a488035
TO
7 * Dual licensed under the MIT (MIT-LICENSE.txt)
8 * and GPL (GPL-LICENSE.txt) licenses.
9 *
10 */
11
12(function($) {
13
14 $.fn.crmasmSelect = function(customOptions) {
15
16 var options = {
17
18 listType: 'ul', // Ordered list 'ol', or unordered list 'ul'
19 sortable: false, // Should the list be sortable?
e1f05567 20 highlight: false, // Use the highlight feature?
6a488035
TO
21 animate: false, // Animate the the adding/removing of items in the list?
22 addItemTarget: 'bottom', // Where to place new selected items in list: top or bottom
23 hideWhenAdded: false, // Hide the option when added to the list? works only in FF
e1f05567 24 debugMode: false, // Debug mode keeps original select visible
6a488035
TO
25
26 removeLabel: 'X', // Text used in the "remove" link
27 highlightAddedLabel: 'Added: ', // Text that precedes highlight of added item
28 highlightRemovedLabel: 'Removed: ', // Text that precedes highlight of removed item
29
30 containerClass: 'crmasmContainer', // Class for container that wraps this widget
31 selectClass: 'crmasmSelect', // Class for the newly created <select>
32 optionDisabledClass: 'asmOptionDisabled', // Class for items that are already selected / disabled
33 listClass: 'crmasmList', // Class for the list ($ol)
34 listSortableClass: 'crmasmListSortable', // Another class given to the list when it is sortable
35 listItemClass: 'crmasmListItem', // Class for the <li> list items
36 listItemLabelClass: 'crmasmListItemLabel', // Class for the label text that appears in list items
37 removeClass: 'crmasmListItemRemove', // Class given to the "remove" link
38 highlightClass: 'crm asmHighlight', // Class given to the highlight <span>
39
40 respectParents: false // Force selection of "parent" option items
41 };
42
e1f05567 43 $.extend(options, customOptions);
6a488035
TO
44
45 return this.each(function(index) {
46
47 var $original = $(this); // the original select multiple
48 var $container; // a container that is wrapped around our widget
49 var $select; // the new select we have created
50 var $ol; // the list that we are manipulating
51 var buildingSelect = false; // is the new select being constructed right now?
52 var ieClick = false; // in IE, has a click event occurred? ignore if not
53 var ignoreOriginalChangeEvent = false; // originalChangeEvent bypassed when this is true
54
55 function init() {
56
57 // initialize the alternate select multiple
58
59 // this loop ensures uniqueness, in case of existing crmasmSelects placed by ajax (1.0.3)
2efcf0c2 60 while($("#" + options.containerClass + index).size() > 0) index++;
6a488035
TO
61
62 $select = $("<select></select>")
63 .addClass(options.selectClass)
64 .attr('name', options.selectClass + index)
2efcf0c2 65 .attr('id', options.selectClass + index);
6a488035 66
2efcf0c2 67 $selectRemoved = $("<select></select>");
6a488035
TO
68
69 $ol = $("<" + options.listType + "></" + options.listType + ">")
70 .addClass(options.listClass)
2efcf0c2 71 .attr('id', options.listClass + index);
6a488035
TO
72
73 $container = $("<div></div>")
74 .addClass(options.containerClass)
75 .attr('id', options.containerClass + index);
76
77 buildSelect();
78
79 $select.change(selectChangeEvent)
80 .click(selectClickEvent);
81
82 $original.change(originalChangeEvent)
83 .wrap($container).before($select).before($ol);
84
85 if(options.sortable) makeSortable();
86
87 if($.browser.msie && $.browser.version < 8) $ol.css('display', 'inline-block'); // Thanks Matthew Hutton
88 }
89
90 function makeSortable() {
91
92 // make any items in the selected list sortable
93 // requires jQuery UI sortables, draggables, droppables
94
95 $ol.sortable({
96 items: 'li.' + options.listItemClass,
97 handle: '.' + options.listItemLabelClass,
98 axis: 'y',
99 update: function(e, data) {
100
101 var updatedOptionId;
102
103 $(this).children("li").each(function(n) {
104
105 $option = $('#' + $(this).attr('rel'));
106
107 if($(this).is(".ui-sortable-helper")) {
108 updatedOptionId = $option.attr('id');
109 return;
110 }
111
112 $original.append($option);
113 });
114
115 if(updatedOptionId) triggerOriginalChange(updatedOptionId, 'sort');
116 }
117
118 }).addClass(options.listSortableClass);
119 }
120
121 function selectChangeEvent(e) {
122
123 // an item has been selected on the regular select we created
124 // check to make sure it's not an IE screwup, and add it to the list
125
126 if($.browser.msie && $.browser.version < 7 && !ieClick) return;
127 var id = $(this).children("option:selected").slice(0,1).attr('rel');
128 addListItem(id);
129 ieClick = false;
130 triggerOriginalChange(id, 'add'); // for use by user-defined callbacks
131 }
132
133 function selectClickEvent() {
134
135 // IE6 lets you scroll around in a select without it being pulled down
136 // making sure a click preceded the change() event reduces the chance
137 // if unintended items being added. there may be a better solution?
138
139 ieClick = true;
140 }
141
142 function originalChangeEvent(e) {
143
144 // select or option change event manually triggered
145 // on the original <select multiple>, so rebuild ours
146
147 if(ignoreOriginalChangeEvent) {
148 ignoreOriginalChangeEvent = false;
149 return;
150 }
151
152 $select.empty();
153 $ol.empty();
154 buildSelect();
155
156 // opera has an issue where it needs a force redraw, otherwise
157 // the items won't appear until something else forces a redraw
158 if($.browser.opera) $ol.hide().fadeIn("fast");
159 }
160
161 function buildSelect() {
162
163 // build or rebuild the new select that the user
164 // will select items from
165
166 buildingSelect = true;
167
168 // add a first option to be the home option / default selectLabel
169 $select.prepend("<option>" + $original.attr('title') + "</option>");
170
171 $original.children("option").each(function(n) {
172
173 var $t = $(this);
174 var id;
175
176 if(!$t.attr('id')) $t.attr('id', 'asm' + index + 'option' + n);
177 id = $t.attr('id');
178 classes = $t.attr('class');
179
180 if($t.is(":selected")) {
181 addListItem(id);
182 addSelectOption(id, classes, true);
183 } else {
184 addSelectOption(id,classes);
185 }
186 });
187
188 if(!options.debugMode) $original.hide(); // IE6 requires this on every buildSelect()
189 selectFirstItem();
190 buildingSelect = false;
191 }
192
193 function addSelectOption(optionId, optionClasses, disabled) {
194
195 // add an <option> to the <select>
196 // used only by buildSelect()
197
198 if(disabled == undefined) var disabled = false;
199
200 var $O = $('#' + optionId);
201 var $option = $("<option>" + $O.text() + "</option>")
202 .val($O.val())
203 .attr('rel', optionId)
204 .addClass(optionClasses);
205
206 if(disabled) disableSelectOption($option);
207
208 $select.append($option);
209 }
210
211 function selectFirstItem() {
212
213 // select the firm item from the regular select that we created
214
215 $select.children(":eq(0)").attr("selected", true);
216 }
217
218 function disableSelectOption($option) {
219
220 // make an option disabled, indicating that it's already been selected
221 // because safari is the only browser that makes disabled items look 'disabled'
222 // we apply a class that reproduces the disabled look in other browsers
223
224 $option.addClass(options.optionDisabledClass)
225 .attr("selected", false)
226 .attr("disabled", true);
227
228 if(options.hideWhenAdded) $option.hide();
229 if($.browser.msie) $select.hide().show(); // this forces IE to update display
230 }
231
232 function enableSelectOption($option) {
233
234 // given an already disabled select option, enable it
235
236 $option.removeClass(options.optionDisabledClass)
237 .attr("disabled", false);
238
239 if(options.hideWhenAdded) $option.show();
240 if($.browser.msie) $select.hide().show(); // this forces IE to update display
241 }
242
243 function addListItem(optionId) {
244
245 // add a new item to the html list
246
247 var $O = $('#' + optionId);
248
249 if(!$O) return; // this is the first item, selectLabel
250
251 var $removeLink = $("<a></a>")
252 .attr("href", "#")
253 .addClass(options.removeClass)
254 .prepend(options.removeLabel)
255 .click(function() {
256 dropListItem($(this).parent('li').attr('rel'));
257 return false;
258 });
259
260 var $itemLabel = $("<span></span>")
261 .addClass(options.listItemLabelClass)
262 .html($O.html());
263
264 var $item = $("<li></li>")
265 .attr('rel', optionId)
266 .addClass(options.listItemClass)
267 .addClass($O.attr('class'))
268 .append($itemLabel)
269 .append($removeLink)
270 .hide();
271
272 if(!buildingSelect) {
273 if($O.is(":selected")) return; // already have it
274 $O.attr('selected', true);
275 }
276
277 if(options.addItemTarget == 'top' && !buildingSelect) {
278 $ol.prepend($item);
279 if(options.sortable) $original.prepend($O);
280 } else {
281 $ol.append($item);
282 if(options.sortable) $original.append($O);
283 }
284
285 addListItemShow($item);
286
287 disableSelectOption($("[rel=" + optionId + "]", $select));
288
289 if(!buildingSelect) {
290 setHighlight($item, options.highlightAddedLabel);
291 selectFirstItem();
292 if(options.sortable) $ol.sortable("refresh");
293 }
294
295 if(options.respectParents) {
296
297 if($O.hasClass('child')) {
298 parentName = $O.attr('class').split('parent-')[1];
299 parentName = parentName.split(' ')[0];
300 parentId = $('.option-'+parentName).attr('rel');
301 addListItem(parentId);
302 };
303 };
304 }
305
306 function addListItemShow($item) {
307
308 // reveal the currently hidden item with optional animation
309 // used only by addListItem()
310
311 if(options.animate && !buildingSelect) {
312 $item.animate({
313 opacity: "show",
314 height: "show"
315 }, 100, "swing", function() {
316 $item.animate({
317 height: "+=2px"
318 }, 50, "swing", function() {
319 $item.animate({
320 height: "-=2px"
321 }, 25, "swing");
322 });
323 });
324 } else {
325 $item.show();
326 }
327 }
328
329 function dropListItem(optionId, highlightItem) {
330
331 // remove an item from the html list
332
333 if(highlightItem == undefined) var highlightItem = true;
334 var $O = $('#' + optionId);
335
336 $O.attr('selected', false);
337 $item = $ol.children("li[rel=" + optionId + "]");
338
339 dropListItemHide($item);
340 enableSelectOption($("[rel=" + optionId + "]", options.removeWhenAdded ? $selectRemoved : $select));
341
342 if(highlightItem) setHighlight($item, options.highlightRemovedLabel);
343
344 triggerOriginalChange(optionId, 'drop');
345
346 }
347
348 function dropListItemHide($item) {
349
350 // remove the currently visible item with optional animation
351 // used only by dropListItem()
352
353 if(options.animate && !buildingSelect) {
354
355 $prevItem = $item.prev("li");
356
357 $item.animate({
358 opacity: "hide",
359 height: "hide"
360 }, 100, "linear", function() {
361 $prevItem.animate({
362 height: "-=2px"
363 }, 50, "swing", function() {
364 $prevItem.animate({
365 height: "+=2px"
366 }, 100, "swing");
367 });
368 $item.remove();
369 });
370
371 } else {
372 $item.remove();
373 }
374 }
375
376 function setHighlight($item, label) {
377
378 // set the contents of the highlight area that appears
379 // directly after the <select> single
380 // fade it in quickly, then fade it out
381
382 if(!options.highlight) return;
383
384 $select.next("#" + options.highlightClass + index).remove();
385
386 var $highlight = $("<span></span>")
387 .hide()
388 .addClass(options.highlightClass)
389 .attr('id', options.highlightClass + index)
390 .html(label + $item.children("." + options.listItemLabelClass).slice(0,1).text());
391
392 $select.after($highlight);
393
394 $highlight.fadeIn("fast", function() {
395 setTimeout(function() { $highlight.fadeOut("slow"); }, 50);
396 });
397 }
398
399 function triggerOriginalChange(optionId, type) {
400
401 // trigger a change event on the original select multiple
402 // so that other scripts can pick them up
403
404 ignoreOriginalChangeEvent = true;
405 $option = $("#" + optionId);
406
407 $original.trigger('change', [{
408 'option': $option,
409 'value': $option.val(),
410 'id': optionId,
411 'item': $ol.children("[rel=" + optionId + "]"),
412 'type': type
2efcf0c2 413 }]);
6a488035
TO
414 }
415
416 init();
417 });
418 };
419
2efcf0c2 420})(jQuery);
421