Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
10121 manish.sha 1
/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */
2
/*
3
 * jQuery MultiSelect UI Widget 1.14pre
4
 * Copyright (c) 2012 Eric Hynds
5
 *
6
 * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
7
 *
8
 * Depends:
9
 *   - jQuery 1.4.2+
10
 *   - jQuery UI 1.8 widget factory
11
 *
12
 * Optional:
13
 *   - jQuery UI effects
14
 *   - jQuery UI position utility
15
 *
16
 * Dual licensed under the MIT and GPL licenses:
17
 *   http://www.opensource.org/licenses/mit-license.php
18
 *   http://www.gnu.org/licenses/gpl.html
19
 *
20
 */
21
(function($, undefined) {
22
 
23
  var multiselectID = 0;
24
  var $doc = $(document);
25
 
26
  $.widget("ech.multiselect", {
27
 
28
    // default options
29
    options: {
30
      header: true,
31
      height: 175,
32
      minWidth: 225,
33
      classes: '',
34
      checkAllText: 'Check all',
35
      uncheckAllText: 'Uncheck all',
36
      noneSelectedText: 'Select options',
37
      selectedText: '# selected',
38
      selectedList: 0,
39
      show: null,
40
      hide: null,
41
      autoOpen: false,
42
      multiple: true,
43
      position: {},
44
      appendTo: "body"
45
    },
46
 
47
    _create: function() {
48
      var el = this.element.hide();
49
      var o = this.options;
50
 
51
      this.speed = $.fx.speeds._default; // default speed for effects
52
      this._isOpen = false; // assume no
53
 
54
      // create a unique namespace for events that the widget
55
      // factory cannot unbind automatically. Use eventNamespace if on
56
      // jQuery UI 1.9+, and otherwise fallback to a custom string.
57
      this._namespaceID = this.eventNamespace || ('multiselect' + multiselectID);
58
 
59
      var button = (this.button = $('<button type="button"><span class="ui-icon ui-icon-triangle-1-s"></span></button>'))
60
        .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all')
61
        .addClass(o.classes)
62
        .attr({ 'title':el.attr('title'), 'aria-haspopup':true, 'tabIndex':el.attr('tabIndex') })
63
        .insertAfter(el),
64
 
65
        buttonlabel = (this.buttonlabel = $('<span />'))
66
          .html(o.noneSelectedText)
67
          .appendTo(button),
68
 
69
        menu = (this.menu = $('<div />'))
70
          .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all')
71
          .addClass(o.classes)
72
          .appendTo($(o.appendTo)),
73
 
74
        header = (this.header = $('<div />'))
75
          .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
76
          .appendTo(menu),
77
 
78
        headerLinkContainer = (this.headerLinkContainer = $('<ul />'))
79
          .addClass('ui-helper-reset')
80
          .html(function() {
81
            if(o.header === true) {
82
              return '<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>' + o.checkAllText + '</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>' + o.uncheckAllText + '</span></a></li>';
83
            } else if(typeof o.header === "string") {
84
              return '<li>' + o.header + '</li>';
85
            } else {
86
              return '';
87
            }
88
          })
89
          .append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>')
90
          .appendTo(header),
91
 
92
        checkboxContainer = (this.checkboxContainer = $('<ul />'))
93
          .addClass('ui-multiselect-checkboxes ui-helper-reset')
94
          .appendTo(menu);
95
 
96
        // perform event bindings
97
        this._bindEvents();
98
 
99
        // build menu
100
        this.refresh(true);
101
 
102
        // some addl. logic for single selects
103
        if(!o.multiple) {
104
          menu.addClass('ui-multiselect-single');
105
        }
106
 
107
        // bump unique ID
108
        multiselectID++;
109
    },
110
 
111
    _init: function() {
112
      if(this.options.header === false) {
113
        this.header.hide();
114
      }
115
      if(!this.options.multiple) {
116
        this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();
117
      }
118
      if(this.options.autoOpen) {
119
        this.open();
120
      }
121
      if(this.element.is(':disabled')) {
122
        this.disable();
123
      }
124
    },
125
 
126
    refresh: function(init) {
127
      var el = this.element;
128
      var o = this.options;
129
      var menu = this.menu;
130
      var checkboxContainer = this.checkboxContainer;
131
      var optgroups = [];
132
      var html = "";
133
      var id = el.attr('id') || multiselectID++; // unique ID for the label & option tags
134
 
135
      // build items
136
      el.find('option').each(function(i) {
137
        var $this = $(this);
138
        var parent = this.parentNode;
139
        var description = this.innerHTML;
140
        var title = this.title;
141
        var value = this.value;
142
        var inputID = 'ui-multiselect-' + (this.id || id + '-option-' + i);
143
        var isDisabled = this.disabled;
144
        var isSelected = this.selected;
145
        var labelClasses = [ 'ui-corner-all' ];
146
        var liClasses = (isDisabled ? 'ui-multiselect-disabled ' : ' ') + this.className;
147
        var optLabel;
148
 
149
        // is this an optgroup?
150
        if(parent.tagName === 'OPTGROUP') {
151
          optLabel = parent.getAttribute('label');
152
 
153
          // has this optgroup been added already?
154
          if($.inArray(optLabel, optgroups) === -1) {
155
            html += '<li class="ui-multiselect-optgroup-label ' + parent.className + '"><a href="#">' + optLabel + '</a></li>';
156
            optgroups.push(optLabel);
157
          }
158
        }
159
 
160
        if(isDisabled) {
161
          labelClasses.push('ui-state-disabled');
162
        }
163
 
164
        // browsers automatically select the first option
165
        // by default with single selects
166
        if(isSelected && !o.multiple) {
167
          labelClasses.push('ui-state-active');
168
        }
169
 
170
        html += '<li class="' + liClasses + '">';
171
 
172
        // create the label
173
        html += '<label for="' + inputID + '" title="' + title + '" class="' + labelClasses.join(' ') + '">';
174
        html += '<input id="' + inputID + '" name="multiselect_' + id + '" type="' + (o.multiple ? "checkbox" : "radio") + '" value="' + value + '" title="' + title + '"';
175
 
176
        // pre-selected?
177
        if(isSelected) {
178
          html += ' checked="checked"';
179
          html += ' aria-selected="true"';
180
        }
181
 
182
        // disabled?
183
        if(isDisabled) {
184
          html += ' disabled="disabled"';
185
          html += ' aria-disabled="true"';
186
        }
187
 
188
        // add the title and close everything off
189
        html += ' /><span>' + description + '</span></label></li>';
190
      });
191
 
192
      // insert into the DOM
193
      checkboxContainer.html(html);
194
 
195
      // cache some moar useful elements
196
      this.labels = menu.find('label');
197
      this.inputs = this.labels.children('input');
198
 
199
      // set widths
200
      this._setButtonWidth();
201
      this._setMenuWidth();
202
 
203
      // remember default value
204
      this.button[0].defaultValue = this.update();
205
 
206
      // broadcast refresh event; useful for widgets
207
      if(!init) {
208
        this._trigger('refresh');
209
      }
210
    },
211
 
212
    // updates the button text. call refresh() to rebuild
213
    update: function() {
214
      var o = this.options;
215
      var $inputs = this.inputs;
216
      var $checked = $inputs.filter(':checked');
217
      var numChecked = $checked.length;
218
      var value;
219
 
220
      if(numChecked === 0) {
221
        value = o.noneSelectedText;
222
      } else {
223
        if($.isFunction(o.selectedText)) {
224
          value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get());
225
        } else if(/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) {
226
          value = $checked.map(function() { return $(this).next().html(); }).get().join(', ');
227
        } else {
228
          value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length);
229
        }
230
      }
231
 
232
      this._setButtonValue(value);
233
 
234
      return value;
235
    },
236
 
237
    // this exists as a separate method so that the developer 
238
    // can easily override it.
239
    _setButtonValue: function(value) {
240
      this.buttonlabel.text(value);
241
    },
242
 
243
    // binds events
244
    _bindEvents: function() {
245
      var self = this;
246
      var button = this.button;
247
 
248
      function clickHandler() {
249
        self[ self._isOpen ? 'close' : 'open' ]();
250
        return false;
251
      }
252
 
253
      // webkit doesn't like it when you click on the span :(
254
      button
255
        .find('span')
256
        .bind('click.multiselect', clickHandler);
257
 
258
      // button events
259
      button.bind({
260
        click: clickHandler,
261
        keypress: function(e) {
262
          switch(e.which) {
263
            case 27: // esc
264
              case 38: // up
265
              case 37: // left
266
              self.close();
267
            break;
268
            case 39: // right
269
              case 40: // down
270
              self.open();
271
            break;
272
          }
273
        },
274
        mouseenter: function() {
275
          if(!button.hasClass('ui-state-disabled')) {
276
            $(this).addClass('ui-state-hover');
277
          }
278
        },
279
        mouseleave: function() {
280
          $(this).removeClass('ui-state-hover');
281
        },
282
        focus: function() {
283
          if(!button.hasClass('ui-state-disabled')) {
284
            $(this).addClass('ui-state-focus');
285
          }
286
        },
287
        blur: function() {
288
          $(this).removeClass('ui-state-focus');
289
        }
290
      });
291
 
292
      // header links
293
      this.header.delegate('a', 'click.multiselect', function(e) {
294
        // close link
295
        if($(this).hasClass('ui-multiselect-close')) {
296
          self.close();
297
 
298
          // check all / uncheck all
299
        } else {
300
          self[$(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll']();
301
        }
302
 
303
        e.preventDefault();
304
      });
305
 
306
      // optgroup label toggle support
307
      this.menu.delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function(e) {
308
        e.preventDefault();
309
 
310
        var $this = $(this);
311
        var $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)');
312
        var nodes = $inputs.get();
313
        var label = $this.parent().text();
314
 
315
        // trigger event and bail if the return is false
316
        if(self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false) {
317
          return;
318
        }
319
 
320
        // toggle inputs
321
        self._toggleChecked(
322
          $inputs.filter(':checked').length !== $inputs.length,
323
          $inputs
324
        );
325
 
326
        self._trigger('optgrouptoggle', e, {
327
          inputs: nodes,
328
          label: label,
329
          checked: nodes[0].checked
330
        });
331
      })
332
      .delegate('label', 'mouseenter.multiselect', function() {
333
        if(!$(this).hasClass('ui-state-disabled')) {
334
          self.labels.removeClass('ui-state-hover');
335
          $(this).addClass('ui-state-hover').find('input').focus();
336
        }
337
      })
338
      .delegate('label', 'keydown.multiselect', function(e) {
339
        e.preventDefault();
340
 
341
        switch(e.which) {
342
          case 9: // tab
343
            case 27: // esc
344
            self.close();
345
          break;
346
          case 38: // up
347
            case 40: // down
348
            case 37: // left
349
            case 39: // right
350
            self._traverse(e.which, this);
351
          break;
352
          case 13: // enter
353
            $(this).find('input')[0].click();
354
          break;
355
        }
356
      })
357
      .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function(e) {
358
        var $this = $(this);
359
        var val = this.value;
360
        var checked = this.checked;
361
        var tags = self.element.find('option');
362
 
363
        // bail if this input is disabled or the event is cancelled
364
        if(this.disabled || self._trigger('click', e, { value: val, text: this.title, checked: checked }) === false) {
365
          e.preventDefault();
366
          return;
367
        }
368
 
369
        // make sure the input has focus. otherwise, the esc key
370
        // won't close the menu after clicking an item.
371
        $this.focus();
372
 
373
        // toggle aria state
374
        $this.attr('aria-selected', checked);
375
 
376
        // change state on the original option tags
377
        tags.each(function() {
378
          if(this.value === val) {
379
            this.selected = checked;
380
          } else if(!self.options.multiple) {
381
            this.selected = false;
382
          }
383
        });
384
 
385
        // some additional single select-specific logic
386
        if(!self.options.multiple) {
387
          self.labels.removeClass('ui-state-active');
388
          $this.closest('label').toggleClass('ui-state-active', checked);
389
 
390
          // close menu
391
          self.close();
392
        }
393
 
394
        // fire change on the select box
395
        self.element.trigger("change");
396
 
397
        // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827
398
        // http://bugs.jquery.com/ticket/3827
399
        setTimeout($.proxy(self.update, self), 10);
400
      });
401
 
402
      // close each widget when clicking on any other element/anywhere else on the page
403
      $doc.bind('mousedown.' + this._namespaceID, function(event) {
404
        var target = event.target;
405
 
406
        if(self._isOpen
407
            && target !== self.button[0]
408
            && target !== self.menu[0]
409
            && !$.contains(self.menu[0], target)
410
            && !$.contains(self.button[0], target)
411
          ) {
412
          self.close();
413
        }
414
      });
415
 
416
      // deal with form resets.  the problem here is that buttons aren't
417
      // restored to their defaultValue prop on form reset, and the reset
418
      // handler fires before the form is actually reset.  delaying it a bit
419
      // gives the form inputs time to clear.
420
      $(this.element[0].form).bind('reset.multiselect', function() {
421
        setTimeout($.proxy(self.refresh, self), 10);
422
      });
423
    },
424
 
425
    // set button width
426
    _setButtonWidth: function() {
427
      var width = this.element.outerWidth();
428
      var o = this.options;
429
 
430
      if(/\d/.test(o.minWidth) && width < o.minWidth) {
431
        width = o.minWidth;
432
      }
433
 
434
      // set widths
435
      this.button.outerWidth(width);
436
    },
437
 
438
    // set menu width
439
    _setMenuWidth: function() {
440
      var m = this.menu;
441
      m.outerWidth(this.button.outerWidth());
442
    },
443
 
444
    // move up or down within the menu
445
    _traverse: function(which, start) {
446
      var $start = $(start);
447
      var moveToLast = which === 38 || which === 37;
448
 
449
      // select the first li that isn't an optgroup label / disabled
450
      var $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)').first();
451
 
452
      // if at the first/last element
453
      if(!$next.length) {
454
        var $container = this.menu.find('ul').last();
455
 
456
        // move to the first/last
457
        this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover');
458
 
459
        // set scroll position
460
        $container.scrollTop(moveToLast ? $container.height() : 0);
461
 
462
      } else {
463
        $next.find('label').trigger('mouseover');
464
      }
465
    },
