diff --git a/lib/live_filters.js b/lib/live_filters.js new file mode 100644 index 0000000..d7f81fc --- /dev/null +++ b/lib/live_filters.js @@ -0,0 +1,1172 @@ +/* + * Package: live_filters.js + * + * Namespace: bbop-widget-set.live_filters + * + * BBOP JS object to allow the live probing of a GOlr personality. + * + * Very much like a separated accordion and filter from the search + * pane. + * + * This is a Bootstrap 3 widget. + * + * See Also: + * + * + */ + +var us = require('underscore'); +var bbop = require('bbop-core'); + +// Code here will be ignored by JSHint, as we are technically +// "redefining" jQuery (although we are not). +/* jshint ignore:start */ +var jQuery = require('jquery'); +/* jshint ignore:end */ + +/* + * Constructor: live_filters + * + * Contructor for the bbop-widget-set.live_filters object. + * + * Widget interface to interactively explore a search personality with + * no direct side effects. + * + * Arguments: + * interface_id - string id of the element to build on + * manager - the shared GOlr manager to use + * golr_conf_obj - the profile of the specific + * in_argument_hash - *[optional]* optional hash of optional arguments + * + * Returns: + * this object + */ +var live_filters = function(interface_id, manager, golr_conf_obj, + in_argument_hash){ + this._is_a = 'bbop-widget-set.live_filters'; + + var anchor = this; + var each = us.each; + + // TODO/BUG: Remove the need for these. + var ui_icon_positive_label = '+'; + var ui_icon_positive_source = null; + var ui_icon_negative_label = '−'; + var ui_icon_negative_source = null; + var ui_icon_remove_label = '−'; + var ui_icon_remove_source = null; + var ui_spinner_shield_source = null; + var ui_spinner_shield_message = ''; + + // Per-UI logger. + var logger = new bbop.logger(); + logger.DEBUG = false; + //logger.DEBUG = true; + function ll(str){ logger.kvetch('LF: ' + str); } + + /// + /// Deal with incoming arguments. + /// + + // this._class_conf = golr_conf_obj; + + // Our argument default hash. + var default_hash = { + 'meta_label': 'Documents: ', + 'display_meta_p': true, + 'display_free_text_p': true, + 'free_text_placeholder': 'Free-text filter', + 'display_accordion_p': true, + 'minimum_free_text_length': 3, // wait for three characters or more + 'on_update_callback': function(){} + }; + var folding_hash = in_argument_hash || {}; + var arg_hash = bbop.fold(default_hash, folding_hash); + // + this._interface_id = interface_id; + this._display_meta_p = arg_hash['display_meta_p']; + this._meta_label = arg_hash['meta_label']; + this._display_free_text_p = arg_hash['display_free_text_p']; + this._free_text_placeholder = arg_hash['free_text_placeholder']; + this._display_accordion_p = arg_hash['display_accordion_p']; + this._minimum_free_text_length = arg_hash['minimum_free_text_length']; + this._on_update_callback = arg_hash['on_update_callback']; + + /// + /// Prepare the interface and setup the div hooks. + /// + + anchor._established_p = false; + + // Mangle everything around this unique id so we don't collide + // with other instances on the same page. + var ui_div_id = this._interface_id; + var mangle = ui_div_id + '_ui_element_' + bbop.uuid() + '_'; + + // Main div id hooks to the easily changable areas of the display. + var container_div_id = mangle + 'container-id'; + // Meta hooks. + var meta_div_id = mangle + 'meta-id'; + var meta_count_id = mangle + 'meta-count-id'; + var meta_wait_id = mangle + 'meta-wait-id'; + // Query hooks + var query_input_div_id = mangle + 'query-id'; + // Sticky hooks. + var sticky_filters_div_id = mangle + 'sticky_filters-id'; + var sticky_title_id = mangle + 'sticky_filters-title-id'; + var sticky_content_id = mangle + 'sticky_filters-content-id'; + // Current hooks. + var current_filters_div_id = mangle + 'current_filters-id'; + var current_title_id = mangle + 'current_filters-title-id'; + var current_content_id = mangle + 'current_filters-content-id'; + var clear_user_filter_span_id = mangle + 'clear-user-filter-id'; + // Accordion hooks. + var filters_div_id = mangle + 'ui-filters-wrapper'; + var clear_query_span_id = mangle + 'clear-query-id'; + // var ui_user_button_div_id = mangle + 'user-button-id'; + // var ui_results_table_div_id = mangle + 'results-table-id'; + // var ui_count_control_div_id = mangle + 'count_control-id'; + + // Blow away whatever was there completely. + // Render a control section into HTML. This includes the accordion + // and current filter sections. + // Get the user interface hook and remove anything that was there. + var container_div = new html.tag('div', {'id': container_div_id}); + jQuery('#' + ui_div_id).empty(); + jQuery('#' + ui_div_id).append(container_div.to_string()); + + // // Globally declared (or not) icons. + // var ui_spinner_search_source = ''; + // var ui_spinner_shield_source = ''; + // var ui_spinner_shield_message = null; + // var ui_icon_positive_label = ''; + // var ui_icon_positive_source = ''; + // var ui_icon_negative_label = ''; + // var ui_icon_negative_source = ''; + // var ui_icon_remove_label = ''; + // var ui_icon_remove_source = ''; + + // // The spinner, if it exists, needs to be accessible by everybody + // // and safe to use. + // var spinner = null; + // function _spinner_gen(elt_id){ + // var spinner_args = { + // //timeout: 5, + // //timeout: 500, + // timeout: 10, + // //classes: 'bbop-widget-search_pane-spinner', + // visible_p: false + // }; + // spinner = new widget.spinner(elt_id, + // ui_spinner_search_source, + // spinner_args); + // } + + // // Additional id hooks for easy callbacks. While these are not as + // // easily changable as the above, we use them often enough and + // // across functions to have a hook. + // var accordion_div_id = mangle + 'filter-accordion-id'; + + // // These pointers are used in multiple functions (e.g. both + // // *_setup and *_draw). + var filter_accordion_widget = null; + var spinner_div = null; + // //var current_filters_div = null; + + function _spin_up(){ + if( spinner_div ){ + jQuery('#' + spinner_div.get_id()).removeClass('hidden'); + jQuery('#' + spinner_div.get_id()).addClass('active'); + } + } + function _spin_down(){ + if( spinner_div ){ + jQuery('#' + spinner_div.get_id()).addClass('hidden'); + jQuery('#' + spinner_div.get_id()).removeClass('active'); + } + } + + /* + * Function: spin_up + * + * Turn on the spinner. + * + * Parameters: + * n/a + * + * Returns + * n/a + */ + this.spin_up = function(){ + _spin_up(); + }; + + /* + * Function: spin_down + * + * Turn off the spinner. + * + * Parameters: + * n/a + * + * Returns + * n/a + */ + this.spin_down = function(){ + _spin_down(); + }; + + /* + * Function: establish_display + * + * Completely redraw the display. + * + * Required to display after setting up the manager. + * + * Also may be useful after a major change to the manager to reset + * it. + * + * Parameters: + * n/a + * + * Returns + * n/a + */ + this.establish_display = function(){ + + // Can only make a display if there is a set + // personality--there is no general default and it is an + // error. + var personality = manager.get_personality(); + var cclass = golr_conf_obj.get_class(personality); + if( ! personality || ! cclass ){ + ll('ERROR: no usable personality set'); + throw new Error('ERROR: no useable personality set'); + } + + /// + /// Setup the UI base. + /// + + // Holder for things like spinner and current number of + // results. + this.setup_meta = function(){ + ll('setup_meta for: ' + meta_div_id); + + // Count area. + var ms_attrs = { + id: meta_count_id, + //'class': 'label label-default pull-right' + //'class': 'label label-default' + 'class': 'badge' + }; + var ms = new html.tag('span', ms_attrs, 'n/a'); + + // Get a progress bar assembled. + var inspan = new html.tag('span', {'class': 'sr-only'}, '...'); + var indiv = new html.tag('div', {'class': 'progress-bar', + 'role': 'progressbar', + 'aria-valuenow': '100', + 'aria-valuemin': '0', + 'aria-valuemax': '100', + 'style': 'width: 100%;'}, + inspan); + spinner_div = + new html.tag('div', + {'generate_id': true, + 'class': + 'progress progress-striped active pull-right', + 'style': 'width: 3em;'}, + indiv); + + // The container area; add in the label and count. + var mdiv_args = { + 'class': 'well well-sm', + 'id': meta_div_id + }; + var mdiv = new html.tag('div', mdiv_args, + [this._meta_label, ms, spinner_div]); + + jQuery('#' + container_div.get_id()).append(mdiv.to_string()); + }; + if( this._display_meta_p ){ + this.setup_meta(); + } + + // Setup the free text query display under contructed tags for + // later population. + // + // If no icon_clear_source is defined, icon_clear_label will be + // used as the defining text. + this.setup_query = function(){ + ll('setup_query for: ' + query_input_div_id); + + // // Some defaults. + // if( ! label_str ){ label_str = ''; } + // // if( ! icon_clear_label ){ icon_clear_label = ''; } + // // if( ! icon_clear_source ){ icon_clear_source = ''; } + + // The incoming label. + var query_label_attrs = { + //'class': 'bbop-js-search-pane-query-label' + }; + var query_label_div = new html.tag('div', query_label_attrs); + + // The text area. + var ta_args = { + //'class': 'bbop-js-search-pane-textarea', + 'placeholder': this._free_text_placeholder, + 'class': 'form-control bbop-js-live-filters-textarea', + //'style': 'height: 1em;', + 'rows': '1', + 'id': query_input_div_id + }; + var query_area = new html.tag('textarea', ta_args); + + // Figure out an icon or a label. + var clear_query_obj = + //widget.display.clickable_object(icon_clear_label); + widget.display.clickable_object(null); + + // And a div to put it in. + var clear_div_attrs = { + //'class': 'bbop-js-search-pane-clear-button', + 'generate_id': true + }; + var clear_div = + new html.tag('div', clear_div_attrs, clear_query_obj); + + // General container div. + // NOTE/TODO: this is just a half panel--just wanted spacing. + var gen_div_attrs = { + 'class': 'panel panel-default', + 'generate_id': true + }; + var gen_div = new html.tag('div', gen_div_attrs); + + // Add to display. + query_label_div.add_to(clear_div.to_string()); + gen_div.add_to(query_label_div.to_string()); + gen_div.add_to(query_area.to_string()); + + jQuery('#' + container_div.get_id()).append(gen_div.to_string()); + }; + if( this._display_free_text_p ){ + this.setup_query(); + } + + // Setup sticky filters display under contructed tags for later + // population. The seeding information is coming in through the + // GOlr conf class. + this.setup_sticky_filters = function(){ + ll('setup_sticky_filters UI for class configuration: ' + + cclass.id()); + + // var stitle_attrs = { + // 'class': 'panel panel-heading', + // 'id': sticky_title_id + // }; + // var stitle = + // new html.tag('div', stitle_attrs, + // 'No applied sticky filters'); + + var scont_attrs = { + 'class': 'panel-body', + 'id': sticky_content_id + }; + var scont = + new html.tag('div', scont_attrs, + 'No applied sticky filters'); + + var sticky_filters_attrs = { + 'class': 'panel panel-default', + 'id': sticky_filters_div_id + }; + var sticky_filters_div = + //new html.tag('div', sticky_filters_attrs, [stitle, scont]); + new html.tag('div', sticky_filters_attrs, scont); + + // Add the output to the page. + var sticky_filters_str = sticky_filters_div.to_string(); + jQuery('#' + container_div.get_id()).append(sticky_filters_str); + }; + // Setup current filters display under contructed tags for later + // population. The seeding information is coming in through the + // GOlr conf class. + // + // Add in the filter state up here. + // + // If no icon_reset_source is defined, icon_reset_label will be + // used as the defining text. + this.setup_current_filters = function(){ + ll('setup_current_filters UI for class configuration: ' + + cclass.id()); + + var ccont_attrs = { + 'class': 'panel-body', + 'id': current_content_id + }; + var ccont = + new html.tag('div', ccont_attrs, + 'No applied user filters'); + + var current_filters_attrs = { + 'class': 'panel panel-default', + 'id': current_filters_div_id + }; + var current_filters_div = + //new html.tag('div', current_filters_attrs, [stitle, scont]); + new html.tag('div', current_filters_attrs, ccont); + + // Add the output to the page. + var current_filters_str = current_filters_div.to_string(); + jQuery('#' + container_div.get_id()).append(current_filters_str); + }; + // Setup the accordion skeleton under contructed tags for later + // population. The seeding information is coming in through the + // GOlr conf class. + // Start building the accordion here. Not an updatable part. + // + // If no icon_*_source is defined, icon_*_label will be + // used as the defining text. + this.setup_accordion = function(){ + ll('setup_accordion UI for class configuration: ' + + cclass.id()); + + var filter_accordion_attrs = { + id: filters_div_id + }; + filter_accordion_widget = // heavy lifting by special widget + new html.collapsible([], filter_accordion_attrs); + + // Add the sections with no contents as a skeleton to be + // filled by draw_accordion. + var field_list = cclass.field_order_by_weight('filter'); + each(field_list, function(in_field){ + ll('saw field: ' + in_field); + var ifield = cclass.get_field(in_field); + var in_attrs = { + id: in_field, + label: ifield.display_name(), + description: ifield.description() + }; + filter_accordion_widget.add_to(in_attrs, '', true); + }); + + // Add the output from the accordion to the page. + var accordion_str = filter_accordion_widget.to_string(); + jQuery('#' + container_div_id).append(accordion_str); + }; + if( this._display_accordion_p ){ + this.setup_current_filters(); + this.setup_sticky_filters(); + this.setup_accordion(); + } + + /// + /// Define the drawing callbacks, as well as the action hooks. + /// + + /* + * Function: draw_meta + * + * Draw meta results. Includes selector for drop down. + * + * (Re)draw the count control with the current information in the + * manager. This also tries to set the selector to the response + * number (to keep things in sync), unbinds any current "change" + * event, and adds a new change event. + * + * Parameters: + * response - the returned from the server + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_meta = function(response, manager){ + + ll('draw_meta for: ' + meta_div_id); + + // Collect numbers for display. + var total_c = response.total_documents(); + + // Draw meta; the current numbers and page--the same for + // every type of return. + // var ms_attrs = { + // //'class': 'label label-default pull-right' + // 'class': 'label label-default' + // }; + // var ms = new html.tag('div', ms_attrs, total_c); + jQuery('#' + meta_count_id).empty(); + jQuery('#' + meta_count_id).append(total_c); + // if( total_c === 0 ){ + // jQuery('#' + meta_div_id).append('No results found.'); + // }else{ + // } + // jQuery('#' + meta_div_id).append(ms.to_string()); + }; + if( this._display_meta_p ){ + manager.register('search', this.draw_meta, -1, 'meta_first'); + } + + // Detect whether or not a keyboard event is ignorable. + function _ignorable_event(event){ + + var retval = false; + + if( event ){ + var kc = event.keyCode; + if( kc ){ + if( kc === 39 || // right + kc === 37 || // left + kc === 32 || // space + kc === 20 || // ctl? + kc === 17 || // ctl? + kc === 16 || // shift + //kc == 8 || // delete + kc === 0 ){ // super + ll('ignorable key event: ' + kc); + retval = true; + } + } + } + return retval; + } + + /* + * Function: draw_query + * + * Draw the query widget. This function makes it active + * as well. + * + * Clicking the reset button will reset the query to ''. + * + * NOTE: Since this is part of the "persistant" interface (i.e. it + * does not get wiped after every call), we make sure to clear the + * event listeners when we redraw the function to prevent them from + * building up. + * + * Parameters: + * response - the returned from the server + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_query = function(response, manager){ + ll('draw_query for: ' + query_input_div_id); + + // Add a smartish listener. + jQuery('#' + query_input_div_id).unbind('keyup'); + jQuery('#' + query_input_div_id).keyup(function(event){ + + // If we're left with a legitimate event, handle it. + if( ! _ignorable_event(event) ){ + + // Can't ignore it anymore, so it goes into the + // manager for testing. + var tmp_q = manager.get_query(); + var input_text = jQuery(this).val(); + manager.set_query(input_text); + + // If the manager feels like it's right, trigger. + if( manager.sensible_query_p() ){ + ll('keeping set query: ' + input_text); + // Set the query to be more "usable" just + // before triggering (so the tests can't be + // confused by our switch). + manager.set_comfy_query(input_text); + manager.search(); + + // We are now searching--show it. + _spin_up(); + }else{ + ll('rolling back query: ' + tmp_q); + manager.set_query(tmp_q); + } + } + }); + + // Now reset the clear button and immediately set the event. + jQuery('#' + clear_query_span_id).unbind('click'); + jQuery('#' + clear_query_span_id).click(function(){ + manager.reset_query(); + //anchor.set_query_field(manager.get_query()); + manager.set_query_field(''); + manager.search(); + // We are now searching--show it. + _spin_up(); + }); + }; + if( this._display_free_text_p ){ + manager.register('search', this.draw_query, 0, 'query_first'); + } + + /* + * Function: draw_accordion + * + * (Re)draw the information in the accordion controls/filters. + * This function makes them active as well. + * + * Parameters: + * response - the returned from the server + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_accordion = function(response, manager){ + ll('draw_accordion for: ' + filters_div_id); + + // + // Make sure that accordion has already been inited. + if( typeof(filter_accordion_widget) === 'undefined' ){ + throw new Error('Need to init accordion widget to use it.'); + } + + // We'll need this in a little bit for calculating when to + // display the "more" option for the field filters. + var real_facet_limit = manager.get_facet_limit(); + var curr_facet_limit = real_facet_limit -1; // the facets we'll show + + // We want this so we can filter out any facets that have the + // same count as the current response total--these facets are + // pretty much information free. + var total_docs = response.total_documents(); + + // A helper function for when no filters are + // displayed. + function _nothing_to_see_here(in_field){ + var section_id = + filter_accordion_widget.get_section_id(in_field); + jQuery('#' + section_id).empty(); + jQuery('#' + section_id).append('Nothing to filter.'); + } + + // Hash where we collect our button information. + // button_id -> [source, filter, count, polarity]; + var button_hash = {}; + + // And a hash to store information to be able to generate the + // complete filter shields. + // span_id -> filter_id + var overflow_hash = {}; + + // Cycle through each facet field; all the items in each, + // create the lists and buttons (while collectong data useful + // in creating the callbacks) and put them into the accordion. + each(response.facet_field_list(), function(in_field){ + + var facet_bd = response.facet_field(in_field); + if( us.isEmpty(facet_bd) ){ + + // No filters means nothing in the box. + _nothing_to_see_here(in_field); + + }else{ + + // Create ul lists of the facet contents. + var tbl_id = mangle + 'filter-list-' + in_field; + var facet_list_tbl_attrs = { + 'class': 'table table-hover table-striped table-condensed', + 'id': tbl_id + }; + + var facet_list_tbl = + new html.table([], [], facet_list_tbl_attrs); + + ll("consider:" + in_field + ": " + + response.facet_field(in_field).length); + + // BUG/TODO: + // Count the number of redundant (not shown) + // facets so we can at least give a face to this + // bug/problem. + // Also filter out "empty filters". + var redundant_count = 0; + // Now go through and get filters and counts. + var good_count = 0; // only count when good + var overflow_p = false; // true when at 24 -> 25 + each(response.facet_field(in_field), function(ff_field, ff_index){ + + // Pull out info early so we can test it + // for information content. + var f_name = ff_field[0]; + var f_count = ff_field[1]; + + // ll(in_field + ": " + f_name + ": " + + // [f_count, + // total_docs, + // ff_index, + // good_count, + // redundant_count, + // real_facet_limit].join(', ')); + + // TODO: The field is likely redundant + // (BUG: not always true in closures), + // so eliminate it. + if( f_count === total_docs ){ + //ll("\tnothing here"); + redundant_count++; + }else if( ! f_name || f_name === "" ){ + // Straight out skip if it is an + // "empty" facet field. + }else if( ff_index < real_facet_limit -1 ){ + //ll("\tgood row"); + good_count++; + + // Create buttons and store them for later + // activation with callbacks to + // the manager. + var b_plus = new html.button( + ui_icon_positive_label, + { + 'generate_id': true, + 'type': 'button', + 'class': + 'btn btn-success btn-xs' + }); + var b_minus = + new html.button( + ui_icon_negative_label, + { + 'generate_id': true, + 'type': 'button', + 'class': + 'btn btn-danger btn-xs' + }); + + // Store in hash for later keying to + // event. + button_hash[b_plus.get_id()] = + [in_field, f_name, f_count, '+']; + button_hash[b_minus.get_id()] = + [in_field, f_name, f_count, '-']; + + // // Add the label and buttons to the + // // appropriate ul list. + //facet_list_ul.add_to( + // fstr,b_plus.to_string(), + // b_minus.to_string()); + // Add the label and buttons to the table. + facet_list_tbl.add_to([f_name, + '('+ f_count+ ')', + b_plus.to_string(), + b_minus.to_string() + ]); + } + + // This must be logically separated from + // the above since we still want to show + // more even if all of the top 25 are + // redundant. + if( ff_index === real_facet_limit -1 ){ + // Add the more button if we get up to + // this many facet rows. This should + // only happen on the last possible + // iteration. + + overflow_p = true; + //ll( "\tadd [more]"); + + // Since this is the overflow item, + // add a span that can be clicked on + // to get the full filter list. + //ll("Overflow for " + in_field); + var b_over = new html.button( + 'more...', + { + 'generate_id': true, + 'type': 'button', + 'title': + 'Display the complete list', + 'class': + 'btn btn-primary btn-xs' + }); + facet_list_tbl.add_to([b_over.to_string(), + '', '']); + overflow_hash[b_over.get_id()] = in_field; + } + }); + + // There is a case when we have filtered out all + // avilable filters (think db source). + if( good_count === 0 && ! overflow_p ){ + _nothing_to_see_here(in_field); + }else{ + // Otherwise, now add the ul to the + // appropriate section of the accordion in + // the DOM. + var sect_id = + filter_accordion_widget.get_section_id(in_field); + jQuery('#' + sect_id).empty(); + + // TODO/BUG: + // Give warning to the redundant facets. + var warn_txt = null; + if( redundant_count === 1 ){ + warn_txt = "field is"; + }else if( redundant_count > 1 ){ + warn_txt = "fields are"; + } + if( warn_txt ){ + jQuery('#' + sect_id).append( + " The top (" + redundant_count + + ") redundant " + warn_txt + " not shown" + + ""); + + } + + // Add facet table. + var final_tbl_str = facet_list_tbl.to_string(); + jQuery('#' + sect_id).append(final_tbl_str); + } + } + }); + + // Okay, now introducing a function that we'll be using a + // couple of times in our callbacks. Given a button id (from + // a button hash) and the [field, filter, count, polarity] + // values from the props, make a button-y thing an active + // filter. + function filter_select_live(create_time_button_props, button_id){ + //var bid = button_id; + //var in_field = create_time_button_props[0]; + //var in_filter = create_time_button_props[1]; + //var in_count = create_time_button_props[2]; + var in_polarity = create_time_button_props[3]; + + // Decide on the button graphical elements. + var b_ui_icon = 'ui-icon-plus'; + if( in_polarity === '-' ){ + b_ui_icon = 'ui-icon-minus'; + } + var b_ui_props = { + icons: { primary: b_ui_icon}, + text: false + }; + + // Create the button and immediately add the event. + //jQuery('#' + button_id).button(b_ui_props).click( + jQuery('#' + button_id).click(function(){ + var tid = jQuery(this).attr('id'); + var call_time_button_props = button_hash[tid]; + var call_field = call_time_button_props[0]; + var call_filter = call_time_button_props[1]; + //var in_count = button_props[2]; + var call_polarity = call_time_button_props[3]; + + // Change manager and fire. + // var bstr =call_field+' '+call_filter+' '+call_polarity; + // alert(bstr); + manager.add_query_filter(call_field, call_filter, + [call_polarity]); + manager.search(); + // We are now searching--show it. + _spin_up(); + }); + } + + // Now let's go back and add the buttons, styles, + // events, etc. in the main accordion section. + each(button_hash, filter_select_live); + + // Next, tie the events to the "more" spans. + each(overflow_hash, function(filter_name, button_id){ + jQuery('#' + button_id).click( + + // On click, set that one field to limitless in + // the manager, setup a shield, and wait for the + // callback. + function(){ + + // Recover the field name. + var tid = jQuery(this).attr('id'); + var call_time_field_name = overflow_hash[tid]; + //alert(call_time_field_name); + + // Set the manager to no limit on that field and + // only rturn the information that we want. + manager.set_facet_limit(0); + manager.set_facet_limit(call_time_field_name, -1); + var curr_row = manager.get('rows'); + manager.set('rows', 0); + + // Create the shield and pop-up the + // placeholder. + var fs = widget.display.filter_shield; + var filter_shield = new fs(ui_spinner_shield_source, + ui_spinner_shield_message); + filter_shield.start_wait(); + + // Open the populated shield. + function draw_shield(resp){ + + // ll("shield what: " + bbop.core.what_is(resp)); + // ll("shield resp: " + bbop.core.dump(resp)); + + // First, extract the fields from the + // minimal response. + var fina = call_time_field_name; + var flist = resp.facet_field(call_time_field_name); + + // Draw the proper contents of the shield. + filter_shield.draw(fina, flist, manager); + } + manager.fetch(draw_shield); + + // Reset the manager to more sane settings. + manager.reset_facet_limit(); + manager.set('rows', curr_row); + }); + }); + + ll('Done current accordion for: ' + filters_div_id); + }; + + /* + * Function: draw_current_filters + * + * (Re)draw the information on the current filter set. + * This function makes them active as well. + * + * Parameters: + * response - the returned from the server + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_current_filters = function(response, manager){ + ll('draw_current_filters for: ' + current_filters_div_id); + + /// + /// Add in the actual HTML for the filters and buttons. While + /// doing so, tie a unique id to the filter--we'll use that + /// later on to add buttons and events to them. + /// + + // First, we need to make the filter clear button for the top + // of the table. + var b_cf = new html.button('×', + { + 'type': 'button', + 'id': + clear_user_filter_span_id, + 'class': + 'btn btn-danger btn-xs', + 'title': + 'Clear all user filters' + }); + + var in_query_filters = response.query_filters(); + //var sticky_query_filters = manager.get_sticky_query_filters(); + ll('filters: ' + bbop.core.dump(in_query_filters)); + var fq_list_tbl = new html.table( + ['', 'User filters', b_cf.to_string()], + [], + // {'class': 'bbop-js-search-pane-filter-table'}); + {'class': 'table table-hover table-striped table-condensed'}); + var has_fq_p = false; // assume there are no filters to begin with + var button_hash = {}; + each(in_query_filters, function(field_vals, field){ + each(field_vals, function(polarity, field_val){ + + // Make note of stickiness, skip adding if sticky. + var qfp = manager.get_query_filter_properties(field, + field_val); + if( ! qfp || qfp['sticky_p'] === false ){ + + // Note the fact that we actually have a + // query filter to work with and display. + has_fq_p = true; + + // Boolean value to a character. + var polstr = '−'; + if( polarity ){ polstr = '+'; } + + + // Argh! Real jQuery buttons are way too slow! + // var b = new html.button('remove filter', + // {'generate_id': true}); + + // Generate a button with a unique id. + var b = new html.button( + ui_icon_remove_label, + { + 'generate_id': true, + 'type': 'button', + 'title': 'Remove filter', + 'class': + 'btn btn-danger btn-xs' + }); + + // Tie the button it to the filter for + // jQuery and events attachment later on. + var bid = b.get_id(); + button_hash[bid] = [polstr, field, field_val]; + + //ll(label_str +' '+ bid); + //fq_list_tbl.add_to(label_str +' '+ b.to_string()); + fq_list_tbl.add_to([''+ polstr +'', + field + ': ' + field_val, + b.to_string()]); + //label_str +' '+ b.to_string()); + } + }); + }); + + // Either add to the display, or display the "empty" message. + var cfid = '#' + current_content_id; + jQuery(cfid).empty(); + if( ! has_fq_p ){ + jQuery(cfid).append("No current user filters."); + }else{ + + // With this, the buttons will be attached to the + // DOM... + jQuery(cfid).append(fq_list_tbl.to_string()); + + // First, lets add the reset for all of the filters. + jQuery('#' + b_cf.get_id()).click( + function(){ + manager.reset_query_filters(); + manager.search(); + // We are now searching--show it. + _spin_up(); + } + ); + + // Now let's go back and add the buttons, styles, + // events, etc. to the filters. + each(button_hash, function(pass, button_id){ + var bid = button_id; + + // // Get the button. + // var bprops = { + // icons: { primary: "ui-icon-close"}, + // text: false + // }; + // Create the button and immediately add the event. + //jQuery('#' + bid).button(bprops).click( + jQuery('#' + bid).click(function(){ + var tid = jQuery(this).attr('id'); + var button_props = button_hash[tid]; + var polstr = button_props[0]; + var field = button_props[1]; + var value = button_props[2]; + + // Change manager and fire. + // var lstr = polstr +' '+ field +' '+ value; + // alert(lstr); + // manager.remove_query_filter(field,value, + // [polstr, '*']); + manager.remove_query_filter(field, value); + manager.search(); + // We are now searching--show it. + _spin_up(); + }); + }); + } + }; + + /* + * Function: draw_sticky_filters + * + * (Re)draw the information on the sticky filter set. + * + * Parameters: + * response - the returned from the server + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_sticky_filters = function(response, manager){ + ll('draw_sticky_filters for: ' + sticky_filters_div_id); + + // Add in the actual HTML for the pinned filters and buttons. + var sticky_query_filters = manager.get_sticky_query_filters(); + ll('sticky filters: ' + bbop.core.dump(sticky_query_filters)); + var fq_list_tbl = new html.table( + ['', 'Your search is pinned to these filters'], + [], + {'class': 'table table-hover table-striped table-condensed'}); + // [{'filter': A, 'value': B, 'negative_p': C, 'sticky_p': D}, ...] + each(sticky_query_filters, function(fset){ + + // + var sfield = fset['filter']; + var sfield_val = fset['value']; + + // Boolean value to a character. + var polarity = fset['negative_p']; + var polstr = '−'; + if( polarity ){ polstr = '+'; } + + // Generate a button with a unique id. + var label_str = polstr + ' ' + sfield + ':' + sfield_val; + fq_list_tbl.add_to([''+ polstr +'', + sfield + ': ' + sfield_val]); + }); + + // Either add to the display, or display the "empty" message. + //var sfid = '#' + sticky_filters_div_id; + var sfid = '#' + sticky_content_id; + jQuery(sfid).empty(); + if( sticky_query_filters.length === 0 ){ + jQuery(sfid).append("No sticky filters."); + }else{ + // Attach to the DOM... + jQuery(sfid).append(fq_list_tbl.to_string()); + } + }; + + if( this._display_accordion_p ){ + manager.register('search', this.draw_accordion, 1, + 'accordion_first'); + manager.register('search', this.draw_current_filters, 2, + 'current_first'); + manager.register('search', this.draw_sticky_filters, 3, + 'sticky_first'); + } + + /* + * Function: draw_error + * + * Somehow report an error to the user. + * + * Parameters: + * error_message - a string(?) describing the error + * manager - that we initially registered with + * + * Returns: + * n/a + */ + this.draw_error = function(error_message, manager){ + ll("draw_error: " + error_message); + alert("Runtime error: " + error_message); + _spin_down(); + }; + manager.register('error', this.draw_error, 0, 'error_first'); + + // + function spin_down_wait(){ + _spin_down(); + } + manager.register('search', spin_down_wait, -100, 'donedonedone'); + + // Start the ball with a reset event. + //manager.search(); + + // The display has been established. + anchor._established_p = true; + }; +}; + +/// +/// Exportable body. +/// + +module.exports.live_filters = live_filters; diff --git a/lib/live_pager.js b/lib/live_pager.js new file mode 100644 index 0000000..a4490a1 --- /dev/null +++ b/lib/live_pager.js @@ -0,0 +1,333 @@ +/* + * Package: live_pager.js + * + * Namespace: bbop-widget-set.live_pager + * + * BBOP JS object to allow the the paging/downloading etc. of a GOlr + * manager. + * + * Very much like a separated pager from the search pane. + * + * This is a Bootstrap 3 widget. + * + * See Also: + * + * + * + */ + +var us = require('underscore'); +var bbop = require('bbop-core'); + +// Code here will be ignored by JSHint, as we are technically +// "redefining" jQuery (although we are not). +/* jshint ignore:start */ +var jQuery = require('jquery'); +/* jshint ignore:end */ + +/* + * Constructor: live_pager + * + * Contructor for the bbop-widget-set.live_pager object. + * + * Display a manager response. Not a manager itself, but can use the + * argument manager to page, download, etc. + * + * Arguments: + * interface_id - string id of the element to build on + * manager - a manager object to probe for display and use for remoting + * in_argument_hash - *[optional]* optional hash of optional arguments + * + * Returns: + * this object + */ +var live_pager = function(interface_id, manager, in_argument_hash){ + this._is_a = 'bbop-widget-set.live_pager'; + + var anchor = this; + var each = us.each; + function ll(str){ console.log(str); } + + // Some top-level variable defined. + var ui_count_control_div_id = + interface_id + '_countctl_div_' + bbop.core.uuid(); + var external_button_location_id = 'pager_button_holder_' + bbop.core.uuid(); + + // Handle incoming arguements. + var default_hash = { + 'callback_priority': 0, + 'selection_counts': [10, 25, 50, 100] + }; + var folding_hash = in_argument_hash || {}; + var arg_hash = bbop.core.fold(default_hash, folding_hash); + // + var callback_priority = arg_hash['callback_priority']; + var selection_counts = arg_hash['selection_counts']; + + // Last things last, bind to the manager. + // TODO/BUG: Should this actually happen outside the widget? How + // complicated is this really? + var fun_id = bbop.core.uuid(); + manager.register('search', _repaint_on_callback, callback_priority, fun_id); + + // Add the "disabled" property to a button if the boolean + // value says so. + function _disable_if(bttn, disbool){ + if( disbool ){ + jQuery('#' + bttn.get_id()).attr('disabled','disabled'); + } + } + + // (Re)draw the count control with the current information in the + // manager. This also tries to set the selector to the response + // number (to keep things in sync), unbinds any current "change" + // event, and adds a new change event. + function _repaint_on_callback(response, manager){ + + //ll('draw live_pager at: ' + interface_id); + + /// + /// Section 1: the numbers display. + /// + + // Collect numbers for display. + var total_c = response.total_documents(); + var first_d = response.start_document(); + var last_d = response.end_document(); + + // Draw meta; the current numbers and page--the same for + // every type of return. + jQuery('#' + interface_id).empty(); + if( total_c === 0 ){ + jQuery('#' + interface_id).append('No results found.'); + }else{ + + // A top-level div to contain the literal meta results, and the count + // selector next to them. + var mdiv_attrs = { + 'class': 'row', + 'generate_id': true + }; + var mdiv = new html.tag('div', mdiv_attrs); + + // The literal return metadata. + var dmeta_attrs = { + //'class': 'bbop-js-search-pane-meta' + 'generate_id': true, + 'class': 'col-xs-6 col-sm-6 col-md-4 col-lg-4' + }; + var dmeta = new html.tag('div', dmeta_attrs); + dmeta.add_to('
Total: ' + total_c + + '; showing: ' + first_d + + '-' + last_d + '
'); + mdiv.add_to(dmeta); + + /// + /// Section 2: results count control. + /// + + // Create a text label. + var sel_label_attrs = { + //'for': ui_count_control_div_id, + // 'generate_id': true, + //'class': 'control-label' + }; + var sel_label = new html.tag('span', sel_label_attrs, + 'Results count  '); + + // Create inputs (the current order is important for proper + // for/id creation). + var cinputs = []; + each(selection_counts, function(num, cindex){ + // Create and store the option. + var sel_input_attrs = { + 'generate_id': true, + 'value': num + }; + var sel_input = new html.tag('option', sel_input_attrs, num); + var sel_input_id = sel_input.get_id(); + cinputs.push(sel_input); + }); + // Option container div. + var sel_attrs = { + 'id': ui_count_control_div_id, + //'class': 'form-control', + 'style': 'width: 5em;' + }; + var sel = new html.tag('select', sel_attrs, cinputs); + + // Container div. + var sel_div_attrs = { + 'generate_id': true + //'class': 'col-xs-6 col-sm-6 col-md-3 col-lg-3' + //'class': 'form-group' + //'style': 'width: 7em;' + }; + var sel_div = new html.tag('div', sel_div_attrs); + + // Assemble these elements into the UI. + sel_div.add_to(sel_label); + sel_div.add_to(sel); + //mdiv.add_to(sel_div); + dmeta.add_to(sel_div); + + // Render out the last two sections. + jQuery('#' + interface_id).append(mdiv.to_string()); + + /// + /// Section 3: results count activity, setting. + /// + + // First, unbind so we don't accidentally trigger with any + // changes and don't pile up event handlers. + jQuery('#' + ui_count_control_div_id).unbind('change'); + + // Next, pull out the number of rows requested. + var step = response.row_step(); + + // Set the value to the number. + jQuery('#' + ui_count_control_div_id).val(step); + + // Finally, reactivate the event handler on the select. + jQuery('#' + ui_count_control_div_id).change( + function(event, ui){ + var sv = jQuery('#' + ui_count_control_div_id).val(); + if( bbop.core.is_defined(sv) ){ + // Convert to a number. + var si = parseInt(sv); + + // Set manager and to the search. + manager.set_results_count(si); + manager.search(); + // We are now searching--show it. + //_spin_up(); + } + }); + + /// + /// Section 4: the paging buttons. + /// + + var bdiv_attrs = { + 'class': 'col-xs-12 col-sm-12 col-md-8 col-lg-8', + 'generate_id': true + }; + var bdiv = new html.tag('div', bdiv_attrs); + //jQuery('#' + interface_id).append(bdiv.to_string()); + jQuery('#' + mdiv.get_id()).append(bdiv.to_string()); + var bdiv_id = bdiv.get_id(); + + // Now add the raw buttons to the interface, and after this, + // activation and adding events. + var bopts = { + 'generate_id': true, + 'class': 'btn btn-primary' + }; + var b_first = new html.button('«First', bopts); + //jQuery('#' + interface_id).append(b_first.to_string()); + jQuery('#' + bdiv_id).append(b_first.to_string()); + var b_back = new html.button('<Prev', bopts); + //jQuery('#' + interface_id).append(b_back.to_string()); + jQuery('#' + bdiv_id).append(b_back.to_string()); + var b_forward = new html.button('Next>', bopts); + //jQuery('#' + interface_id).append(b_forward.to_string()); + jQuery('#' + bdiv_id).append(b_forward.to_string()); + var b_last = new html.button('Last»', bopts); + //jQuery('#' + interface_id).append(b_last.to_string()); + jQuery('#' + bdiv_id).append(b_last.to_string()); + + // Do the math about what buttons to activate. + var b_first_disabled_p = false; + var b_back_disabled_p = false; + var b_forward_disabled_p = false; + var b_last_disabled_p = false; + + // Only activate paging if it is necessary to the returns. + if( ! response.paging_p() ){ + b_first_disabled_p = true; + b_back_disabled_p = true; + b_forward_disabled_p = true; + b_last_disabled_p = true; + } + + // Don't activate back on the first page. + if( ! response.paging_previous_p() ){ + b_first_disabled_p = true; + b_back_disabled_p = true; + } + + // Don't activate next on the last page. + if( ! response.paging_next_p() ){ + b_forward_disabled_p = true; + b_last_disabled_p = true; + } + + // First page button. + _disable_if(b_first, b_first_disabled_p); + jQuery('#' + b_first.get_id()).click(function(){ + // Cheat and trust reset by proxy to work. + manager.page_first(); + // We are now searching--show it. + //_spin_up(); + }); + + // Previous page button. + _disable_if(b_back, b_back_disabled_p); + jQuery('#' + b_back.get_id()).click(function(){ + manager.page_previous(); + // We are now searching--show it. + //_spin_up(); + }); + + // Next page button. + _disable_if(b_forward, b_forward_disabled_p); + jQuery('#' + b_forward.get_id()).click(function(){ + manager.page_next(); + // We are now searching--show it. + //_spin_up(); + }); + + // Last page button. + _disable_if(b_last, b_last_disabled_p); + jQuery('#' + b_last.get_id()).click(function(){ + // A little trickier. + manager.page_last(total_c); + // We are now searching--show it. + //_spin_up(); + }); + + /// + /// Section 5: make a place for external buttons. + /// + + var holder_attrs = { + 'id': external_button_location_id + }; + var holder = new html.tag('span', holder_attrs); + jQuery('#' + bdiv_id).append(' ' + holder.to_string()); + } + } + + + /* + * Function: button_span_id + * + * Returns the location of a place to add external buttons if you + * want. + * + * Parameters: + * n/a + * + * Returns + * string rep of place to put external buttons (span tag) + */ + anchor.button_span_id = function(){ + return external_button_location_id; + }; +}; + +/// +/// Exportable body. +/// + +module.exports.live_pager = live_pager; diff --git a/lib/live_results.js b/lib/live_results.js new file mode 100644 index 0000000..0f2c850 --- /dev/null +++ b/lib/live_results.js @@ -0,0 +1,320 @@ +/* + * Package: live_results.js + * + * Namespace: bbop-widget-set.live_results + * + * BBOP JS widget to display the results of a search on callback. + * + * TODO: Button insertion in other non-internal places. + * + * This is a Bootstrap 3 widget. + */ + +var us = require('underscore'); +var bbop = require('bbop-core'); + +// Code here will be ignored by JSHint, as we are technically +// "redefining" jQuery (although we are not). +/* jshint ignore:start */ +var jQuery = require('jquery'); +/* jshint ignore:end */ + +/* + * Constructor: live_results + * + * Contructor for the bbop-widget-set.live_results object. + * + * Results table and optional buttons. + * + * Optional options looks like: + * callback_priority - default 0 + * user_buttons - default [], should be any passable renderable button + * user_buttons_div_id - default null + * selectable_p - have selectable side buttons (default true) + * + * Arguments: + * interface_id - string id of the element to build on + * manager - the shared GOlr manager to use + * conf_class - the profile of the specific conf to use + * handler - handler to use in rendering + * linker - linker to use in rendering + * in_argument_hash - *[optional]* optional hash of optional arguments, described above + * + * Returns: + * this object + */ +var live_results = function(interface_id, manager, conf_class, + handler, linker, in_argument_hash){ + this._is_a = 'bbop-widget-set.live_results'; + + var anchor = this; + var each = us.each; + + // Per-UI logger. + var logger = new bbop.logger(); + logger.DEBUG = false; + //logger.DEBUG = true; + function ll(str){ logger.kvetch('LR: ' + str); } + + var results_table = null; + + // Capture the last response for downstream widgets. + var last_response = null; + + // Some top-level variable defined. + // Special id and names for optional select column. + var local_mangle = bbop.core.uuid(); + var select_column_id = 'rtbcc_select_' + local_mangle; + var select_item_name = 'rtbcc_select_name_' + local_mangle; + + /// + /// Deal with incoming arguments. + /// + + // Our argument default hash. + var default_hash = { + 'callback_priority': 0, + 'user_buttons': [], + 'user_buttons_div_id': null, + 'selectable_p': true + }; + var folding_hash = in_argument_hash || {}; + var arg_hash = bbop.core.fold(default_hash, folding_hash); + // + var callback_priority = arg_hash['callback_priority']; + var user_buttons = arg_hash['user_buttons']; + var user_buttons_div_id = arg_hash['user_buttons_div_id']; + var selectable_p = arg_hash['selectable_p']; + + // + var fun_id = bbop.core.uuid(); + + /// + /// Set the callbacks. + /// + + // Add the "disabled" property to a button if the boolean + // value says so. + function _disable_if(bttn, disbool){ + if( disbool ){ + jQuery('#' + bttn.get_id()).attr('disabled','disabled'); + } + } + + // (Re)draw the user-defined buttons in the meta + // information area. Will naturally fail if there is no + // meta div that has been nested with the user button + // element. + function _draw_user_buttons(button_definitions, loc_id){ + + function _button_rollout(button_def_hash){ + var default_hash = { + label : '?', + disabled_p : false, + click_function_generator : + function(anchor, manager){ + return function(anchor, manager){ + alert('No callback defined for this button--' + + 'the generator may have been empty!'); + }; + } + }; + var folding_hash = button_def_hash || {}; + var arg_hash = bbop.core.fold(default_hash, folding_hash); + + var label = arg_hash['label']; + var disabled_p = arg_hash['disabled_p']; + var click_function_generator = arg_hash['click_function_generator']; + + /// Add button to DOM. + var b_props = { + 'generate_id': true, + 'class': 'btn btn-primary' + }; + var b = new html.button(label, b_props); + jQuery('#' + loc_id).append(b.to_string()); + _disable_if(b, disabled_p); + + // Bind function to action. + var click_fun = click_function_generator(anchor, manager); + jQuery('#' + b.get_id()).click(click_fun); + } + + // Check that we're not about to do the impossible. + if( ! jQuery('#' + loc_id) ){ + alert('cannot refresh buttons without a place to draw them'); + }else{ + jQuery('#' + loc_id).empty(); + each(button_definitions, _button_rollout); + } + } + + // Draw a table at the right place or an error message. + function _draw_table_or_something(resp, manager){ + + // Wipe interface. + jQuery('#' + interface_id).empty(); + + // Vary by what we got. + if( ! resp.success() || resp.total_documents() === 0 ){ + jQuery('#' + interface_id).append('No results given your input and search fields. Please refine and try again.'); + last_response = null; + }else{ + last_response = resp; + + // Render the buttons. + //console.log('user_buttons: ', user_buttons); + if( user_buttons && user_buttons.length && user_buttons.length > 0 ){ + + // Ensure we have somewhere to put our buttons. If not + // supplied with an injection id, make our own and use + // it. + var insert_div_id = user_buttons_div_id; + if( ! user_buttons_div_id ){ + + // Generate new dic and add it to the display. + var ubt_attrs = { + 'generate_id': true + }; + var ubt = new html.tag('div', ubt_attrs); + jQuery('#' + interface_id).append(ubt.to_string()); + + // Ensure the id. + insert_div_id = ubt.get_id(); + } + + // Add all of the defined buttons after the spacing. + _draw_user_buttons(user_buttons, insert_div_id); + } + + // Display results. + var bwd = widget.display; + results_table = + new bwd.results_table_by_class_conf_b3(conf_class, resp, linker, + handler, interface_id, + selectable_p, + select_column_id, + select_item_name); + } + } + manager.register('search', _draw_table_or_something, + callback_priority, fun_id); + + // Somehow report an error to the user. + // error_message - a string(?) describing the error + // manager - that we initially registered with + function _draw_error(error_message, manager){ + ll("draw_error: " + error_message); + alert("Runtime error: " + error_message); + //_spin_down(); + } + manager.register('error', _draw_error, callback_priority, fun_id); + + /// + /// External API. + /// + + /* + * Function: item_name + * + * Return a string of the name attribute used by the checkboxes if + * we selected for checkboxes to be displayed. + * + * Parameters: + * n/a + * + * Returns: + * string or null if displaying checkboxes was false + */ + this.item_name = function(){ + return select_item_name; + }; + + /* + * Function: toggle_id + * + * Return a string of the id of the checkbox in the header if we + * selected for checkboxes to be displayed. + * + * Parameters: + * n/a + * + * Returns: + * string or null if displaying checkboxes was false + */ + this.toggle_id = function(){ + return select_column_id; + }; + + /* + * Function: last_response + * + * The response for the last call. If the call was not successful + * or returned no documents, this will be null. + * + * Parameters: + * n/a + * + * Returns: + * response or null + */ + this.last_response = function(){ + return last_response; + }; + + /* + * Function: get_selected_items + * + * The idea is to return a list of the items selected (with + * checkboxes) in the display. This means that there are three + * possibilities. 1) We are not using checkboxes or the display + * has not been established, so we return null; 2) no or all items + * have been selected, so we get back an empty list (all === none + * in our view); 3) a subset list of strings (ids). + * + * NOTE: Naturally, does not function until the display is established. + * + * Parameters: + * n/a + * + * Returns + * string list or null + */ + this.get_selected_items = function(){ + var retval = null; + + // + if( selectable_p ){ + retval = []; + + // Cycle through and pull out the values of the checked + // ones. + var total_count = 0; + var nstr = 'input[name=' + select_item_name + ']'; + jQuery(nstr).each(function(){ + if( this.checked ){ + var val = jQuery(this).val(); + retval.push(val); + } + total_count++; + }); + + // If we are selecting all of the items on this page, that + // is the same as not selecting any in our world, so reset + // and warn. + if( total_count > 0 && total_count === retval.length ){ + alert('You can "select" all of the items on a results page by not selecting any (all being the default). This will also get your results processed faster and cause significantly less overhead on the servers.'); + retval = []; + } + } + + return retval; + }; + +}; + +/// +/// Exportable body. +/// + +module.exports.live_results = live_results; diff --git a/lib/set.js b/lib/set.js index ba14fd5..89c085c 100644 --- a/lib/set.js +++ b/lib/set.js @@ -37,4 +37,15 @@ var set = function(more_dispatch){ /// module.exports.set = set; -module.exports.autocomplete_simple = require('./autocomplete_simple'); + +try { + module.exports.autocomplete_simple = require('./autocomplete_simple'); + module.exports.live_filters = require('./live_filters'); + module.exports.live_pager = require('./live_pager'); + module.exports.live_results = require('./live_results'); +}catch (e){ + //console.error(e); + console.error('vvvvv'); + console.error('WARNING/BUG/TODO: Testing (?), so caught error when trying to pull in jQuery, as it fails on 1.9.1. This is solved for deployment using browserify. At some point need to upgrade to have an actually usable jQuery version in a node/npm environment, or at least a better behaved one.'); + console.error('^^^^^'); +}