/*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));