466
 
467
    // This is an internal function to toggle the checked property and
468
    // other related attributes of a checkbox.
469
    //
470
    // The context of this function should be a checkbox; do not proxy it.
471
    _toggleState: function(prop, flag) {
472
      return function() {
473
        if(!this.disabled) {
474
          this[ prop ] = flag;
475
        }
476
 
477
        if(flag) {
478
          this.setAttribute('aria-selected', true);
479
        } else {
480
          this.removeAttribute('aria-selected');
481
        }
482
      };
483
    },
484
 
485
    _toggleChecked: function(flag, group) {
486
      var $inputs = (group && group.length) ?  group : this.inputs;
487
      var self = this;
488
 
489
      // toggle state on inputs
490
      $inputs.each(this._toggleState('checked', flag));
491
 
492
      // give the first input focus
493
      $inputs.eq(0).focus();
494
 
495
      // update button text
496
      this.update();
497
 
498
      // gather an array of the values that actually changed
499
      var values = $inputs.map(function() {
500
        return this.value;
501
      }).get();
502
 
503
      // toggle state on original option tags
504
      this.element
505
        .find('option')
506
        .each(function() {
507
          if(!this.disabled && $.inArray(this.value, values) > -1) {
508
            self._toggleState('selected', flag).call(this);
509
          }
510
        });
511
 
512
      // trigger the change event on the select
513
      if($inputs.length) {
514
        this.element.trigger("change");
515
      }
516
    },
