968 lines
32 KiB
JavaScript
968 lines
32 KiB
JavaScript
|
/*
|
||
|
* jQuery UI Multiselect
|
||
|
*
|
||
|
* Authors:
|
||
|
* Michael Aufreiter (quasipartikel.at)
|
||
|
* Yanick Rochon (yanick.rochon[at]gmail[dot]com)
|
||
|
*
|
||
|
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
||
|
* and GPL (GPL-LICENSE.txt) licenses.
|
||
|
*
|
||
|
* http://yanickrochon.uuuq.com/multiselect/
|
||
|
*
|
||
|
*
|
||
|
* Depends:
|
||
|
* ui.core.js
|
||
|
* ui.draggable.js
|
||
|
* ui.droppable.js
|
||
|
* ui.sortable.js
|
||
|
* jquery.blockUI (http://github.com/malsup/blockui/)
|
||
|
* jquery.tmpl (http://andrew.hedges.name/blog/2008/09/03/introducing-jquery-simple-templates
|
||
|
*
|
||
|
* Optional:
|
||
|
* localization (http://plugins.jquery.com/project/localisation)
|
||
|
*
|
||
|
* Notes:
|
||
|
* The strings in this plugin use a templating engine to enable localization
|
||
|
* and allow flexibility in the messages. Read the documentation for more details.
|
||
|
*
|
||
|
* Todo:
|
||
|
* restore selected items on remote searchable multiselect upon page reload (same behavior as local mode)
|
||
|
* (is it worth it??) add a public function to apply the nodeComparator to all items (when using nodeComparator setter)
|
||
|
* support for option groups, disabled options, etc.
|
||
|
* speed improvements
|
||
|
* tests and optimizations
|
||
|
* - test getters/setters (including options from the defaults)
|
||
|
*/
|
||
|
|
||
|
|
||
|
// objectKeys return
|
||
|
$jqPm.extend({ objectKeys: function(obj){ if (typeof Object.keys == 'function') return Object.keys(obj); var a = []; $jqPm.each(obj, function(k){ a.push(k) }); return a; }});
|
||
|
|
||
|
/********************************
|
||
|
* Default callbacks
|
||
|
********************************/
|
||
|
|
||
|
// expect data to be "val1=text1[\nval2=text2[\n...]]"
|
||
|
var defaultDataParser = function(data) {
|
||
|
if ( typeof data == 'string' ) {
|
||
|
var pattern = /^(\s\n\r\t)*\+?$/;
|
||
|
var selected, line, lines = data.split(/\n/);
|
||
|
data = {};
|
||
|
for (var i in lines) {
|
||
|
line = lines[i].split("=");
|
||
|
// make sure the key is not empty
|
||
|
if (!pattern.test(line[0])) {
|
||
|
selected = (line[0].lastIndexOf('+') == line.length - 1);
|
||
|
if (selected) line[0] = line.substr(0,line.length-1);
|
||
|
// if no value is specified, default to the key value
|
||
|
data[line[0]] = {
|
||
|
selected: false,
|
||
|
value: line[1] || line[0]
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
this._messages($jqPm.ui.multiselect.constante.MESSAGE_ERROR, $.ui.multiselect.locale.errorDataFormat);
|
||
|
data = false;
|
||
|
}
|
||
|
return data;
|
||
|
};
|
||
|
|
||
|
var defaultNodeComparator = function(node1,node2) {
|
||
|
var text1 = node1.text(),
|
||
|
text2 = node2.text();
|
||
|
return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
|
||
|
};
|
||
|
|
||
|
(function($jqPm) {
|
||
|
|
||
|
$jqPm.widget("ui.multiselect", {
|
||
|
options: {
|
||
|
// sortable and droppable
|
||
|
sortable: 'none',
|
||
|
droppable: 'none',
|
||
|
// searchable
|
||
|
searchable: true,
|
||
|
searchDelay: 400,
|
||
|
searchAtStart: false,
|
||
|
remoteUrl: null,
|
||
|
remoteParams: {},
|
||
|
remoteLimit: 50,
|
||
|
remoteLimitIncrement: 50,
|
||
|
remoteStart: 0,
|
||
|
displayMore: false,
|
||
|
// animated
|
||
|
animated: 'fast',
|
||
|
show: 'slideDown',
|
||
|
hide: 'slideUp',
|
||
|
// ui
|
||
|
dividerLocation: 0.6,
|
||
|
// callbacks
|
||
|
dataParser: defaultDataParser,
|
||
|
nodeComparator: defaultNodeComparator,
|
||
|
nodeInserted: null,
|
||
|
// Add Vincent - Permet le click sur le li pour le passer d'un côté à un autre
|
||
|
triggerOnLiClick: false,
|
||
|
// Add Vincent - Permet le choix de la langue... en fonction de l'iso_code de PrestaShop (en, fr)
|
||
|
localeIsoCode: 'en'
|
||
|
},
|
||
|
_create: function() {
|
||
|
|
||
|
if (this.options.locale != undefined) {
|
||
|
$jqPm.ui.multiselect.locale = this.options.locale;
|
||
|
} else {
|
||
|
$jqPm.ui.multiselect.locale = {
|
||
|
addAll:'Add all',
|
||
|
removeAll:'Remove all',
|
||
|
itemsCount:'#{count} items selected',
|
||
|
itemsTotal:'#{count} items total',
|
||
|
busy:'please wait...',
|
||
|
errorDataFormat:"Cannot add options, unknown data format",
|
||
|
errorInsertNode:"There was a problem trying to add the item:\n\n\t[#{key}] => #{value}\n\nThe operation was aborted.",
|
||
|
errorReadonly:"The option #{option} is readonly",
|
||
|
errorRequest:"Sorry! There seemed to be a problem with the remote call. (Type: #{status})",
|
||
|
sInputSearch:'Please enter the first letters of the search item',
|
||
|
sInputShowMore: 'Show more'
|
||
|
};
|
||
|
}
|
||
|
|
||
|
this.element.hide();
|
||
|
this.busy = false; // busy state
|
||
|
this.idMultiSelect = this._uniqid(); // busy state
|
||
|
this.container = $jqPm('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
|
||
|
this.selectedContainer = $jqPm('<div class="ui-widget-content list-container selected"></div>').appendTo(this.container);
|
||
|
this.availableContainer = $jqPm('<div class="ui-widget-content list-container available"></div>').appendTo(this.container);
|
||
|
this.selectedActions = $jqPm('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">'+$jqPm.tmpl($jqPm.ui.multiselect.locale.itemsCount,{count:0})+'</span><a href="#" class="remove-all">'+$jqPm.tmpl($jqPm.ui.multiselect.locale.removeAll)+'</a></div>').appendTo(this.selectedContainer);
|
||
|
this.availableActions = $jqPm('<div class="actions ui-widget-header ui-helper-clearfix"><span class="busy">'+$jqPm.tmpl($jqPm.ui.multiselect.locale.busy)+'</span><input type="text" class="search ui-widget-content ui-corner-all" value="'+$jqPm.tmpl($jqPm.ui.multiselect.locale.sInputSearch)+'" onfocus="javascript:if(this.value==\''+$jqPm.tmpl($jqPm.ui.multiselect.locale.sInputSearch)+'\')this.value=\'\';" onblur="javascript:if(this.value==\'\')this.value=\''+$jqPm.tmpl($jqPm.ui.multiselect.locale.sInputSearch)+'\';" /><a href="#" class="add-all">'+$jqPm.tmpl($jqPm.ui.multiselect.locale.addAll)+'</a></div>').appendTo(this.availableContainer);
|
||
|
this.selectedList = $jqPm('<ul class="list selected"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
|
||
|
this.availableList = $jqPm('<ul class="list available"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
|
||
|
|
||
|
var that = this;
|
||
|
|
||
|
// initialize data cache
|
||
|
this.availableList.data('multiselect.cache', {});
|
||
|
this.selectedList.data('multiselect.cache', {});
|
||
|
|
||
|
if ( !this.options.animated ) {
|
||
|
this.options.show = 'show';
|
||
|
this.options.hide = 'hide';
|
||
|
}
|
||
|
|
||
|
// sortable / droppable / draggable
|
||
|
var dragOptions = {
|
||
|
selected: {
|
||
|
sortable: ('both' == this.options.sortable || 'left' == this.options.sortable),
|
||
|
droppable: ('both' == this.options.droppable || 'left' == this.options.droppable)
|
||
|
},
|
||
|
available: {
|
||
|
sortable: ('both' == this.options.sortable || 'right' == this.options.sortable),
|
||
|
droppable: ('both' == this.options.droppable || 'right' == this.options.droppable)
|
||
|
}
|
||
|
};
|
||
|
this._prepareLists('selected', 'available', dragOptions);
|
||
|
this._prepareLists('available', 'selected', dragOptions);
|
||
|
|
||
|
// set up livesearch
|
||
|
this._registerSearchEvents(this.availableContainer.find('input.search'), this.options.searchAtStart);
|
||
|
//
|
||
|
// make sure that we're not busy yet
|
||
|
this._setBusy(false);
|
||
|
|
||
|
// batch actions
|
||
|
this.container.find(".remove-all").bind('click.multiselect', function() { that.selectNone(); return false; });
|
||
|
this.container.find(".add-all").bind('click.multiselect', function() { that.selectAll(); return false; });
|
||
|
|
||
|
// set dimensions
|
||
|
this.container.width(this.element.width()+1);
|
||
|
this._refreshDividerLocation();
|
||
|
// set max width of search input dynamically
|
||
|
this.availableActions.find('input').width(Math.max(this.availableActions.width() - this.availableActions.find('a.add-all').width() - 30, 20));
|
||
|
// fix list height to match <option> depending on their individual header's heights
|
||
|
this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
|
||
|
this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
|
||
|
|
||
|
// init lists
|
||
|
this._populateLists(this.element.find('option'));
|
||
|
},
|
||
|
_uniqid: function() {
|
||
|
var newDate = new Date;
|
||
|
return newDate.getTime();
|
||
|
},
|
||
|
/**************************************
|
||
|
* Public
|
||
|
**************************************/
|
||
|
|
||
|
destroy: function() {
|
||
|
this.container.remove();
|
||
|
this.element.show();
|
||
|
|
||
|
$jqPm.widget.prototype.destroy.apply(this, arguments);
|
||
|
},
|
||
|
isBusy: function() {
|
||
|
return !!this.busy;
|
||
|
},
|
||
|
isSelected: function(item) {
|
||
|
if (this.enabled()) {
|
||
|
return !!this._findItem(item, this.selectedList);
|
||
|
} else {
|
||
|
return null;
|
||
|
}
|
||
|
},
|
||
|
// get all selected values in an array
|
||
|
selectedValues: function() {
|
||
|
return $jqPm.map( this.element.find('option[selected]'), function(item,i) { return $jqPm(item).val(); });
|
||
|
},
|
||
|
// get/set enable state
|
||
|
enabled: function(state, msg) {
|
||
|
if (undefined !== state) {
|
||
|
if (state) {
|
||
|
this.container.unblock();
|
||
|
this.element.removeAttr('disabled');
|
||
|
} else {
|
||
|
this.container.block({message:msg||null,overlayCSS:{backgroundColor:'#fff',opacity:0.4,cursor:'default'}});
|
||
|
this.element.attr('disabled', true);
|
||
|
}
|
||
|
}
|
||
|
return !this.element.attr('disabled');
|
||
|
},
|
||
|
selectAll: function() {
|
||
|
if (this.enabled()) {
|
||
|
this._batchSelect(this.availableList.children('li.ui-element:visible'), true);
|
||
|
}
|
||
|
},
|
||
|
selectNone: function() {
|
||
|
if (this.enabled()) {
|
||
|
this._batchSelect(this.selectedList.children('li.ui-element:visible'), false);
|
||
|
}
|
||
|
},
|
||
|
select: function(text) {
|
||
|
if (this.enabled()) {
|
||
|
var available = this._findItem(text, this.availableList);
|
||
|
if ( available ) {
|
||
|
this._setSelected(available, true);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
deselect: function(text) {
|
||
|
if (this.enabled()) {
|
||
|
var selected = this._findItem(text, this.selectedList);
|
||
|
if ( selected ) {
|
||
|
this._setSelected(selected, false);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
search: function(query) {
|
||
|
if (!this.busy && this.enabled() && this.options.searchable) {
|
||
|
var input = this.availableActions.children('input:first');
|
||
|
input.val(query);
|
||
|
input.trigger('keydown.multiselect');
|
||
|
}
|
||
|
},
|
||
|
// insert new <option> and _populate
|
||
|
// @return int the number of options added
|
||
|
addOptions: function(data) {
|
||
|
if (this.enabled()) {
|
||
|
this._setBusy(true);
|
||
|
// format data
|
||
|
var elements = [];
|
||
|
if (data = this.options.dataParser.call(this, data)) {
|
||
|
for (var key in data) {
|
||
|
// check if the option does not exist already
|
||
|
if (this.element.find('option[value="'+key+'"]').size()==0) {
|
||
|
elements.push( $jqPm('<option value="'+key+'"/>').text(data[key].value).appendTo(this.element)[0] );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (elements.length>0) {
|
||
|
this._populateLists($jqPm(elements));
|
||
|
}
|
||
|
this._filter(this.availableList.children('li.ui-element'), data);
|
||
|
|
||
|
var availableItemsCount = $jqPm.objectKeys(data).length;
|
||
|
|
||
|
if(availableItemsCount >= this.options.remoteLimit && this.options.displayMore == true) {
|
||
|
if (!$jqPm('#multiselectShowMore_'+this.idMultiSelect).length) {
|
||
|
var showMoreLink = $jqPm('<p id="multiselectShowMore_'+this.idMultiSelect+'"><a href="javascript:void(0);">'+$jqPm.tmpl($jqPm.ui.multiselect.locale.sInputShowMore)+'</a></p>');
|
||
|
this.availableList.after(showMoreLink);
|
||
|
} else {
|
||
|
var showMoreLink = $jqPm('#multiselectShowMore_'+this.idMultiSelect);
|
||
|
}
|
||
|
var that = this;
|
||
|
showMoreLink.unbind('click').bind('click', function() {
|
||
|
that.options.remoteStart += that.options.remoteLimit;
|
||
|
that.options.remoteLimit = that.options.remoteLimitIncrement;
|
||
|
that._registerSearchEvents(that.availableContainer.find('input.search'), true);
|
||
|
});
|
||
|
}
|
||
|
else if(this.options.displayMore == false || availableItemsCount < this.options.remoteLimit) {
|
||
|
$jqPm('#multiselectShowMore_'+this.idMultiSelect).fadeOut('fast',function() {$jqPm(this).remove();});
|
||
|
}
|
||
|
this._setBusy(false);
|
||
|
return elements.length;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**************************************
|
||
|
* Private
|
||
|
**************************************/
|
||
|
|
||
|
_setData: function(key, value) {
|
||
|
switch (key) {
|
||
|
// special treatement must be done for theses values when changed
|
||
|
case 'dividerLocation':
|
||
|
this.options.dividerLocation = value;
|
||
|
this._refreshDividerLocation();
|
||
|
break;
|
||
|
case 'searchable':
|
||
|
this.options.searchable = value;
|
||
|
this._registerSearchEvents(this.availableContainer.find('input.search'), false);
|
||
|
break;
|
||
|
|
||
|
case 'droppable':
|
||
|
case 'sortable':
|
||
|
// readonly options
|
||
|
this._messages(
|
||
|
$jqPm.ui.multiselect.constants.MESSAGE_WARNING,
|
||
|
$jqPm.ui.multiselect.locale.errorReadonly,
|
||
|
{option: key}
|
||
|
);
|
||
|
default:
|
||
|
// default behavior
|
||
|
this.options[key] = value;
|
||
|
break;
|
||
|
}
|
||
|
},
|
||
|
_ui: function(type) {
|
||
|
var uiObject = {sender: this.element};
|
||
|
switch (type) {
|
||
|
// events: messages
|
||
|
case 'message':
|
||
|
uiObject.type = arguments[1];
|
||
|
uiObject.message = arguments[2];
|
||
|
break;
|
||
|
|
||
|
// events: selected, deselected
|
||
|
case 'selection':
|
||
|
uiObject.option = arguments[1];
|
||
|
break;
|
||
|
}
|
||
|
return uiObject;
|
||
|
},
|
||
|
_messages: function(type, msg, params) {
|
||
|
this._trigger('messages', null, this._ui('message', type, $jqPm.tmpl(msg, params)));
|
||
|
},
|
||
|
|
||
|
_refreshDividerLocation: function() {
|
||
|
this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
|
||
|
this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
|
||
|
},
|
||
|
_prepareLists: function(side, otherSide, opts) {
|
||
|
var that = this;
|
||
|
var itemSelected = ('selected' == side);
|
||
|
var list = this[side+'List'];
|
||
|
var otherList = this[otherSide+'List'];
|
||
|
var listDragHelper = opts[otherSide].sortable ? _dragHelper : 'clone';
|
||
|
|
||
|
list
|
||
|
.data('multiselect.sortable', opts[side].sortable )
|
||
|
.data('multiselect.droppable', opts[side].droppable )
|
||
|
.data('multiselect.draggable', !opts[side].sortable && (opts[otherSide].sortable || opts[otherSide].droppable) );
|
||
|
|
||
|
if (opts[side].sortable) {
|
||
|
list.sortable({
|
||
|
appendTo: this.container,
|
||
|
connectWith: otherList,
|
||
|
containment: this.container,
|
||
|
helper: listDragHelper,
|
||
|
items: 'li.ui-element',
|
||
|
revert: !(opts[otherSide].sortable || opts[otherSide].droppable),
|
||
|
receive: function(event, ui) {
|
||
|
// DEBUG
|
||
|
//that._messages(0, "Receive : " + ui.item.data('multiselect.optionLink') + ":" + ui.item.parent()[0].className + " = " + itemSelected);
|
||
|
|
||
|
// we received an element from a sortable to another sortable...
|
||
|
if (opts[otherSide].sortable) {
|
||
|
var optionLink = ui.item.data('multiselect.optionLink');
|
||
|
|
||
|
that._applyItemState(ui.item.hide(), itemSelected);
|
||
|
|
||
|
// if the cache already contain an element, remove it
|
||
|
if (otherList.data('multiselect.cache')[optionLink.val()]) {
|
||
|
delete otherList.data('multiselect.cache')[optionLink.val()];
|
||
|
}
|
||
|
|
||
|
ui.item.hide();
|
||
|
that._setSelected(ui.item, itemSelected, true);
|
||
|
} else {
|
||
|
// the other is droppable only, so merely select the element...
|
||
|
setTimeout(function() {
|
||
|
that._setSelected(ui.item, itemSelected);
|
||
|
}, 10);
|
||
|
}
|
||
|
},
|
||
|
stop: function(event, ui) {
|
||
|
// DEBUG
|
||
|
//that._messages(0, "Stop : " + (ui.item.parent()[0] == otherList[0]));
|
||
|
that._moveOptionNode(ui.item);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// cannot be droppable if both lists are sortable, it breaks the receive function
|
||
|
if (!(opts[side].sortable && opts[otherSide].sortable)
|
||
|
&& (opts[side].droppable || opts[otherSide].sortable || opts[otherSide].droppable)) {
|
||
|
//alert( side + " is droppable ");
|
||
|
list.droppable({
|
||
|
accept: '.ui-multiselect li.ui-element',
|
||
|
hoverClass: 'ui-state-highlight',
|
||
|
revert: !(opts[otherSide].sortable || opts[otherSide].droppable),
|
||
|
greedy: true,
|
||
|
drop: function(event, ui) {
|
||
|
// DEBUG
|
||
|
//that._messages(0, "drop " + side + " = " + ui.draggable.data('multiselect.optionLink') + ":" + ui.draggable.parent()[0].className);
|
||
|
|
||
|
//alert( "drop " + itemSelected );
|
||
|
// if no optionLink is defined, it was dragged in
|
||
|
if (!ui.draggable.data('multiselect.optionLink')) {
|
||
|
var optionLink = ui.helper.data('multiselect.optionLink');
|
||
|
ui.draggable.data('multiselect.optionLink', optionLink);
|
||
|
|
||
|
// if the cache already contain an element, remove it
|
||
|
if (list.data('multiselect.cache')[optionLink.val()]) {
|
||
|
delete list.data('multiselect.cache')[optionLink.val()];
|
||
|
}
|
||
|
list.data('multiselect.cache')[optionLink.val()] = ui.draggable;
|
||
|
|
||
|
that._applyItemState(ui.draggable, itemSelected);
|
||
|
|
||
|
// received an item from a sortable to a droppable
|
||
|
} else if (!opts[side].sortable) {
|
||
|
setTimeout(function() {
|
||
|
ui.draggable.hide();
|
||
|
that._setSelected(ui.draggable, itemSelected);
|
||
|
}, 10);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
_populateLists: function(options) {
|
||
|
this._setBusy(true);
|
||
|
|
||
|
var that = this;
|
||
|
// do this async so the browser actually display the waiting message
|
||
|
setTimeout(function() {
|
||
|
$jqPm(options.each(function(i) {
|
||
|
var list = (this.selected ? that.selectedList : that.availableList);
|
||
|
var item = that._getOptionNode(this).show();
|
||
|
that._applyItemState(item, this.selected);
|
||
|
item.data('multiselect.idx', i);
|
||
|
|
||
|
// cache
|
||
|
list.data('multiselect.cache')[item.data('multiselect.optionLink').val()] = item;
|
||
|
|
||
|
that._insertToList(item, list);
|
||
|
}));
|
||
|
|
||
|
// update count
|
||
|
that._setBusy(false);
|
||
|
that._updateCount();
|
||
|
}, 1);
|
||
|
},
|
||
|
_insertToList: function(node, list) {
|
||
|
|
||
|
// Add Vincent - Permet le click sur le li pour le passer d'un côté à un autre
|
||
|
if (this.options.triggerOnLiClick == true) {
|
||
|
node.unbind('click.multiselect').bind('click.multiselect', function() { node.find('a.action').trigger('click.multiselect'); });
|
||
|
}
|
||
|
// End Vincent - Permet le click sur le li pour le passer d'un côté à un autre
|
||
|
|
||
|
var that = this;
|
||
|
this._setBusy(true);
|
||
|
// the browsers don't like batch node insertion...
|
||
|
var _addNodeRetry = 0;
|
||
|
var _addNode = function() {
|
||
|
var succ = (that.options.nodeComparator ? that._getSuccessorNode(node, list) : null);
|
||
|
try {
|
||
|
if (succ) {
|
||
|
node.insertBefore(succ);
|
||
|
} else {
|
||
|
list.append(node);
|
||
|
}
|
||
|
if (list === that.selectedList) that._moveOptionNode(node);
|
||
|
|
||
|
// callback after node insertion
|
||
|
if ('function' == typeof that.options.nodeInserted) that.options.nodeInserted(node);
|
||
|
that._setBusy(false);
|
||
|
} catch (e) {
|
||
|
// if this problem did not occur too many times already
|
||
|
if ( _addNodeRetry++ < 10 ) {
|
||
|
// try again later (let the browser cool down first)
|
||
|
setTimeout(function() { _addNode(); }, 1);
|
||
|
} else {
|
||
|
that._messages(
|
||
|
$jqPm.ui.multiselect.constants.MESSAGE_EXCEPTION,
|
||
|
$jqPm.ui.multiselect.locale.errorInsertNode,
|
||
|
{key:node.data('multiselect.optionLink').val(), value:node.text()}
|
||
|
);
|
||
|
that._setBusy(false);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
_addNode();
|
||
|
},
|
||
|
_updateCount: function() {
|
||
|
var that = this;
|
||
|
// defer until system is not busy
|
||
|
if (this.busy) setTimeout(function() { that._updateCount(); }, 100);
|
||
|
// count only visible <li> (less .ui-helper-hidden*)
|
||
|
var count = this.selectedList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible').size();
|
||
|
var total = this.availableList.children('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder,.shadowed)').size() + count;
|
||
|
this.selectedContainer.find('span.count')
|
||
|
.html($jqPm.tmpl($jqPm.ui.multiselect.locale.itemsCount, {count:count}))
|
||
|
.attr('title', $jqPm.tmpl($jqPm.ui.multiselect.locale.itemsTotal, {count:total}));
|
||
|
},
|
||
|
_getOptionNode: function(option) {
|
||
|
option = $jqPm(option);
|
||
|
var node = $jqPm('<li class="ui-state-default ui-element"><span class="ui-icon"/>'+option.text()+'<a href="#" class="ui-state-default action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
|
||
|
|
||
|
// Add Vincent - Permet le click sur le li pour le passer d'un côté à un autre
|
||
|
if (this.options.triggerOnLiClick == true) {
|
||
|
node.unbind('click.multiselect').bind('click.multiselect', function() { node.find('a.action').trigger('click.multiselect'); });
|
||
|
}
|
||
|
// End Vincent - Permet le click sur le li pour le passer d'un côté à un autre
|
||
|
|
||
|
node.data('multiselect.optionLink', option);
|
||
|
return node;
|
||
|
},
|
||
|
_moveOptionNode: function(item) {
|
||
|
// call this async to let the item be placed correctly
|
||
|
setTimeout( function() {
|
||
|
var optionLink = item.data('multiselect.optionLink');
|
||
|
if (optionLink) {
|
||
|
var prevItem = item.prev('li:not(.ui-helper-hidden-accessible,.ui-sortable-placeholder):visible');
|
||
|
var prevOptionLink = prevItem.size() ? prevItem.data('multiselect.optionLink') : null;
|
||
|
|
||
|
if (prevOptionLink) {
|
||
|
optionLink.insertAfter(prevOptionLink);
|
||
|
} else {
|
||
|
optionLink.prependTo(optionLink.parent());
|
||
|
}
|
||
|
}
|
||
|
}, 100);
|
||
|
},
|
||
|
// used by select and deselect, etc.
|
||
|
_findItem: function(text, list) {
|
||
|
var found = null;
|
||
|
list.children('li.ui-element:visible').each(function(i,el) {
|
||
|
el = $jqPm(el);
|
||
|
if (el.text().toLowerCase() === text.toLowerCase()) {
|
||
|
found = el;
|
||
|
}
|
||
|
});
|
||
|
if (found && found.size()) {
|
||
|
return found;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
},
|
||
|
// clones an item with
|
||
|
// didn't find a smarter away around this (michael)
|
||
|
// now using cache to speed up the process (yr)
|
||
|
_cloneWithData: function(clonee, cacheName, insertItem) {
|
||
|
var that = this;
|
||
|
var id = clonee.data('multiselect.optionLink').val();
|
||
|
var selected = ('selected' == cacheName);
|
||
|
var list = (selected ? this.selectedList : this.availableList);
|
||
|
var clone = list.data('multiselect.cache')[id];
|
||
|
|
||
|
if (!clone) {
|
||
|
clone = clonee.clone().hide();
|
||
|
this._applyItemState(clone, selected);
|
||
|
// update cache
|
||
|
list.data('multiselect.cache')[id] = clone;
|
||
|
// update <option> and idx
|
||
|
clone.data('multiselect.optionLink', clonee.data('multiselect.optionLink'));
|
||
|
// need this here because idx is needed in _getSuccessorNode
|
||
|
clone.data('multiselect.idx', clonee.data('multiselect.idx'));
|
||
|
|
||
|
// insert the node into it's list
|
||
|
if (insertItem) {
|
||
|
this._insertToList(clone, list);
|
||
|
}
|
||
|
} else {
|
||
|
// update idx
|
||
|
clone.data('multiselect.idx', clonee.data('multiselect.idx'));
|
||
|
}
|
||
|
return clone;
|
||
|
},
|
||
|
_batchSelect: function(elements, state) {
|
||
|
this._setBusy(true);
|
||
|
|
||
|
var that = this;
|
||
|
// do this async so the browser actually display the waiting message
|
||
|
setTimeout(function() {
|
||
|
var _backup = {
|
||
|
animated: that.options.animated,
|
||
|
hide: that.options.hide,
|
||
|
show: that.options.show
|
||
|
};
|
||
|
|
||
|
that.options.animated = null;
|
||
|
that.options.hide = 'hide';
|
||
|
that.options.show = 'show';
|
||
|
|
||
|
elements.each(function(i,element) {
|
||
|
that._setSelected($jqPm(element), state);
|
||
|
});
|
||
|
|
||
|
// filter available items
|
||
|
if (!state) that._filter(that.availableList.find('li.ui-element'));
|
||
|
|
||
|
// restore
|
||
|
$jqPm.extend(that.options, _backup);
|
||
|
|
||
|
that._updateCount();
|
||
|
that._setBusy(false);
|
||
|
}, 10);
|
||
|
},
|
||
|
// find the best successor the given item in the specified list
|
||
|
// TODO implement a faster sorting algorithm (and remove the idx dependancy)
|
||
|
_getSuccessorNode: function(item, list) {
|
||
|
// look for successor based on initial option index
|
||
|
var items = list.find('li.ui-element'), comparator = this.options.nodeComparator;
|
||
|
var itemsSize = items.size();
|
||
|
|
||
|
// no successor, list is null
|
||
|
if (items.size() == 0) return null;
|
||
|
|
||
|
|
||
|
var succ, i = Math.min(item.data('multiselect.idx'),itemsSize-1), direction = comparator(item, $jqPm(items[i]));
|
||
|
|
||
|
if ( direction ) {
|
||
|
// quick checks
|
||
|
if (0>direction && 0>=i) {
|
||
|
succ = items[0];
|
||
|
} else if (0<direction && itemsSize-1<=i) {
|
||
|
i++;
|
||
|
succ = null;
|
||
|
} else {
|
||
|
while (i>=0 && i<items.length) {
|
||
|
direction > 0 ? i++ : i--;
|
||
|
if (i<0) {
|
||
|
succ = item[0]
|
||
|
}
|
||
|
if ( direction != comparator(item, $jqPm(items[i])) ) {
|
||
|
// going up, go back one item down, otherwise leave as is
|
||
|
succ = items[direction > 0 ? i : i+1];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
succ = items[i];
|
||
|
}
|
||
|
// update idx
|
||
|
item.data('multiselect.idx', i);
|
||
|
|
||
|
return succ;
|
||
|
},
|
||
|
// @param DOMElement item is the item to set
|
||
|
// @param bool selected true|false (state)
|
||
|
// @param bool noclone (optional) true only if item should not be cloned on the other list
|
||
|
_setSelected: function(item, selected, noclone) {
|
||
|
var that = this, otherItem;
|
||
|
var optionLink = item.data('multiselect.optionLink');
|
||
|
|
||
|
if (selected) {
|
||
|
// already selected
|
||
|
if (optionLink.attr('selected')) return;
|
||
|
optionLink.attr('selected','selected');
|
||
|
|
||
|
if (noclone) {
|
||
|
otherItem = item;
|
||
|
} else {
|
||
|
// retrieve associatd or cloned item
|
||
|
otherItem = this._cloneWithData(item, 'selected', true).hide();
|
||
|
item.addClass('shadowed')[this.options.hide](this.options.animated, function() { that._updateCount(); });
|
||
|
}
|
||
|
otherItem[this.options.show](this.options.animated);
|
||
|
} else {
|
||
|
// already deselected
|
||
|
if (!optionLink.attr('selected')) return;
|
||
|
optionLink.removeAttr('selected');
|
||
|
|
||
|
if (noclone) {
|
||
|
otherItem = item;
|
||
|
} else {
|
||
|
// retrieve associated or clone the item
|
||
|
otherItem = this._cloneWithData(item, 'available', true).hide().removeClass('shadowed');
|
||
|
item[this.options.hide](this.options.animated, function() { that._updateCount() });
|
||
|
}
|
||
|
if (!otherItem.is('.filtered')) otherItem[this.options.show](this.options.animated);
|
||
|
}
|
||
|
|
||
|
if (!this.busy) {
|
||
|
if (this.options.animated) {
|
||
|
// pulse
|
||
|
//otherItem.effect("pulsate", { times: 1, mode: 'show' }, 400); // pulsate twice???
|
||
|
otherItem.fadeTo('fast', 0.3, function() { $jqPm(this).fadeTo('fast', 1); });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fire selection event
|
||
|
this._trigger(selected ? 'selected' : 'deselected', null, this._ui('selection', optionLink));
|
||
|
|
||
|
return otherItem;
|
||
|
},
|
||
|
_setBusy: function(state) {
|
||
|
var input = this.availableActions.children('input.search');
|
||
|
var busy = this.availableActions.children('.busy');
|
||
|
|
||
|
this.busy = Math.max(state ? ++this.busy : --this.busy, 0);
|
||
|
|
||
|
this.container.find("a.remove-all, a.add-all")[this.busy ? 'hide' : 'show']();
|
||
|
if (state && (1 == this.busy)) {
|
||
|
if (this.options.searchable) {
|
||
|
// backup input state
|
||
|
input.data('multiselect.hadFocus', input.data('multiselect.hasFocus'));
|
||
|
// webkit needs to blur before hiding or it won't fire focus again in the else block
|
||
|
input.blur().hide();
|
||
|
}
|
||
|
busy.show();
|
||
|
} else if(!this.busy) {
|
||
|
if (this.options.searchable) {
|
||
|
input.show();
|
||
|
if (input.data('multiselect.hadFocus')) input.focus();
|
||
|
}
|
||
|
busy.hide();
|
||
|
}
|
||
|
|
||
|
// DEBUG
|
||
|
//this._messages(0, "Busy state changed to : " + this.busy);
|
||
|
},
|
||
|
_applyItemState: function(item, selected) {
|
||
|
if (selected) {
|
||
|
item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon');
|
||
|
item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
|
||
|
this._registerRemoveEvents(item.find('a.action'));
|
||
|
} else {
|
||
|
item.children('span').addClass('ui-helper-hidden').removeClass('ui-icon');
|
||
|
item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
|
||
|
this._registerAddEvents(item.find('a.action'));
|
||
|
}
|
||
|
|
||
|
this._registerHoverEvents(item);
|
||
|
|
||
|
return item;
|
||
|
},
|
||
|
// apply filter and return elements
|
||
|
_filter: function(elements, data) {
|
||
|
var input = this.availableActions.children('input.search');
|
||
|
var term = $jqPm.trim( input.val().toLowerCase() );
|
||
|
|
||
|
if ( !term ) {
|
||
|
elements.removeClass('filtered');
|
||
|
} else {
|
||
|
elements.each(function(i,element) {
|
||
|
element = $jqPm(element);
|
||
|
if (data != undefined) {
|
||
|
if (data[element.data('multiselect.optionLink').val()] != undefined) {
|
||
|
element['removeClass']('filtered');
|
||
|
} else {
|
||
|
element['addClass']('filtered');
|
||
|
}
|
||
|
} else {
|
||
|
element[(element.text().toLowerCase().indexOf(term)>=0 ? 'remove' : 'add')+'Class']('filtered');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return elements.not('.filtered, .shadowed').show().end().filter('.filtered, .shadowed').hide().end();
|
||
|
},
|
||
|
_registerHoverEvents: function(elements) {
|
||
|
elements
|
||
|
.unbind('mouseover.multiselect').bind('mouseover.multiselect', function() {
|
||
|
$jqPm(this).find('a').andSelf().addClass('ui-state-hover');
|
||
|
})
|
||
|
.unbind('mouseout.multiselect').bind('mouseout.multiselect', function() {
|
||
|
$jqPm(this).find('a').andSelf().removeClass('ui-state-hover');
|
||
|
})
|
||
|
.find('a').andSelf().removeClass('ui-state-hover')
|
||
|
;
|
||
|
},
|
||
|
_registerAddEvents: function(elements) {
|
||
|
var that = this;
|
||
|
elements.unbind('click.multiselect').bind('click.multiselect', function() {
|
||
|
// ignore if busy...
|
||
|
if (!this.busy) {
|
||
|
that._setSelected($jqPm(this).parent(), true);
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
if (this.availableList.data('multiselect.draggable')) {
|
||
|
// make draggable
|
||
|
elements.each(function() {
|
||
|
$jqPm(this).parent().draggable({
|
||
|
connectToSortable: that.selectedList,
|
||
|
helper: _dragHelper,
|
||
|
appendTo: that.container,
|
||
|
containment: that.container,
|
||
|
revert: 'invalid'
|
||
|
});
|
||
|
});
|
||
|
// refresh the selected list or the draggable will not connect to it first hand
|
||
|
if (this.selectedList.data('multiselect.sortable')) {
|
||
|
this.selectedList.sortable('refresh');
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
_registerRemoveEvents: function(elements) {
|
||
|
var that = this;
|
||
|
elements.unbind('click.multiselect').bind('click.multiselect', function() {
|
||
|
// ignore if busy...
|
||
|
if (!that.busy) {
|
||
|
that._setSelected($jqPm(this).parent(), false);
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
if (this.selectedList.data('multiselect.draggable')) {
|
||
|
// make draggable
|
||
|
elements.each(function() {
|
||
|
$jqPm(this).parent().draggable({
|
||
|
connectToSortable: that.availableList,
|
||
|
helper: _dragHelper,
|
||
|
appendTo: that.container,
|
||
|
containment: that.container,
|
||
|
revert: 'invalid'
|
||
|
});
|
||
|
});
|
||
|
// refresh the selected list or the draggable will not connect to it first hand
|
||
|
if (this.availableList.data('multiselect.sortable')) {
|
||
|
this.availableList.sortable('refresh');
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
_registerSearchEvents: function(input, searchNow) {
|
||
|
var that = this;
|
||
|
var previousValue = input.val(), timer;
|
||
|
|
||
|
var _searchNow = function(forceUpdate) {
|
||
|
if (that.busy) return;
|
||
|
|
||
|
var value = input.val();
|
||
|
if ((value != previousValue) || (forceUpdate)) {
|
||
|
that._setBusy(true);
|
||
|
|
||
|
if (that.options.remoteUrl) {
|
||
|
var params = $jqPm.extend({}, that.options.remoteParams);
|
||
|
try {
|
||
|
$jqPm.ajax({
|
||
|
url: that.options.remoteUrl,
|
||
|
data: $jqPm.extend(params, {q:escape(value), start:that.options.remoteStart, limit:that.options.remoteLimit}),
|
||
|
success: function(data) {
|
||
|
that.addOptions(data);
|
||
|
that._setBusy(false);
|
||
|
},
|
||
|
error: function(request,status,e) {
|
||
|
that._messages(
|
||
|
$jqPm.ui.multiselect.constants.MESSAGE_ERROR,
|
||
|
$jqPm.ui.multiselect.locale.errorRequest,
|
||
|
{status:status}
|
||
|
);
|
||
|
that._setBusy(false);
|
||
|
}
|
||
|
});
|
||
|
} catch (e) {
|
||
|
that._messages($jqPm.ui.multiselect.constants.MESSAGE_EXCEPTION, e.message); // error message template ??
|
||
|
that._setBusy(false);
|
||
|
}
|
||
|
} else {
|
||
|
that._filter(that.availableList.children('li.ui-element'));
|
||
|
that._setBusy(false);
|
||
|
}
|
||
|
|
||
|
previousValue = value;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// reset any events... if any
|
||
|
input.unbind('focus.multiselect blur.multiselect keydown.multiselect keypress.multiselect');
|
||
|
if (this.options.searchable) {
|
||
|
input
|
||
|
.bind('focus.multiselect', function() {
|
||
|
$jqPm(this).addClass('ui-state-active').data('multiselect.hasFocus', true);
|
||
|
})
|
||
|
.bind('blur.multiselect', function() {
|
||
|
$jqPm(this).removeClass('ui-state-active').data('multiselect.hasFocus', false);
|
||
|
})
|
||
|
.bind('keydown.multiselect keypress.multiselect', function(e) {
|
||
|
if (timer) clearTimeout(timer);
|
||
|
switch (e.which) {
|
||
|
case 13: // enter
|
||
|
_searchNow(true);
|
||
|
return false;
|
||
|
|
||
|
default:
|
||
|
timer = setTimeout(function() { _searchNow(); }, Math.max(that.options.searchDelay,1));
|
||
|
}
|
||
|
})
|
||
|
.show();
|
||
|
} else {
|
||
|
input.val('').hide();
|
||
|
this._filter(that.availableList.find('li.ui-element'))
|
||
|
}
|
||
|
// initiate search filter (delayed)
|
||
|
var _initSearch = function() {
|
||
|
if (that.busy) {
|
||
|
setTimeout(function() { _initSearch(); }, 100);
|
||
|
}
|
||
|
_searchNow(true);
|
||
|
};
|
||
|
|
||
|
if (searchNow) _initSearch();
|
||
|
}
|
||
|
});
|
||
|
// END ui.multiselect
|
||
|
|
||
|
|
||
|
/********************************
|
||
|
* Internal functions
|
||
|
********************************/
|
||
|
|
||
|
var _dragHelper = function(event, ui) {
|
||
|
var item = $jqPm(event.target);
|
||
|
var clone = item.clone().width(item.width());
|
||
|
if (clone.data('multiselect.optionLink') != undefined && item.data('multiselect.optionLink') != undefined) {
|
||
|
clone
|
||
|
.data('multiselect.optionLink', item.data('multiselect.optionLink'))
|
||
|
.data('multiselect.list', item.parent() )
|
||
|
// node ui cleanup
|
||
|
.find('a').remove()
|
||
|
;
|
||
|
}
|
||
|
return clone;
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
/****************************
|
||
|
* Settings
|
||
|
****************************/
|
||
|
|
||
|
$jqPm.extend($jqPm.ui.multiselect, {
|
||
|
getter: 'selectedValues enabled isBusy',
|
||
|
constants: {
|
||
|
MESSAGE_WARNING: 0,
|
||
|
MESSAGE_EXCEPTION: 1,
|
||
|
MESSAGE_ERROR: 2
|
||
|
}
|
||
|
});
|
||
|
|
||
|
})($jqPm);
|