/*jslint vars: true, unparam: true, white: true */
/*global jQuery, IQVOC */
IQVOC.EntitySelector = (function($) {
"use strict";
var EntitySelector = function(node) {
if(arguments.length === 0) { // subclassing; skip initialization
return;
}
this.el = $(node).hide(); // XXX: rename
this.container = $('
').data("widget", this);
this.bootstrapInputGroup = $('');
this.indicator = $('');
this.indicatorWrapper = $('');
this.languageWrapper = $('');
this.language = this.el.data("language") || false;
this.delimiter = ",";
this.singular = this.el.data("singular") || false;
this.entities = this.getSelection();
this.uriTemplate = this.el.data("entity-uri");
this.noResultsMsg = {
label: this.el.data("no-results-msg"),
value: ''
};
var self = this;
this.indicator.css("visibility", "hidden");
var selection = $.map(this.el.data("entities"), function(entity, i) {
return self.createEntity(entity);
});
selection = $('').append(selection);
this.input = $("").autocomplete({
minLength: 3,
source: $.proxy(this, "onInput"), // XXX: discards original `this` context
search: function(ev, ui) { self.indicator.css("visibility", "visible"); },
focus: function(ev, ui) { return false; },
select: this.onSelect,
response: function(ev, ui) {
if (!ui.content.length) {
ui.content.push(self.noResultsMsg);
}
}
});
// jQuery UI does not add a type attribute
// Bootstrap expects it to be there
this.input.attr('type', 'text').addClass('form-control');
this.container.append(this.bootstrapInputGroup.append(this.input).append(this.indicatorWrapper.append(this.indicator))).
append(selection).insertAfter(node).prepend(node);
if(this.language) {
this.bootstrapInputGroup.prepend(this.languageWrapper.append(this.language));
}
if(this.singular && this.entities.length) {
this.input.hide();
this.indicatorWrapper.hide();
}
};
// data transformations; target format is an array of objects with members
// `value` and `label`
// optional second argument `excludes` is an array of item IDs to exclude
EntitySelector.preprocessors = {
// converts an array of objects with members `id` and `name`
"default": function(data, excludes) {
return $.map(data, function(entity, i) {
return $.inArray(entity.id, excludes) !== -1 ? null :
{ value: entity.id, label: entity.name };
});
}
};
EntitySelector.sourceSelectors = {
"default": function(callback) {
var uri = this.el.data("query-url");
callback(uri);
},
"dummy-prompt": function(callback) {
var uri = this.el.data("query-url");
var sources = { iQvoc: "hello", GEMET: "world" };
var params = $.map(sources, function(id, name) {
return confirm("include " + name) ? 'source=' + id : null;
}).join("&");
callback(uri + "?" + params);
}
};
$.extend(EntitySelector.prototype, {
onInput: function(req, callback) {
var self = this;
var responder = function(data, status, xhr) { // TODO: rename, move elsewhere
data = self.processResponse(data);
self.input.autocomplete("option", "autoFocus", data.length === 1);
callback(data);
self.indicator.css("visibility", "hidden");
};
var sourceSelector = this.el.data("source-selector") || "default";
EntitySelector.sourceSelectors[sourceSelector].call(this, function(uri) { // XXX: direct `EntitySelector` reference limits subclassing
$.getJSON(uri, { query: req.term }, responder); // TODO: error handling
});
},
onSelect: function(ev, ui) {
var el = $(this).val(""),
widget = el.closest(".entity_select").data("widget");
if(widget.add(ui.item.value)) {
var entity = widget.
createEntity({ id: ui.item.value, name: ui.item.label });
widget.container.find("ul").append(entity);
if(widget.singular) {
widget.input.hide();
widget.indicatorWrapper.hide();
}
}
return false;
},
onDelete: function(ev) {
var el = $(this),
entity = el.closest("li"),
widget = el.closest(".entity_select").data("widget");
widget.remove(entity.data("id"));
entity.remove();
if(widget.singular && !widget.entities.length) {
widget.input.show();
widget.indicatorWrapper.show();
}
ev.preventDefault();
},
processResponse: function(data) { // TODO: rename
var preprocessor = this.el.data("preprocessor") || "default";
var exclude = this.el.data("exclude") || null;
var excludes = this.getSelection().concat(exclude ? [exclude] : []);
return EntitySelector.preprocessors[preprocessor](data, excludes); // XXX: direct `EntitySelector` reference limits subclassing
},
createEntity: function(entity) {
var el;
if(this.uriTemplate) {
var uri = this.uriTemplate.replace("%7Bid%7D", entity.id); // XXX: not very generic
el = $('').attr("href", uri).text(entity.name);
} else {
el = $('').text(entity.name);
}
var delBtn = $('x'). // "btn" to avoid fancy "button" class -- XXX: hacky workaround!?
click(this.onDelete);
return $("").data("id", entity.id).append(el).append(delBtn)[0];
},
add: function(entity) {
if($.inArray(entity, this.entities) === -1) {
this.entities.push(entity);
this.setSelection();
return true;
} else {
return false;
}
},
remove: function(entity) {
var pos = $.inArray(entity, this.entities);
if(pos !== -1) {
this.entities.splice(pos, 1);
this.setSelection();
}
},
setSelection: function() {
this.el.val(this.entities.join(this.delimiter));
},
getSelection: function() {
return $.map(this.el.val().split(this.delimiter), function(entity, i) {
return entity ? $.trim(entity) : null;
});
}
});
return EntitySelector;
}(jQuery));