517
 
518
    _toggleDisabled: function(flag) {
519
      this.button.attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
520
 
521
      var inputs = this.menu.find('input');
522
      var key = "ech-multiselect-disabled";
523
 
524
      if(flag) {
525
        // remember which elements this widget disabled (not pre-disabled)
526
        // elements, so that they can be restored if the widget is re-enabled.
527
        inputs = inputs.filter(':enabled').data(key, true)
528
      } else {
529
        inputs = inputs.filter(function() {
530
          return $.data(this, key) === true;
531
        }).removeData(key);
532
      }
533
 
534
      inputs
535
        .attr({ 'disabled':flag, 'arial-disabled':flag })
536
        .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled');
537
 
538
      this.element.attr({
539
        'disabled':flag,
540
        'aria-disabled':flag
541
      });
542
    },
543
 
544
    // open the menu
545
    open: function(e) {
546
      var self = this;
547
      var button = this.button;
548
      var menu = this.menu;
549
      var speed = this.speed;
550
      var o = this.options;
551
      var args = [];
552
 
553
      // bail if the multiselectopen event returns false, this widget is disabled, or is already open
554
      if(this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen) {
555
        return;
556
      }
557
 
558
      var $container = menu.find('ul').last();
559
      var effect = o.show;
560
 
561
      // figure out opening effects/speeds
562
      if($.isArray(o.show)) {
563
        effect = o.show[0];
564
        speed = o.show[1] || self.speed;
565
      }
566
 
567
      // if there's an effect, assume jQuery UI is in use
568
      // build the arguments to pass to show()
569
      if(effect) {
570
        args = [ effect, speed ];
571
      }
572
 
573
      // set the scroll of the checkbox container
574
      $container.scrollTop(0).height(o.height);
575
 
576
      // positon
577
      this.position();
578
 
579
      // show the menu, maybe with a speed/effect combo
580
      $.fn.show.apply(menu, args);
581
 
582
      // select the first not disabled option
583
      // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover
584
      // will actually trigger mouseenter.  the mouseenter trigger is there for when it's eventually fixed
585
      this.labels.filter(':not(.ui-state-disabled)').eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');
586
 
587
      button.addClass('ui-state-active');
588
      this._isOpen = true;
589
      this._trigger('open');
590
    },
