$.extend(edges, { /////////////////////////////////////////////////// // Selector implementations newRefiningANDTermSelector : function(params) { return edges.instantiate(edges.RefiningANDTermSelector, params, edges.newSelector); }, RefiningANDTermSelector : function(params) { //////////////////////////////////////////// // configurations to be passed in // how many terms should the facet limit to this.size = params.size || 10; // which ordering to use term/count and asc/desc this.orderBy = params.orderBy || "count"; this.orderDir = params.orderDir || "desc"; // number of facet terms below which the facet is disabled this.deactivateThreshold = params.deactivateThreshold || false; // should the terms facet ignore empty strings in display this.ignoreEmptyString = params.ignoreEmptyString || true; // should filters defined in the baseQuery be excluded from the selector this.excludePreDefinedFilters = params.excludePreDefinedFilters || true; // provide a map of values for terms to displayable terms, or a function // which can be used to translate terms to displyable values this.valueMap = params.valueMap || false; this.valueFunction = params.valueFunction || false; // function to parse the value selected (which will be a string) into whatever // datatype the filter requires this.parseSelectedValueString = edges.getParam(params.parseSelectedValueString, false); // function to convert the filter value to the same type as the aggregation value, if they // differ (e.g. if the filter is `true` but the agg value is `1` this function can convert // between them. this.filterToAggValue = edges.getParam(params.filterToAggValue, false); // due to a limitation in elasticsearch's clustered node facet counts, we need to inflate // the number of facet results we need to ensure that the results we actually want are // accurate. This option tells us by how much. this.inflation = params.inflation || 100; // override the parent's defaultRenderer this.defaultRenderer = params.defaultRenderer || "newRefiningANDTermSelectorRenderer"; this.active = edges.getParam(params.active, true); // whether this component updates itself on every request, or whether it is static // throughout its lifecycle. One of "update" or "static" this.lifecycle = edges.getParam(params.lifecycle, "update"); ////////////////////////////////////////// // properties used to store internal state // filters that have been selected via this component // {display: <display>, term: <term>} this.filters = []; // values that the renderer should render // wraps an object (so the list is ordered) which in turn is the // { display: <display>, term: <term>, count: <count> } this.values = false; ////////////////////////////////////////// // overrides on the parent object's standard functions this.init = function(edge) { // first kick the request up to the superclass edges.up(this, "init", [edge]); if (this.lifecycle === "static") { this.listAll(); } }; this.contrib = function(query) { var params = { name: this.id, field: this.field, orderBy: this.orderBy, orderDir: this.orderDir }; if (this.size) { params["size"] = this.size } query.addAggregation( es.newTermsAggregation(params) ); }; this.synchronise = function() { // reset the state of the internal variables if (this.lifecycle === "update") { // if we are in the "update" lifecycle, then reset and read all the values this.values = []; if (this.edge.result) { this._readValues({result: this.edge.result}); } } this.filters = []; // extract all the filter values that pertain to this selector var filters = this.edge.currentQuery.listMust(es.newTermFilter({field: this.field})); for (var i = 0; i < filters.length; i++) { var val = filters[i].value; if (this.filterToAggValue) { val = this.filterToAggValue(val); } let term = val; val = this._translate(val); this.filters.push({display: val, term: term}); } }; this._readValues = function(params) { var result = params.result; // assign the terms and counts from the aggregation var buckets = result.buckets(this.id); if (this.deactivateThreshold) { if (buckets.length < this.deactivateThreshold) { this.active = false } else { this.active = true; } } // list all of the pre-defined filters for this field from the baseQuery var predefined = []; if (this.excludePreDefinedFilters && this.edge.baseQuery) { predefined = this.edge.baseQuery.listMust(es.TermFilter({field: this.field})); } var realCount = 0; for (var i = 0; i < buckets.length; i++) { var bucket = buckets[i]; // ignore empty strings if (this.ignoreEmptyString && bucket.key === "") { continue; } // ignore pre-defined filters if (this.excludePreDefinedFilters) { var exclude = false; for (var j = 0; j < predefined.length; j++) { var f = predefined[j]; let filterValue = f.value; if (this.filterToAggValue) { filterValue = this.filterToAggValue(f.value) } if (bucket.key === filterValue) { exclude = true; break; } } if (exclude) { continue; } } // if we get to here we're going to add this to the values, so // increment the real count realCount++; // we must cut off at the set size, as there may be more // terms that we care about if (realCount > this.size) { break; } // translate the term if necessary var key = this._translate(bucket.key); // store the original value and the translated value plus the count var obj = {display: key, term: bucket.key, count: bucket.doc_count}; this.values.push(obj); } }; ///////////////////////////////////////////////// // query handlers for getting the full list of terms to display this.listAll = function() { // to list all possible terms, build off the base query var bq = this.edge.cloneBaseQuery(); bq.clearAggregations(); bq.size = 0; // now add the aggregation that we want var params = { name: this.id, field: this.field, orderBy: this.orderBy, orderDir: this.orderDir, size: this.size }; bq.addAggregation( es.newTermsAggregation(params) ); // issue the query to elasticsearch this.edge.queryAdapter.doQuery({ edge: this.edge, query: bq, success: edges.objClosure(this, "listAllQuerySuccess", ["result"]), error: edges.objClosure(this, "listAllQueryFail") }); }; this.listAllQuerySuccess = function(params) { var result = params.result; // set the values according to what comes back this.values = []; this._readValues({result: result}); // since this happens asynchronously, we may want to draw this.draw(); }; this.listAllQueryFail = function() { this.values = []; console.log("RefiningANDTermSelector asynchronous query failed"); }; ////////////////////////////////////////// // functions that can be called on this component to change its state this.selectTerm = function(term) { if (this.parseSelectedValueString) { term = this.parseSelectedValueString(term); } var nq = this.edge.cloneQuery(); // first make sure we're not double-selecting a term var removeCount = nq.removeMust(es.newTermFilter({ field: this.field, value: term })); // all we've done is remove and then re-add the same term, so take no action if (removeCount > 0) { return false; } // just add a new term filter (the query builder will ensure there are no duplicates) // this means that the behaviour here is that terms are ANDed together nq.addMust(es.newTermFilter({ field: this.field, value: term })); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); return true; }; this.removeFilter = function(term) { if (this.parseSelectedValueString) { term = this.parseSelectedValueString(term); } var nq = this.edge.cloneQuery(); nq.removeMust(es.newTermFilter({ field: this.field, value: term })); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.clearFilters = function(params) { var triggerQuery = edges.getParam(params.triggerQuery, true); if (this.filters.length > 0) { var nq = this.edge.cloneQuery(); for (var i = 0; i < this.filters.length; i++) { var filter = this.filters[i]; nq.removeMust(es.newTermFilter({ field: this.field, value: filter.term })); } this.edge.pushQuery(nq); } if (triggerQuery) { this.edge.doQuery(); } }; this.changeSize = function(newSize) { this.size = newSize; var nq = this.edge.cloneQuery(); var agg = nq.getAggregation({ name: this.id }); agg.size = this.size; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.changeSort = function(orderBy, orderDir) { this.orderBy = orderBy; this.orderDir = orderDir; var nq = this.edge.cloneQuery(); var agg = nq.getAggregation({ name: this.id }); agg.setOrdering(this.orderBy, this.orderDir); this.edge.pushQuery(nq); this.edge.doQuery(); }; ////////////////////////////////////////// // "private" functions for internal use this._translate = function(term) { if (this.valueMap) { if (term in this.valueMap) { return this.valueMap[term]; } } else if (this.valueFunction) { return this.valueFunction(term); } return term; }; }, newORTermSelector : function(params) { if (!params) { params = {} } edges.ORTermSelector.prototype = edges.newSelector(params); return new edges.ORTermSelector(params); }, ORTermSelector : function(params) { // whether this component updates itself on every request, or whether it is static // throughout its lifecycle. One of "update" or "static" this.lifecycle = edges.getParam(params.lifecycle, "static"); // if the update type is "update", then how should this component update the facet values // * mergeInitial - always keep the initial list in the original order, and merge the bucket counts onto the correct terms // * fresh - just use the values in the most recent aggregation, ignoring the initial values this.updateType = edges.getParam(params.updateType, "mergeInitial"); // which ordering to use term/count and asc/desc this.orderBy = edges.getParam(params.orderBy, "term"); this.orderDir = edges.getParam(params.orderDir, "asc"); // number of results that we should display - remember that this will only // be used once, so should be large enough to gather all the values that might // be in the index this.size = edges.getParam(params.size, 10); // provide a map of values for terms to displayable terms, or a function // which can be used to translate terms to displyable values this.valueMap = edges.getParam(params.valueMap, false); this.valueFunction = edges.getParam(params.valueFunction, false); // should we try to synchronise the term counts from an equivalent aggregation on the // primary query? You can turn this off if you aren't displaying counts or otherwise // modifying the display based on the counts this.syncCounts = edges.getParam(params.syncCounts, true); // override the parent's defaultRenderer this.defaultRenderer = edges.getParam(params.defaultRenderer, "newORTermSelectorRenderer"); ////////////////////////////////////////// // properties used to store internal state // an explicit list of terms to be displayed. If this is not passed in, then a query // will be issues which will populate this with the values // of the form // [{term: "<value>", display: "<display value>", count: <number of records>}] this.terms = edges.getParam(params.terms, false); // values of terms that have been selected from this.terms // this is just a plain list of the values this.selected = []; // is the object currently updating itself this.updating = false; this.reQueryAfterListAll = false; this.init = function(edge) { // first kick the request up to the superclass edges.newSelector().init.call(this, edge); // now trigger a request for the terms to present, if not explicitly provided if (!this.terms) { if (this.edge.openingQuery || this.edge.urlQuery) { this.reQueryAfterListAll = true; } this.listAll(); } }; this.synchronise = function() { // reset the internal properties this.selected = []; // extract all the filter values that pertain to this selector if (this.edge.currentQuery) { var filters = this.edge.currentQuery.listMust(es.newTermsFilter({field: this.field})); for (var i = 0; i < filters.length; i++) { for (var j = 0; j < filters[i].values.length; j++) { var val = filters[i].values[j]; this.selected.push(val); } } } if (this.syncCounts && this.edge.result && this.terms) { this._synchroniseTerms({result: this.edge.result}); } }; this._synchroniseTermsMergeInitial = function(params) { var result = params.result; // mesh the terms in the aggregation with the terms in the terms list var buckets = result.buckets(this.id); for (var i = 0; i < this.terms.length; i++) { var t = this.terms[i]; var found = false; for (var j = 0; j < buckets.length; j++) { var b = buckets[j]; if (t.term === b.key) { t.count = b.doc_count; found = true; break; } } if (!found) { t.count = 0; } } }; this._synchroniseTerms = function(params) { if (this.updateType === "mergeInitial") { this._synchroniseTermsMergeInitial(params); } else { this._synchroniseTermsFresh(params); } }; this._synchroniseTermsFresh = function(params) { var result = params.result; this.terms = []; var buckets = result.buckets(this.id); for (var i = 0; i < buckets.length; i++) { var bucket = buckets[i]; this.terms.push({term: bucket.key, display: this._translate(bucket.key), count: bucket.doc_count}); } }; ///////////////////////////////////////////////// // query handlers for getting the full list of terms to display this.listAll = function() { // to list all possible terms, build off the base query var bq = this.edge.cloneBaseQuery(); bq.clearAggregations(); bq.size = 0; // now add the aggregation that we want var params = { name: this.id, field: this.field, orderBy: this.orderBy, orderDir: this.orderDir, size: this.size }; bq.addAggregation( es.newTermsAggregation(params) ); // issue the query to elasticsearch this.edge.queryAdapter.doQuery({ edge: this.edge, query: bq, success: edges.objClosure(this, "listAllQuerySuccess", ["result"]), error: edges.objClosure(this, "listAllQueryFail") }); }; this.listAllQuerySuccess = function(params) { var result = params.result; // get the terms out of the aggregation this.terms = []; var buckets = result.buckets(this.id); for (var i = 0; i < buckets.length; i++) { var bucket = buckets[i]; this.terms.push({term: bucket.key, display: this._translate(bucket.key), count: bucket.doc_count}); } // allow the event handler to be set up this.setupEvent(); // in case there's a race between this and another update operation, subsequently synchronise this.synchronise(); if (this.reQueryAfterListAll) { this.doUpdate(); } else { // since this happens asynchronously, we may want to draw this.draw(); } }; this.listAllQueryFail = function() { this.terms = []; }; this.setupEvent = function() { if (this.lifecycle === "update") { this.edge.context.on("edges:pre-query", edges.eventClosure(this, "doUpdate")); // we used to do this, but no need, as when the query cycles, the event handler set above will run it anyway // this.doUpdate(); } }; this.doUpdate = function() { // is an update already happening? if (this.updating) { return } this.udpating = true; // to list all current terms, build off the current query var bq = this.edge.cloneQuery(); // remove any constraint on this field, and clear the aggregations and set size to 0 for performance bq.removeMust(es.newTermsFilter({field: this.field})); bq.clearAggregations(); bq.size = 0; // now add the aggregation that we want var params = { name: this.id, field: this.field, orderBy: this.orderBy, orderDir: this.orderDir, size: this.size }; bq.addAggregation( es.newTermsAggregation(params) ); // issue the query to elasticsearch this.edge.queryAdapter.doQuery({ edge: this.edge, query: bq, success: edges.objClosure(this, "doUpdateQuerySuccess", ["result"]), error: edges.objClosure(this, "doUpdateQueryFail") }); }; this.doUpdateQuerySuccess = function(params) { var result = params.result; this._synchroniseTerms({result: result}); // turn off the update flag this.updating = false; // since this happens asynchronously, we may want to draw this.draw(); }; this.doUpdateQueryFail = function() { // just do nothing, hopefully the next request will be successful this.updating = false; }; /////////////////////////////////////////// // state change functions this.selectTerms = function(params) { var terms = params.terms; var clearOthers = edges.getParam(params.clearOthers, false); var nq = this.edge.cloneQuery(); // first find out if there was a terms filter already in place var filters = nq.listMust(es.newTermsFilter({field: this.field})); // if there is, just add the term to it if (filters.length > 0) { var filter = filters[0]; if (clearOthers) { filter.clear_terms(); } var hadTermAlready = 0; for (var i = 0; i < terms.length; i++) { var term = terms[i]; if (filter.has_term(term)) { hadTermAlready++; } else { filter.add_term(term); } } // if all we did was remove terms that we're then going to re-add, just do nothing if (filter.has_terms() && hadTermAlready === terms.length) { return false; } else if (!filter.has_terms()) { nq.removeMust(es.newTermsFilter({field: this.field})); } } else { // otherwise, set the Terms Filter nq.addMust(es.newTermsFilter({ field: this.field, values: terms })); } // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); return true; }; this.selectTerm = function(term) { return this.selectTerms({terms : [term]}); }; this.removeFilter = function(term) { var nq = this.edge.cloneQuery(); // first find out if there was a terms filter already in place var filters = nq.listMust(es.newTermsFilter({field: this.field})); if (filters.length > 0) { var filter = filters[0]; if (filter.has_term(term)) { filter.remove_term(term); } if (!filter.has_terms()) { nq.removeMust(es.newTermsFilter({field: this.field})); } } // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.clearFilters = function(params) { var triggerQuery = edges.getParam(params.triggerQuery, true); if (this.selected.length > 0) { var nq = this.edge.cloneQuery(); nq.removeMust(es.newTermsFilter({ field: this.field })); this.edge.pushQuery(nq); } if (triggerQuery) { this.edge.doQuery(); } }; ////////////////////////////////////////// // "private" functions for internal use this._translate = function(term) { if (this.valueMap) { if (term in this.valueMap) { return this.valueMap[term]; } } else if (this.valueFunction) { return this.valueFunction(term); } return term; }; }, newBasicRangeSelector : function(params) { if (!params) { params = {} } edges.BasicRangeSelector.prototype = edges.newSelector(params); return new edges.BasicRangeSelector(params); }, BasicRangeSelector : function(params) { ////////////////////////////////////////////// // values that can be passed in // list of ranges (in order) which define the filters // {"from" : <num>, "to" : <num>, "display" : "<display name>"} this.ranges = params.ranges || []; // function to use to format any unknown ranges (there is a sensible default // so you can mostly leave this alone) this.formatUnknown = params.formatUnknown || false; // override the parent's defaultRenderer this.defaultRenderer = params.defaultRenderer || "newBasicRangeSelectorRenderer"; ////////////////////////////////////////////// // values to track internal state // values that the renderer should render // wraps an object (so the list is ordered) which in turn is the // { display: <display>, from: <from>, to: <to>, count: <count> } this.values = []; // a list of already-selected ranges for this field // wraps an object which in turn is // {display: <display>, from: <from>, to: <to> } this.filters = []; this.contrib = function(query) { var ranges = []; for (var i = 0; i < this.ranges.length; i++) { var r = this.ranges[i]; var obj = {}; if (r.from) { obj.from = r.from; } if (r.to) { obj.to = r.to; } ranges.push(obj); } query.addAggregation( es.newRangeAggregation({ name: this.id, field: this.field, ranges: ranges }) ); }; this.synchronise = function() { // reset the state of the internal variables this.values = []; this.filters = []; // first copy over the results from the aggregation buckets if (this.edge.result) { var buckets = this.edge.result.buckets(this.id); for (var i = 0; i < this.ranges.length; i++) { var r = this.ranges[i]; var bucket = this._getRangeBucket(buckets, r.from, r.to); var obj = $.extend(true, {}, r); obj["count"] = bucket.doc_count; this.values.push(obj); } } // now check to see if there are any range filters set on this field if (this.edge.currentQuery) { var filters = this.edge.currentQuery.listMust(es.newRangeFilter({field: this.field})); for (var i = 0; i < filters.length; i++) { var to = filters[i].lt; var from = filters[i].gte; var r = this._getRangeDef(from, to); if (r) { // one of our ranges has been set this.filters.push(r); } else { // this is a previously unknown range definition, so we need to be able to understand it this.filters.push({display: this._formatUnknown(from, to), from: from, to: to}) } } } }; this.selectRange = function(from, to) { var nq = this.edge.cloneQuery(); // just add a new range filter (the query builder will ensure there are no duplicates) var params = {field: this.field}; if (from) { params["gte"] = from; } if (to) { params["lt"] = to; } nq.addMust(es.newRangeFilter(params)); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.removeFilter = function(from, to) { var nq = this.edge.cloneQuery(); // just add a new range filter (the query builder will ensure there are no duplicates) var params = {field: this.field}; if (from) { params["gte"] = from; } if (to) { params["lt"] = to; } nq.removeMust(es.newRangeFilter(params)); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this._getRangeDef = function(from, to) { for (var i = 0; i < this.ranges.length; i++) { var r = this.ranges[i]; var frMatch = true; var toMatch = true; // if one is set and the other not, no match if ((from && !r.from) || (!from && r.from)) { frMatch = false; } if ((to && !r.to) || (!to && r.to)) { toMatch = false; } // if both set, and they don't match, no match if (from && r.from && from !== r.from) { frMatch = false; } if (to && r.to && to !== r.to) { toMatch = false; } // both have to match for a match if (frMatch && toMatch) { return r } } return false; }; this._getRangeBucket = function(buckets, from, to) { for (var i = 0; i < buckets.length; i++) { var r = buckets[i]; var frMatch = true; var toMatch = true; // if one is set and the other not, no match if ((from && !r.from) || (!from && r.from)) { frMatch = false; } if ((to && !r.to) || (!to && r.to)) { toMatch = false; } // if both set, and they don't match, no match if (from && r.from && from !== r.from) { frMatch = false; } if (to && r.to && to !== r.to) { toMatch = false; } if (frMatch && toMatch) { return r } } return false; }; this._formatUnknown = function(from, to) { if (this.formatUnknown) { return this.formatUnknown(from, to) } else { var frag = ""; if (from) { frag += from; } else { frag += "< "; } if (to) { if (from) { frag += " - " + to; } else { frag += to; } } else { if (from) { frag += "+"; } else { frag = "unknown"; } } return frag; } }; }, newBasicGeoDistanceRangeSelector : function(params) { if (!params) { params = {} } edges.BasicGeoDistanceRangeSelector.prototype = edges.newSelector(params); return new edges.BasicGeoDistanceRangeSelector(params); }, BasicGeoDistanceRangeSelector : function(params) { // list of distances (in order) which define the filters // {"from" : <num>, "to" : <num>, "display" : "<display name>"} this.distances = params.distances || []; // if there are no results for a given distance range, should it be hidden this.hideEmptyDistance = params.hideEmptyDistance || true; // unit to measure distances in this.unit = params.unit || "m"; // lat/lon of centre point from which to measure distance this.lat = params.lat || false; this.lon = params.lon || false; ////////////////////////////////////////////// // values to be rendered this.values = []; this.synchronise = function() { // reset the state of the internal variables this.values = []; }; }, newDateHistogramSelector : function(params) { if (!params) { params = {} } edges.DateHistogramSelector.prototype = edges.newSelector(params); return new edges.DateHistogramSelector(params); }, DateHistogramSelector : function(params) { // "year, quarter, month, week, day, hour, minute ,second" // period to use for date histogram this.interval = params.interval || "year"; this.sortFunction = edges.getParam(params.sortFunction, false); this.displayFormatter = edges.getParam(params.displayFormatter, false); this.active = edges.getParam(params.active, true); ////////////////////////////////////////////// // values to be rendered this.values = []; this.filters = []; this.contrib = function(query) { query.addAggregation( es.newDateHistogramAggregation({ name: this.id, field: this.field, interval: this.interval }) ); }; this.synchronise = function() { // reset the state of the internal variables this.values = []; this.filters = []; if (this.edge.result) { var buckets = this.edge.result.buckets(this.id); for (var i = 0; i < buckets.length; i++) { var bucket = buckets[i]; var key = bucket.key; if (this.displayFormatter) { key = this.displayFormatter(key); } var obj = {"display" : key, "gte": bucket.key, "count" : bucket.doc_count}; if (i < buckets.length - 1) { obj["lt"] = buckets[i+1].key; } this.values.push(obj); } } if (this.sortFunction) { this.values = this.sortFunction(this.values); } // now check to see if there are any range filters set on this field // this works in a very specific way: if there is a filter on this field, and it // starts from the date of a filter in the result list, then we make they assumption // that they are a match. This is because a date histogram either has all the results // or only one date bin, if that date range has been selected. And once a range is selected // there will be no "lt" date field to compare the top of the range to. So, this is the best // we can do, and it means that if you have both a date histogram and another range selector // for the same field, they may confuse eachother. if (this.edge.currentQuery) { var filters = this.edge.currentQuery.listMust(es.newRangeFilter({field: this.field})); for (var i = 0; i < filters.length; i++) { var from = filters[i].gte; for (var j = 0; j < this.values.length; j++) { var val = this.values[j]; if (val.gte.toString() === from) { this.filters.push(val); } } } } }; this.selectRange = function(params) { var from = params.gte; var to = params.lt; var nq = this.edge.cloneQuery(); // just add a new range filter (the query builder will ensure there are no duplicates) var params = {field: this.field}; if (from) { params["gte"] = from; } if (to) { params["lt"] = to; } params["format"] = "epoch_millis" // Required for ES7.x date ranges against dateOptionalTime formats nq.addMust(es.newRangeFilter(params)); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.removeFilter = function(params) { var from = params.gte; var to = params.lt; var nq = this.edge.cloneQuery(); // just add a new range filter (the query builder will ensure there are no duplicates) var params = {field: this.field}; if (from) { params["gte"] = from; } if (to) { params["lt"] = to; } nq.removeMust(es.newRangeFilter(params)); // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this.clearFilters = function(params) { var triggerQuery = edges.getParam(params.triggerQuery, true); var nq = this.edge.cloneQuery(); var qargs = {field: this.field}; nq.removeMust(es.newRangeFilter(qargs)); this.edge.pushQuery(nq); if (triggerQuery) { this.edge.doQuery(); } }; }, newAutocompleteTermSelector : function(params) { if (!params) { params = {} } edges.AutocompleteTermSelector.prototype = edges.newComponent(params); return new edges.AutocompleteTermSelector(params); }, AutocompleteTermSelector : function(params) { this.defaultRenderer = params.defaultRenderer || "newAutocompleteTermSelectorRenderer"; }, newNavigationTermList : function(params) { return edges.instantiate(edges.NavigationTermList, params, edges.newComponent); }, NavigationTermList : function(params) { this.urlTemplate = params.urlTemplate; this.placeholder = edges.getParam(params.placeholder, "{term}"); this.sourceResults = edges.getParam(params.sourceResults, false); this.sourceAggregation = params.sourceAggregation; this.terms = []; this.synchronise = function() { this.terms = []; var results = this.edge.result; if (this.sourceResults !== false) { if (!this.edge.secondaryResults.hasOwnProperty(this.sourceResults)) { return; } results = this.edge.secondaryResults[this.sourceResults]; } if (!results) { return; } var agg = results.aggregation(this.sourceAggregation); this.terms = agg.buckets.map(function(x) { return x.key}); }; this.navigate = function(params) { var term = params.term; var url = this.urlTemplate.replace(this.placeholder, term); window.location.href = url; } }, newTreeBrowser : function(params) { return edges.instantiate(edges.TreeBrowser, params, edges.newComponent); }, TreeBrowser : function(params) { this.field = edges.getParam(params.field, false); this.size = edges.getParam(params.size, 10); this.tree = edges.getParam(params.tree, {}); this.nodeMatch = edges.getParam(params.nodeMatch, false); this.filterMatch = edges.getParam(params.filterMatch, false); this.nodeIndex = edges.getParam(params.nodeIndex, false); this.pruneTree = edges.getParam(params.pruneTree, false); this.syncTree = []; this.parentIndex = {}; this.pruned = false; this.nodeCount = 0; this.init = function(edge) { // first kick the request up to the superclass edges.newSelector().init.call(this, edge); // now trigger a request for the terms to present, if not explicitly provided if (this.pruneTree) { this._pruneTree(); } }; this.contrib = function(query) { var params = { name: this.id, field: this.field }; if (this.size) { params["size"] = this.size } query.addAggregation( es.newTermsAggregation(params) ); }; this.synchronise = function() { // synchronise if: // * we are not pruning the tree // * we are pruning the tree, and it has now been pruned this.nodeCount = 0; if (!(!this.pruneTree || (this.pruneTree && this.pruned))) { this.syncTree = []; this.parentIndex = {}; return; } this.syncTree = $.extend(true, [], this.tree); var results = this.edge.result; if (!results) { return; } var selected = []; var filters = this.edge.currentQuery.listMust(es.newTermsFilter({field: this.field})); for (var i = 0; i < filters.length; i++) { var vals = filters[i].values; selected = selected.concat(vals); } var agg = results.aggregation(this.id); var buckets = $.extend(true, [], agg.buckets); var that = this; function recurse(tree, path) { var anySelected = false; var childCount = 0; for (var i = 0; i < tree.length; i++) { var node = tree[i]; that.nodeCount++; that.parentIndex[node.value] = $.extend(true, [], path); var idx = that.nodeMatch(node, buckets); if (idx === -1) { node.count = 0; } else { node.count = buckets[idx].doc_count; } childCount += node.count; if (that.filterMatch(node, selected)) { node.selected = true; anySelected = true; } if (that.nodeIndex) { node.index = that.nodeIndex(node); } else { node.index = node.display; } if (node.children) { path.push(node.value); var childReport = recurse(node.children, path); path.pop(); if (childReport.anySelected) { node.selected = true; anySelected = true; } childCount += childReport.childCount; node.childCount = childReport.childCount; } else { node.childCount = 0; } } return {anySelected: anySelected, childCount: childCount} } var path = []; recurse(this.syncTree, path); }; this.addFilter = function(params) { var value = params.value; var parents = this.parentIndex[value]; var terms = [params.value]; var clearOthers = edges.getParam(params.clearOthers, false); var nq = this.edge.cloneQuery(); // first find out if there was a terms filter already in place var filters = nq.listMust(es.newTermsFilter({field: this.field})); // if there is, just add the term to it (removing and parent terms along the way) if (filters.length > 0) { var filter = filters[0]; var originalValues = $.extend(true, [], filter.values); originalValues.sort(); // if this is an exclusive filter that clears all others, just do that if (clearOthers) { filter.clear_terms(); } // next, if there are any terms left, remove all the parent terms for (var i = 0; i < parents.length; i++) { var parent = parents[i]; if (filter.has_term(parent)) { filter.remove_term(parent); } } // now add all the provided terms var hadTermAlready = 0; for (var i = 0; i < terms.length; i++) { var term = terms[i]; if (filter.has_term(term)) { hadTermAlready++; } else { filter.add_term(term); } } // if, as a result of the all the operations, the values didn't change, then don't search if (originalValues === filter.values.sort()) { return false; } else if (!filter.has_terms()) { nq.removeMust(es.newTermsFilter({field: this.field})); } } else { // otherwise, set the Terms Filter nq.addMust(es.newTermsFilter({ field: this.field, values: terms })); } // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); return true; }; this.removeFilter = function(params) { var term = params.value; var nq = this.edge.cloneQuery(); // first find out if there was a terms filter already in place var filters = nq.listMust(es.newTermsFilter({field: this.field})); if (filters.length > 0) { var filter = filters[0]; if (filter.has_term(term)) { // the filter we are being asked to remove is the actual selected one filter.remove_term(term); } else { // the filter we are being asked to remove may be a parent of the actual selected one // first get all the parent sets of the values that are currently in force var removes = []; for (var i = 0; i < filter.values.length; i++) { var val = filter.values[i]; var parentSet = this.parentIndex[val]; if ($.inArray(term, parentSet) > -1) { removes.push(val); } } for (var i = 0; i < removes.length; i++) { filter.remove_term(removes[i]); } } // look to see if the term has a parent chain var grandparents = this.parentIndex[term]; if (grandparents.length > 0) { // if it does, get a candidate value to add to the filter var immediate = grandparents[grandparents.length - 1]; // we only want to add the candidate value to the filter if it is not a grandparent of any // of the existing filters var other_terms = filter.values; var tripwire = false; for (var i = 0; i < other_terms.length; i++) { var ot = other_terms[i]; var other_parents = this.parentIndex[ot]; if ($.inArray(immediate, other_parents) > -1) { tripwire = true; break; } } if (!tripwire) { filter.add_term(immediate); } } if (!filter.has_terms()) { nq.removeMust(es.newTermsFilter({field: this.field})); } } // reset the search page to the start and then trigger the next query nq.from = 0; this.edge.pushQuery(nq); this.edge.doQuery(); }; this._pruneTree = function() { // to list all possible terms, build off the base query var bq = this.edge.cloneBaseQuery(); bq.clearAggregations(); bq.size = 0; // now add the aggregation that we want var params = { name: this.id, field: this.field }; if (this.size) { params["size"] = this.size } bq.addAggregation( es.newTermsAggregation(params) ); // issue the query to elasticsearch this.edge.queryAdapter.doQuery({ edge: this.edge, query: bq, success: edges.objClosure(this, "_querySuccess", ["result"]), error: edges.objClosure(this, "_queryFail") }); }; this._querySuccess = function(params) { var result = params.result; var agg = result.aggregation(this.id); var buckets = $.extend(true, [], agg.buckets); var that = this; function recurse(tree) { var treeCount = 0; var newTree = []; for (var i = 0; i < tree.length; i++) { var node = $.extend({}, tree[i]); var nodeCount = 0; var idx = that.nodeMatch(node, buckets); if (idx === -1) { nodeCount = 0; } else { nodeCount = buckets[idx].doc_count; } treeCount += nodeCount; if (node.children) { var childUpdate = recurse(node.children); treeCount += childUpdate.treeCount; nodeCount += childUpdate.treeCount; if (childUpdate.newTree.length > 0) { node.children = childUpdate.newTree; } else { delete node.children; } } if (nodeCount > 0) { newTree.push(node); } } return {newTree: newTree, treeCount: treeCount} } var treeUpdate = recurse(this.tree); this.tree = treeUpdate.newTree; this.pruned = true; // in case there's a race between this and another update operation, subsequently synchronise this.synchronise(); // since this happens asynchronously, we may want to draw this.draw(); }; this._queryFail = function() { console.log("pruneTree query failed"); this.tree = []; this.pruned = true; // in case there's a race between this and another update operation, subsequently synchronise this.synchronise(); // since this happens asynchronously, we may want to draw this.draw(); }; } });