Commit | Line | Data |
---|---|---|
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 | ||
6f9cd76f | 215 | $select.children(":eq(0)").prop("selected", true); |
6a488035 TO |
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) | |
6f9cd76f CW |
225 | .prop("selected", false) |
226 | .prop("disabled", true); | |
6a488035 TO |
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) | |
6f9cd76f | 237 | .prop("disabled", false); |
6a488035 TO |
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 | |
6f9cd76f | 274 | $O.prop('selected', true); |
6a488035 TO |
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 | ||
6f9cd76f | 336 | $O.prop('selected', false); |
6a488035 TO |
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 |