591
 
592
    // close the menu
593
    close: function() {
594
      if(this._trigger('beforeclose') === false) {
595
        return;
596
      }
597
 
598
      var o = this.options;
599
      var effect = o.hide;
600
      var speed = this.speed;
601
      var args = [];
602
 
603
      // figure out opening effects/speeds
604
      if($.isArray(o.hide)) {
605
        effect = o.hide[0];
606
        speed = o.hide[1] || this.speed;
607
      }
608
 
609
      if(effect) {
610
        args = [ effect, speed ];
611
      }
612
 
613
      $.fn.hide.apply(this.menu, args);
614
      this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');
615
      this._isOpen = false;
616
      this._trigger('close');
617
    },
618
 
619
    enable: function() {
620
      this._toggleDisabled(false);
621
    },
622
 
623
    disable: function() {
624
      this._toggleDisabled(true);
625
    },
626
 
627
    checkAll: function(e) {
628
      this._toggleChecked(true);
629
      this._trigger('checkAll');
630
    },
631
 
632
    uncheckAll: function() {
633
      this._toggleChecked(false);
634
      this._trigger('uncheckAll');
635
    },
636
 
637
    getChecked: function() {
638
      return this.menu.find('input').filter(':checked');
639
    },
640
 
641
    destroy: function() {
642
      // remove classes + data
643
      $.Widget.prototype.destroy.call(this);
644
 
645
      // unbind events
646
      $doc.unbind(this._namespaceID);
647
 
648
      this.button.remove();
649
      this.menu.remove();
650
      this.element.show();
651
 
652
      return this;
653
    },
