/*
* 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('
');
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
(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('
'+option.text()+'
').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