diff --git a/app/static/multiselect/css/multi-select.css b/app/static/multiselect/css/multi-select.css new file mode 100644 index 0000000..8b3bb5e --- /dev/null +++ b/app/static/multiselect/css/multi-select.css @@ -0,0 +1,93 @@ +.ms-container{ + background: transparent url('../img/switch.png') no-repeat 50% 50%; + width: 370px; +} + +.ms-container:after{ + content: "."; + display: block; + height: 0; + line-height: 0; + font-size: 0; + clear: both; + min-height: 0; + visibility: hidden; +} + +.ms-container .ms-selectable, .ms-container .ms-selection{ + background: #fff; + color: #555555; + float: left; + width: 45%; +} +.ms-container .ms-selection{ + float: right; +} + +.ms-container .ms-list{ + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + position: relative; + height: 200px; + padding: 0; + overflow-y: auto; +} + +.ms-container .ms-list.ms-focus{ + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + outline: 0; + outline: thin dotted \9; +} + +.ms-container ul{ + margin: 0; + list-style-type: none; + padding: 0; +} + +.ms-container .ms-optgroup-container{ + width: 100%; +} + +.ms-container .ms-optgroup-label{ + margin: 0; + padding: 5px 0px 0px 5px; + cursor: pointer; + color: #999; +} + +.ms-container .ms-selectable li.ms-elem-selectable, +.ms-container .ms-selection li.ms-elem-selection{ + border-bottom: 1px #eee solid; + padding: 2px 10px; + color: #555; + font-size: 14px; +} + +.ms-container .ms-selectable li.ms-hover, +.ms-container .ms-selection li.ms-hover{ + cursor: pointer; + color: #fff; + text-decoration: none; + background-color: #08c; +} + +.ms-container .ms-selectable li.disabled, +.ms-container .ms-selection li.disabled{ + background-color: #eee; + color: #aaa; + cursor: text; +} \ No newline at end of file diff --git a/app/static/multiselect/img/switch.png b/app/static/multiselect/img/switch.png new file mode 100644 index 0000000..784b0c7 Binary files /dev/null and b/app/static/multiselect/img/switch.png differ diff --git a/app/static/multiselect/js/jquery.multi-select.js b/app/static/multiselect/js/jquery.multi-select.js new file mode 100644 index 0000000..fb478f5 --- /dev/null +++ b/app/static/multiselect/js/jquery.multi-select.js @@ -0,0 +1,536 @@ +/* +* MultiSelect v0.9.12 +* Copyright (c) 2012 Louis Cuny +* +* This program is free software. It comes without any warranty, to +* the extent permitted by applicable law. You can redistribute it +* and/or modify it under the terms of the Do What The Fuck You Want +* To Public License, Version 2, as published by Sam Hocevar. See +* http://sam.zoy.org/wtfpl/COPYING for more details. +*/ + +!function ($) { + + "use strict"; + + + /* MULTISELECT CLASS DEFINITION + * ====================== */ + + var MultiSelect = function (element, options) { + this.options = options; + this.$element = $(element); + this.$container = $('
', { 'class': "ms-container" }); + this.$selectableContainer = $('
', { 'class': 'ms-selectable' }); + this.$selectionContainer = $('
', { 'class': 'ms-selection' }); + this.$selectableUl = $('
    ', { 'class': "ms-list", 'tabindex' : '-1' }); + this.$selectionUl = $('
      ', { 'class': "ms-list", 'tabindex' : '-1' }); + this.scrollTo = 0; + this.elemsSelector = 'li:visible:not(.ms-optgroup-label,.ms-optgroup-container,.'+options.disabledClass+')'; + }; + + MultiSelect.prototype = { + constructor: MultiSelect, + + init: function(){ + var that = this, + ms = this.$element; + + if (ms.next('.ms-container').length === 0){ + ms.css({ position: 'absolute', left: '-9999px' }); + ms.attr('id', ms.attr('id') ? ms.attr('id') : Math.ceil(Math.random()*1000)+'multiselect'); + this.$container.attr('id', 'ms-'+ms.attr('id')); + this.$container.addClass(that.options.cssClass); + ms.find('option').each(function(){ + that.generateLisFromOption(this); + }); + + this.$selectionUl.find('.ms-optgroup-label').hide(); + + if (that.options.selectableHeader){ + that.$selectableContainer.append(that.options.selectableHeader); + } + that.$selectableContainer.append(that.$selectableUl); + if (that.options.selectableFooter){ + that.$selectableContainer.append(that.options.selectableFooter); + } + + if (that.options.selectionHeader){ + that.$selectionContainer.append(that.options.selectionHeader); + } + that.$selectionContainer.append(that.$selectionUl); + if (that.options.selectionFooter){ + that.$selectionContainer.append(that.options.selectionFooter); + } + + that.$container.append(that.$selectableContainer); + that.$container.append(that.$selectionContainer); + ms.after(that.$container); + + that.activeMouse(that.$selectableUl); + that.activeKeyboard(that.$selectableUl); + + var action = that.options.dblClick ? 'dblclick' : 'click'; + + that.$selectableUl.on(action, '.ms-elem-selectable', function(){ + that.select($(this).data('ms-value')); + }); + that.$selectionUl.on(action, '.ms-elem-selection', function(){ + that.deselect($(this).data('ms-value')); + }); + + that.activeMouse(that.$selectionUl); + that.activeKeyboard(that.$selectionUl); + + ms.on('focus', function(){ + that.$selectableUl.focus(); + }); + } + + var selectedValues = ms.find('option:selected').map(function(){ return $(this).val(); }).get(); + that.select(selectedValues, 'init'); + + if (typeof that.options.afterInit === 'function') { + that.options.afterInit.call(this, this.$container); + } + }, + + 'generateLisFromOption' : function(option, index, $container){ + var that = this, + ms = that.$element, + attributes = "", + $option = $(option); + + for (var cpt = 0; cpt < option.attributes.length; cpt++){ + var attr = option.attributes[cpt]; + + if(attr.name !== 'value' && attr.name !== 'disabled'){ + attributes += attr.name+'="'+attr.value+'" '; + } + } + var selectableLi = $('
    • '+that.escapeHTML($option.text())+'
    • '), + selectedLi = selectableLi.clone(), + value = $option.val(), + elementId = that.sanitize(value); + + selectableLi + .data('ms-value', value) + .addClass('ms-elem-selectable') + .attr('id', elementId+'-selectable'); + + selectedLi + .data('ms-value', value) + .addClass('ms-elem-selection') + .attr('id', elementId+'-selection') + .hide(); + + if ($option.prop('disabled') || ms.prop('disabled')){ + selectedLi.addClass(that.options.disabledClass); + selectableLi.addClass(that.options.disabledClass); + } + + var $optgroup = $option.parent('optgroup'); + + if ($optgroup.length > 0){ + var optgroupLabel = $optgroup.attr('label'), + optgroupId = that.sanitize(optgroupLabel), + $selectableOptgroup = that.$selectableUl.find('#optgroup-selectable-'+optgroupId), + $selectionOptgroup = that.$selectionUl.find('#optgroup-selection-'+optgroupId); + + if ($selectableOptgroup.length === 0){ + var optgroupContainerTpl = '
    • ', + optgroupTpl = '
      • '+optgroupLabel+'
      '; + + $selectableOptgroup = $(optgroupContainerTpl); + $selectionOptgroup = $(optgroupContainerTpl); + $selectableOptgroup.attr('id', 'optgroup-selectable-'+optgroupId); + $selectionOptgroup.attr('id', 'optgroup-selection-'+optgroupId); + $selectableOptgroup.append($(optgroupTpl)); + $selectionOptgroup.append($(optgroupTpl)); + if (that.options.selectableOptgroup){ + $selectableOptgroup.find('.ms-optgroup-label').on('click', function(){ + var values = $optgroup.children(':not(:selected, :disabled)').map(function(){ return $(this).val();}).get(); + that.select(values); + }); + $selectionOptgroup.find('.ms-optgroup-label').on('click', function(){ + var values = $optgroup.children(':selected:not(:disabled)').map(function(){ return $(this).val();}).get(); + that.deselect(values); + }); + } + that.$selectableUl.append($selectableOptgroup); + that.$selectionUl.append($selectionOptgroup); + } + index = index === undefined ? $selectableOptgroup.find('ul').children().length : index + 1; + selectableLi.insertAt(index, $selectableOptgroup.children()); + selectedLi.insertAt(index, $selectionOptgroup.children()); + } else { + index = index === undefined ? that.$selectableUl.children().length : index; + + selectableLi.insertAt(index, that.$selectableUl); + selectedLi.insertAt(index, that.$selectionUl); + } + }, + + 'addOption' : function(options){ + var that = this; + + if (options.value !== undefined && options.value !== null){ + options = [options]; + } + $.each(options, function(index, option){ + if (option.value !== undefined && option.value !== null && + that.$element.find("option[value='"+option.value+"']").length === 0){ + var $option = $(''), + index = parseInt((typeof option.index === 'undefined' ? that.$element.children().length : option.index)), + $container = option.nested === undefined ? that.$element : $("optgroup[label='"+option.nested+"']"); + + $option.insertAt(index, $container); + that.generateLisFromOption($option.get(0), index, option.nested); + } + }); + }, + + 'escapeHTML' : function(text){ + return $("
      ").text(text).html(); + }, + + 'activeKeyboard' : function($list){ + var that = this; + + $list.on('focus', function(){ + $(this).addClass('ms-focus'); + }) + .on('blur', function(){ + $(this).removeClass('ms-focus'); + }) + .on('keydown', function(e){ + switch (e.which) { + case 40: + case 38: + e.preventDefault(); + e.stopPropagation(); + that.moveHighlight($(this), (e.which === 38) ? -1 : 1); + return; + case 37: + case 39: + e.preventDefault(); + e.stopPropagation(); + that.switchList($list); + return; + case 9: + if(that.$element.is('[tabindex]')){ + e.preventDefault(); + var tabindex = parseInt(that.$element.attr('tabindex'), 10); + tabindex = (e.shiftKey) ? tabindex-1 : tabindex+1; + $('[tabindex="'+(tabindex)+'"]').focus(); + return; + }else{ + if(e.shiftKey){ + that.$element.trigger('focus'); + } + } + } + if($.inArray(e.which, that.options.keySelect) > -1){ + e.preventDefault(); + e.stopPropagation(); + that.selectHighlighted($list); + return; + } + }); + }, + + 'moveHighlight': function($list, direction){ + var $elems = $list.find(this.elemsSelector), + $currElem = $elems.filter('.ms-hover'), + $nextElem = null, + elemHeight = $elems.first().outerHeight(), + containerHeight = $list.height(), + containerSelector = '#'+this.$container.prop('id'); + + $elems.removeClass('ms-hover'); + if (direction === 1){ // DOWN + + $nextElem = $currElem.nextAll(this.elemsSelector).first(); + if ($nextElem.length === 0){ + var $optgroupUl = $currElem.parent(); + + if ($optgroupUl.hasClass('ms-optgroup')){ + var $optgroupLi = $optgroupUl.parent(), + $nextOptgroupLi = $optgroupLi.next(':visible'); + + if ($nextOptgroupLi.length > 0){ + $nextElem = $nextOptgroupLi.find(this.elemsSelector).first(); + } else { + $nextElem = $elems.first(); + } + } else { + $nextElem = $elems.first(); + } + } + } else if (direction === -1){ // UP + + $nextElem = $currElem.prevAll(this.elemsSelector).first(); + if ($nextElem.length === 0){ + var $optgroupUl = $currElem.parent(); + + if ($optgroupUl.hasClass('ms-optgroup')){ + var $optgroupLi = $optgroupUl.parent(), + $prevOptgroupLi = $optgroupLi.prev(':visible'); + + if ($prevOptgroupLi.length > 0){ + $nextElem = $prevOptgroupLi.find(this.elemsSelector).last(); + } else { + $nextElem = $elems.last(); + } + } else { + $nextElem = $elems.last(); + } + } + } + if ($nextElem.length > 0){ + $nextElem.addClass('ms-hover'); + var scrollTo = $list.scrollTop() + $nextElem.position().top - + containerHeight / 2 + elemHeight / 2; + + $list.scrollTop(scrollTo); + } + }, + + 'selectHighlighted' : function($list){ + var $elems = $list.find(this.elemsSelector), + $highlightedElem = $elems.filter('.ms-hover').first(); + + if ($highlightedElem.length > 0){ + if ($list.parent().hasClass('ms-selectable')){ + this.select($highlightedElem.data('ms-value')); + } else { + this.deselect($highlightedElem.data('ms-value')); + } + $elems.removeClass('ms-hover'); + } + }, + + 'switchList' : function($list){ + $list.blur(); + this.$container.find(this.elemsSelector).removeClass('ms-hover'); + if ($list.parent().hasClass('ms-selectable')){ + this.$selectionUl.focus(); + } else { + this.$selectableUl.focus(); + } + }, + + 'activeMouse' : function($list){ + var that = this; + + this.$container.on('mouseenter', that.elemsSelector, function(){ + $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover'); + $(this).addClass('ms-hover'); + }); + + this.$container.on('mouseleave', that.elemsSelector, function () { + $(this).parents('.ms-container').find(that.elemsSelector).removeClass('ms-hover'); + }); + }, + + 'refresh' : function() { + this.destroy(); + this.$element.multiSelect(this.options); + }, + + 'destroy' : function(){ + $("#ms-"+this.$element.attr("id")).remove(); + this.$element.off('focus'); + this.$element.css('position', '').css('left', ''); + this.$element.removeData('multiselect'); + }, + + 'select' : function(value, method){ + if (typeof value === 'string'){ value = [value]; } + + var that = this, + ms = this.$element, + msIds = $.map(value, function(val){ return(that.sanitize(val)); }), + selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable').filter(':not(.'+that.options.disabledClass+')'), + selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection').filter(':not(.'+that.options.disabledClass+')'), + options = ms.find('option:not(:disabled)').filter(function(){ return($.inArray(this.value, value) > -1); }); + + if (method === 'init'){ + selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'), + selections = this.$selectionUl.find('#' + msIds.join('-selection, #') + '-selection'); + } + + if (selectables.length > 0){ + selectables.addClass('ms-selected').hide(); + selections.addClass('ms-selected').show(); + + options.prop('selected', true); + + that.$container.find(that.elemsSelector).removeClass('ms-hover'); + + var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container'); + if (selectableOptgroups.length > 0){ + selectableOptgroups.each(function(){ + var selectablesLi = $(this).find('.ms-elem-selectable'); + if (selectablesLi.length === selectablesLi.filter('.ms-selected').length){ + $(this).find('.ms-optgroup-label').hide(); + } + }); + + var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container'); + selectionOptgroups.each(function(){ + var selectionsLi = $(this).find('.ms-elem-selection'); + if (selectionsLi.filter('.ms-selected').length > 0){ + $(this).find('.ms-optgroup-label').show(); + } + }); + } else { + if (that.options.keepOrder && method !== 'init'){ + var selectionLiLast = that.$selectionUl.find('.ms-selected'); + if((selectionLiLast.length > 1) && (selectionLiLast.last().get(0) != selections.get(0))) { + selections.insertAfter(selectionLiLast.last()); + } + } + } + if (method !== 'init'){ + ms.trigger('change'); + if (typeof that.options.afterSelect === 'function') { + that.options.afterSelect.call(this, value); + } + } + } + }, + + 'deselect' : function(value){ + if (typeof value === 'string'){ value = [value]; } + + var that = this, + ms = this.$element, + msIds = $.map(value, function(val){ return(that.sanitize(val)); }), + selectables = this.$selectableUl.find('#' + msIds.join('-selectable, #')+'-selectable'), + selections = this.$selectionUl.find('#' + msIds.join('-selection, #')+'-selection').filter('.ms-selected').filter(':not(.'+that.options.disabledClass+')'), + options = ms.find('option').filter(function(){ return($.inArray(this.value, value) > -1); }); + + if (selections.length > 0){ + selectables.removeClass('ms-selected').show(); + selections.removeClass('ms-selected').hide(); + options.prop('selected', false); + + that.$container.find(that.elemsSelector).removeClass('ms-hover'); + + var selectableOptgroups = that.$selectableUl.children('.ms-optgroup-container'); + if (selectableOptgroups.length > 0){ + selectableOptgroups.each(function(){ + var selectablesLi = $(this).find('.ms-elem-selectable'); + if (selectablesLi.filter(':not(.ms-selected)').length > 0){ + $(this).find('.ms-optgroup-label').show(); + } + }); + + var selectionOptgroups = that.$selectionUl.children('.ms-optgroup-container'); + selectionOptgroups.each(function(){ + var selectionsLi = $(this).find('.ms-elem-selection'); + if (selectionsLi.filter('.ms-selected').length === 0){ + $(this).find('.ms-optgroup-label').hide(); + } + }); + } + ms.trigger('change'); + if (typeof that.options.afterDeselect === 'function') { + that.options.afterDeselect.call(this, value); + } + } + }, + + 'select_all' : function(){ + var ms = this.$element, + values = ms.val(); + + ms.find('option:not(":disabled")').prop('selected', true); + this.$selectableUl.find('.ms-elem-selectable').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').hide(); + this.$selectionUl.find('.ms-optgroup-label').show(); + this.$selectableUl.find('.ms-optgroup-label').hide(); + this.$selectionUl.find('.ms-elem-selection').filter(':not(.'+this.options.disabledClass+')').addClass('ms-selected').show(); + this.$selectionUl.focus(); + ms.trigger('change'); + if (typeof this.options.afterSelect === 'function') { + var selectedValues = $.grep(ms.val(), function(item){ + return $.inArray(item, values) < 0; + }); + this.options.afterSelect.call(this, selectedValues); + } + }, + + 'deselect_all' : function(){ + var ms = this.$element, + values = ms.val(); + + ms.find('option').prop('selected', false); + this.$selectableUl.find('.ms-elem-selectable').removeClass('ms-selected').show(); + this.$selectionUl.find('.ms-optgroup-label').hide(); + this.$selectableUl.find('.ms-optgroup-label').show(); + this.$selectionUl.find('.ms-elem-selection').removeClass('ms-selected').hide(); + this.$selectableUl.focus(); + ms.trigger('change'); + if (typeof this.options.afterDeselect === 'function') { + this.options.afterDeselect.call(this, values); + } + }, + + sanitize: function(value){ + var hash = 0, i, character; + if (value.length == 0) return hash; + var ls = 0; + for (i = 0, ls = value.length; i < ls; i++) { + character = value.charCodeAt(i); + hash = ((hash<<5)-hash)+character; + hash |= 0; // Convert to 32bit integer + } + return hash; + } + }; + + /* MULTISELECT PLUGIN DEFINITION + * ======================= */ + + $.fn.multiSelect = function () { + var option = arguments[0], + args = arguments; + + return this.each(function () { + var $this = $(this), + data = $this.data('multiselect'), + options = $.extend({}, $.fn.multiSelect.defaults, $this.data(), typeof option === 'object' && option); + + if (!data){ $this.data('multiselect', (data = new MultiSelect(this, options))); } + + if (typeof option === 'string'){ + data[option](args[1]); + } else { + data.init(); + } + }); + }; + + $.fn.multiSelect.defaults = { + keySelect: [32], + selectableOptgroup: false, + disabledClass : 'disabled', + dblClick : false, + keepOrder: false, + cssClass: '' + }; + + $.fn.multiSelect.Constructor = MultiSelect; + + $.fn.insertAt = function(index, $parent) { + return this.each(function() { + if (index === 0) { + $parent.prepend(this); + } else { + $parent.children().eq(index - 1).after(this); + } + }); +}; + +}(window.jQuery); diff --git a/app/templates/base.html b/app/templates/base.html index bfea2f5..3fa9583 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -35,6 +35,8 @@ + + @@ -306,6 +308,8 @@ + + {% endblock %}