654
 
655
    isOpen: function() {
656
      return this._isOpen;
657
    },
658
 
659
    widget: function() {
660
      return this.menu;
661
    },
662
 
663
    getButton: function() {
664
      return this.button;
665
    },
666
 
667
    position: function() {
668
      var o = this.options;
669
 
670
      // use the position utility if it exists and options are specifified
671
      if($.ui.position && !$.isEmptyObject(o.position)) {
672
        o.position.of = o.position.of || this.button;
673
 
674
        this.menu
675
          .show()
676
          .position(o.position)
677
          .hide();
678
 
679
        // otherwise fallback to custom positioning
680
      } else {
681
        var pos = this.button.offset();
682
 
683
        this.menu.css({
684
          top: pos.top + this.button.outerHeight(),
685
          left: pos.left
686
        });
687
      }
688
    },
689
 
690
    // react to option changes after initialization
691
    _setOption: function(key, value) {
692
      var menu = this.menu;
693
 
694
      switch(key) {
695
        case 'header':
696
          menu.find('div.ui-multiselect-header')[value ? 'show' : 'hide']();
697
          break;
698
        case 'checkAllText':
699
          menu.find('a.ui-multiselect-all span').eq(-1).text(value);
700
          break;
701
        case 'uncheckAllText':
702
          menu.find('a.ui-multiselect-none span').eq(-1).text(value);
703
          break;
704
        case 'height':
705
          menu.find('ul').last().height(parseInt(value, 10));
706
          break;
707
        case 'minWidth':
708
          this.options[key] = parseInt(value, 10);
709
          this._setButtonWidth();
710
          this._setMenuWidth();
711
          break;
712
        case 'selectedText':
713
        case 'selectedList':
714
        case 'noneSelectedText':
715
          this.options[key] = value; // these all needs to update immediately for the update() call
716
          this.update();
717
          break;
718
        case 'classes':
719
          menu.add(this.button).removeClass(this.options.classes).addClass(value);
720
          break;
721
        case 'multiple':
722
          menu.toggleClass('ui-multiselect-single', !value);
723
          this.options.multiple = value;
724
          this.element[0].multiple = value;
725
          this.refresh();
726
          break;
727
        case 'position':
728
          this.position();
729
      }
730
 
731
      $.Widget.prototype._setOption.apply(this, arguments);
732
    }
733
  });
734
 
735
})(jQuery);