diff --git a/src/components/RefiningANDTermSelector.js b/src/components/RefiningANDTermSelector.js index fbeb37a..c87ea63 100644 --- a/src/components/RefiningANDTermSelector.js +++ b/src/components/RefiningANDTermSelector.js @@ -2,350 +2,378 @@ // requires: edges // requires: edges.util -if (!window.hasOwnProperty("edges")) { edges = {}} -if (!edges.hasOwnProperty("components")) { edges.components = {}} +if (!window.hasOwnProperty("edges")) { + edges = {}; +} +if (!edges.hasOwnProperty("components")) { + edges.components = {}; +} edges.components.RefiningANDTermSelector = class extends edges.Component { - constructor(params) { - super(params); + constructor(params) { + super(params); - this.field = edges.util.getParam(params, "field"); + this.field = edges.util.getParam(params, "field"); - // how many terms should the facet limit to - this.size = edges.util.getParam(params, "size", 10); + // how many terms should the facet limit to + this.size = edges.util.getParam(params, "size", 10); - // which ordering to use term/count and asc/desc - this.orderBy = edges.util.getParam(params, "orderBy", "count"); - this.orderDir = edges.util.getParam(params, "orderDir", "desc"); + // which ordering to use term/count and asc/desc + this.orderBy = edges.util.getParam(params, "orderBy", "count"); + this.orderDir = edges.util.getParam(params, "orderDir", "desc"); - // any values that should be removed from the normal ordering and displayed at the end insead - this.pushToBottom = edges.util.getParam(params, "pushToBottom", []); + // any values that should be removed from the normal ordering and displayed at the end insead + this.pushToBottom = edges.util.getParam(params, "pushToBottom", []); - // number of facet terms below which the facet is disabled - this.deactivateThreshold = edges.util.getParam(params, "deactivateThreshold", false); + // number of facet terms below which the facet is disabled + this.deactivateThreshold = edges.util.getParam( + params, + "deactivateThreshold", + false + ); - // should the terms facet ignore empty strings in display - this.ignoreEmptyString = edges.util.getParam(params, "ignoreEmptyString", true); + // should the terms facet ignore empty strings in display + this.ignoreEmptyString = edges.util.getParam( + params, + "ignoreEmptyString", + true + ); - // should filters defined in the baseQuery be excluded from the selector - this.excludePreDefinedFilters = edges.util.getParam(params, "excludePreDefinedFilters", true); + // should filters defined in the baseQuery be excluded from the selector + this.excludePreDefinedFilters = edges.util.getParam( + 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 = edges.util.getParam(params, "valueMap", false); - this.valueFunction = edges.util.getParam(params, "valueFunction", false); + // 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.util.getParam(params, "valueMap", false); + this.valueFunction = edges.util.getParam(params, "valueFunction", 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 = edges.util.getParam(params, "inflation", 100); + // 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 = edges.util.getParam(params, "inflation", 100); - this.active = edges.util.getParam(params, "active", true); + this.active = edges.util.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.util.getParam(params, "lifecycle", "update"); + // whether this component updates itself on every request, or whether it is static + // throughout its lifecycle. One of "update" or "static" + this.lifecycle = edges.util.getParam(params, "lifecycle", "update"); - // for the static lifecycle, should the counts be synchronised with the data - this.syncCounts = edges.util.getParam(params, "syncCounts", true); + // for the static lifecycle, should the counts be synchronised with the data + this.syncCounts = edges.util.getParam(params, "syncCounts", true); - ////////////////////////////////////////// - // properties used to store internal state + ////////////////////////////////////////// + // properties used to store internal state - // filters that have been selected via this component - // {display: , term: } - this.filters = []; + // filters that have been selected via this component + // {display: , term: } + this.filters = []; - // values that the renderer should render - // wraps an object (so the list is ordered) which in turn is the - // { display: , term: , count: } - this.values = false; - } + // values that the renderer should render + // wraps an object (so the list is ordered) which in turn is the + // { display: , term: , count: } + this.values = false; + } - ////////////////////////////////////////// - // overrides on the parent object's standard functions + ////////////////////////////////////////// + // overrides on the parent object's standard functions - init(edge) { - // first kick the request up to the superclass - super.init(edge) + init(edge) { + // first kick the request up to the superclass + super.init(edge); - if (this.lifecycle === "static") { - this.listAll(); - } + if (this.lifecycle === "static") { + this.listAll(); } - - contrib(query) { - let params = { - name: this.id, - field: this.field, - orderBy: this.orderBy, - orderDir: this.orderDir - }; - if (this.size) { - params["size"] = this.size - } - query.addAggregation( - new es.TermsAggregation(params) - ); + } + + contrib(query) { + let params = { + name: this.id, + field: this.field, + orderBy: this.orderBy, + orderDir: this.orderDir, }; - - synchronise() { - // 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}); - } - } else if (this.lifecycle === "static" && this.syncCounts) { - if (this.edge.result) { - this._syncCounts({result: this.edge.result}) - } - } - this.filters = []; - - // extract all the filter values that pertain to this selector - let filters = this.edge.currentQuery.listMust(new es.TermFilter({field: this.field})); - for (let i = 0; i < filters.length; i++) { - let val = filters[i].value; - val = this._translate(val); - this.filters.push({display: val, term: filters[i].value}); - } - }; - - _syncCounts(params) { - let result = params.result; - let buckets = result.buckets(this.id); - - for (let j = 0; j < this.values.length; j++) { - let updated = false; - for (let i = 0; i < buckets.length; i++) { - let bucket = buckets[i]; - if (this.values[j].term === bucket.key) { - this.values[j].count = bucket.doc_count; - updated = true; - } - } - if (!updated) { - this.values[j].count = 0 - } + if (this.size) { + params["size"] = this.size; + } + query.addAggregation(new es.TermsAggregation(params)); + } + + synchronise() { + // 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 }); + } + } else if (this.lifecycle === "static" && this.syncCounts) { + if (this.edge.result) { + this._syncCounts({ result: this.edge.result }); + } + } + this.filters = []; + + // extract all the filter values that pertain to this selector + let filters = this.edge.currentQuery.listMust( + new es.TermFilter({ field: this.field }) + ); + for (let i = 0; i < filters.length; i++) { + let val = filters[i].value; + let translate_val = this._translate(val); + + if (val != translate_val) { + this.filters.push({ display: translate_val, term: val }); + } else { + this.filters.push({ display: val, term: val }); + } + } + } + + _syncCounts(params) { + let result = params.result; + let buckets = result.buckets(this.id); + + for (let j = 0; j < this.values.length; j++) { + let updated = false; + for (let i = 0; i < buckets.length; i++) { + let bucket = buckets[i]; + if (this.values[j].term === bucket.key) { + this.values[j].count = bucket.doc_count; + updated = true; } + } + if (!updated) { + this.values[j].count = 0; + } } + } - _readValues(params) { - let result = params.result; + _readValues(params) { + let result = params.result; - // assign the terms and counts from the aggregation - let buckets = result.buckets(this.id); + // assign the terms and counts from the aggregation + let buckets = result.buckets(this.id); - if (this.deactivateThreshold) { - if (buckets.length < this.deactivateThreshold) { - this.active = false - } else { - this.active = true; - } - } + 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 - let predefined = []; - if (this.excludePreDefinedFilters && this.edge.baseQuery) { - predefined = this.edge.baseQuery.listMust(new es.TermFilter({field: this.field})); - } + // list all of the pre-defined filters for this field from the baseQuery + let predefined = []; + if (this.excludePreDefinedFilters && this.edge.baseQuery) { + predefined = this.edge.baseQuery.listMust( + new es.TermFilter({ field: this.field }) + ); + } - let pushedToBottom = [] - let realCount = 0; - for (let i = 0; i < buckets.length; i++) { - let bucket = buckets[i]; - - // ignore empty strings - if (this.ignoreEmptyString && bucket.key === "") { - continue; - } - - // ignore pre-defined filters - if (this.excludePreDefinedFilters) { - let exclude = false; - for (let j = 0; j < predefined.length; j++) { - let f = predefined[j]; - if (bucket.key === f.value) { - 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 - let key = this._translate(bucket.key); - - // store the original value and the translated value plus the count - let obj = {display: key, term: bucket.key, count: bucket.doc_count}; - - if (this.pushToBottom.includes(bucket.key)) { - pushedToBottom.push(obj) - } else { - this.values.push(obj); - } + let pushedToBottom = []; + let realCount = 0; + for (let i = 0; i < buckets.length; i++) { + let bucket = buckets[i]; + + // ignore empty strings + if (this.ignoreEmptyString && bucket.key === "") { + continue; + } + + // ignore pre-defined filters + if (this.excludePreDefinedFilters) { + let exclude = false; + for (let j = 0; j < predefined.length; j++) { + let f = predefined[j]; + if (bucket.key === f.value) { + exclude = true; + break; + } } - this.values = this.values.concat(pushedToBottom); - }; - - ///////////////////////////////////////////////// - // query handlers for getting the full list of terms to display - - listAll() { - // to list all possible terms, build off the base query - let bq = this.edge.cloneBaseQuery(); - bq.clearAggregations(); - bq.size = 0; - - // now add the aggregation that we want - let params = { - name: this.id, - field: this.field, - orderBy: this.orderBy, - orderDir: this.orderDir, - size: this.size - }; - bq.addAggregation( - new es.TermsAggregation(params) - ); - - // issue the query to elasticsearch - this.edge.queryAdapter.doQuery({ - edge: this.edge, - query: bq, - success: edges.util.objClosure(this, "listAllQuerySuccess", ["result"]), - error: edges.util.objClosure(this, "listAllQueryFail") - }); - }; - - listAllQuerySuccess(params) { - let 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(); - }; - - listAllQueryFail() { - this.values = []; - console.log("RefiningANDTermSelector asynchronous query failed"); - }; - - ////////////////////////////////////////// - // functions that can be called on this component to change its state - - selectTerm(term) { - let nq = this.edge.cloneQuery(); - - // first make sure we're not double-selecting a term - let removeCount = nq.removeMust(new es.TermFilter({ - 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; + if (exclude) { + continue; } - - // 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(new es.TermFilter({ - 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.cycle(); - - return true; + } + + // 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 + let key = this._translate(bucket.key); + + // store the original value and the translated value plus the count + let obj = { display: key, term: bucket.key, count: bucket.doc_count }; + if (this.pushToBottom.includes(bucket.key)) { + pushedToBottom.push(obj); + } else { + this.values.push(obj); + } + } + this.values = this.values.concat(pushedToBottom); + } + + ///////////////////////////////////////////////// + // query handlers for getting the full list of terms to display + + listAll() { + // to list all possible terms, build off the base query + let bq = this.edge.cloneBaseQuery(); + bq.clearAggregations(); + bq.size = 0; + + // now add the aggregation that we want + let params = { + name: this.id, + field: this.field, + orderBy: this.orderBy, + orderDir: this.orderDir, + size: this.size, }; + bq.addAggregation(new es.TermsAggregation(params)); + + // issue the query to elasticsearch + this.edge.queryAdapter.doQuery({ + edge: this.edge, + query: bq, + success: edges.util.objClosure(this, "listAllQuerySuccess", ["result"]), + error: edges.util.objClosure(this, "listAllQueryFail"), + }); + } + + listAllQuerySuccess(params) { + let 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(); + } + + listAllQueryFail() { + this.values = []; + console.log("RefiningANDTermSelector asynchronous query failed"); + } + + ////////////////////////////////////////// + // functions that can be called on this component to change its state + + selectTerm(term) { + let nq = this.edge.cloneQuery(); + + // first make sure we're not double-selecting a term + let removeCount = nq.removeMust( + new es.TermFilter({ + 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; + } - removeFilter(term) { - let nq = this.edge.cloneQuery(); - - nq.removeMust(new es.TermFilter({ + // 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( + new es.TermFilter({ + 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.cycle(); + + return true; + } + + removeFilter(term) { + let nq = this.edge.cloneQuery(); + + nq.removeMust( + new es.TermFilter({ + 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.cycle(); + } + + clearFilters(params) { + let triggerQuery = edges.util.getParam(params, "triggerQuery", true); + + if (this.filters.length > 0) { + let nq = this.edge.cloneQuery(); + for (let i = 0; i < this.filters.length; i++) { + let filter = this.filters[i]; + nq.removeMust( + new es.TermFilter({ 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.cycle(); - }; - - clearFilters(params) { - let triggerQuery = edges.util.getParam(params, "triggerQuery", true); - - if (this.filters.length > 0) { - let nq = this.edge.cloneQuery(); - for (let i = 0; i < this.filters.length; i++) { - let filter = this.filters[i]; - nq.removeMust(new es.TermFilter({ - field: this.field, - value: filter.term - })); - } - this.edge.pushQuery(nq); - } - if (triggerQuery) { - this.edge.cycle(); - } - }; - - changeSize(newSize) { - this.size = newSize; - - let nq = this.edge.cloneQuery(); - let agg = nq.getAggregation({ - name: this.id - }); - agg.size = this.size; - this.edge.pushQuery(nq); - this.edge.cycle(); - }; - - changeSort(orderBy, orderDir) { - this.orderBy = orderBy; - this.orderDir = orderDir; - - let nq = this.edge.cloneQuery(); - let agg = nq.getAggregation({ - name: this.id - }); - agg.setOrdering(this.orderBy, this.orderDir); - this.edge.pushQuery(nq); - this.edge.cycle(); - }; - - _translate(term) { - if (this.valueMap) { - if (term in this.valueMap) { - return this.valueMap[term]; - } - } else if (this.valueFunction) { - return this.valueFunction(term); - } - return term; - }; -} \ No newline at end of file + value: filter.term, + }) + ); + } + this.edge.pushQuery(nq); + } + if (triggerQuery) { + this.edge.cycle(); + } + } + + changeSize(newSize) { + this.size = newSize; + + let nq = this.edge.cloneQuery(); + let agg = nq.getAggregation({ + name: this.id, + }); + agg.size = this.size; + this.edge.pushQuery(nq); + this.edge.cycle(); + } + + changeSort(orderBy, orderDir) { + this.orderBy = orderBy; + this.orderDir = orderDir; + + let nq = this.edge.cloneQuery(); + let agg = nq.getAggregation({ + name: this.id, + }); + agg.setOrdering(this.orderBy, this.orderDir); + this.edge.pushQuery(nq); + this.edge.cycle(); + } + + _translate(term) { + if (this.valueMap) { + if (term in this.valueMap) { + return this.valueMap[term]; + } + } else if (this.valueFunction) { + return this.valueFunction(term); + } + return term; + } +}; diff --git a/src/datasources/solr9x.js b/src/datasources/solr9x.js new file mode 100644 index 0000000..0441b81 --- /dev/null +++ b/src/datasources/solr9x.js @@ -0,0 +1,1286 @@ +if (!window.hasOwnProperty("es")) { + es = {}; +} + +// request method to be used throughout. Set this before using the module if you want it different +es.requestMethod = "get"; + +// add request headers (such as Auth) if you need to +es.requestHeaders = false; + +// Base classes +es.Aggregation = class { + static type = "aggregation"; + + constructor(params) { + this.name = params.name; + this.aggs = params.aggs || []; + } + + addAggregation(agg) { + for (var i = 0; i < this.aggs.length; i++) { + if (this.aggs[i].name === agg.name) { + return; + } + } + this.aggs.push(agg); + } + + removeAggregation() {} + clearAggregations() {} + + // for use by sub-classes, for their convenience in rendering + // the overall structure of the aggregation to an object + _make_aggregation(type, body) { + var obj = {}; + obj[this.name] = {}; + obj[this.name][type] = body; + + if (this.aggs.length > 0) { + obj[this.name]["aggs"] = {}; + for (var i = 0; i < this.aggs.length; i++) { + $.extend(obj[this.name]["aggs"], this.aggs[i].objectify()); + } + } + + return obj; + } + + _parse_wrapper(obj, type) { + this.name = Object.keys(obj)[0]; + var body = obj[this.name][type]; + + var aggs = obj[this.name].aggs + ? obj[this.name].aggs + : obj[this.name].aggregations; + if (aggs) { + var anames = Object.keys(aggs); + for (var i = 0; i < anames.length; i++) { + var name = anames[i]; + var agg = aggs[anames[i]]; + var subtype = Object.keys(agg)[0]; + var raw = {}; + raw[name] = agg; + var oa = es.aggregationFactory(subtype, { raw: raw }); + if (oa) { + this.addAggregation(oa); + } + } + } + + return body; + } +}; + +es.Filter = class { + static type = "filter"; + + constructor(params) { + this.field = params.field; + } + + matches(other) { + return this._baseMatch(other); + } + + _baseMatch(other) { + // type must match + if (other.type !== this.type) { + return false; + } + // field (if set) must match + if (other.field && other.field !== this.field) { + return false; + } + // otherwise this matches + return true; + } + + objectify() {} + parse() {} +}; + +es.Sort = class { + constructor(params) { + this.field = params.field || "score"; + this.order = params.order || "desc"; // Solr uses 'score' instead of '_score' for default relevance sorting + + if (params.raw) { + this.parse(params.raw); + } + } + + objectify() { + return `${this.field} ${this.order}`; // Solr uses 'field order' format as a string + } + + parse(str) { + const parts = str.split(" "); + this.field = parts[0]; + this.order = parts[1] || "desc"; // Default to 'desc' if no order is provided + } +}; + +// Define the Query class +es.Query = class { + constructor(params) { + if (!params) { + params = {}; + } + + // Properties initialization + this.filtered = false; // no longer present in ES 5.x+ + this.trackTotalHits = true; // FIXME: hard code this for now + + // Initialize with default values or from params + this.size = es.getParam(params.size, false); + this.from = es.getParam(params.from, false); + this.fields = es.getParam(params.fields, []); + this.aggs = es.getParam(params.aggs, []); + this.must = es.getParam(params.must, []); + this.mustNot = es.getParam(params.mustNot, []); + this.should = es.getParam(params.should, []); + this.minimumShouldMatch = es.getParam(params.minimumShouldMatch, false); + this.query = es.getParam(params.query, {}); + this.queryStrings = es.getParam(params.queryStrings, []); + + // Defaults from properties set through their setters + this.queryString = false; + this.sort = []; + + // Properties awaiting implementation + this.source = es.getParam(params.source, false); + this.partialFields = es.getParam(params.partialFields, false); // using partialFields instead of partialField + this.scriptFields = es.getParam(params.scriptFields, false); + + // For older ES versions, may not be implemented + this.facets = es.getParam(params.facets, []); + + // Final part of construction - set dynamic properties via their setters + if (params.queryString) { + this.setQueryString(params.queryString); + } + + if (params.sort) { + this.setSortBy(params.sort); + } + + // Parse raw query if provided + if (params.raw) { + this.parse(params.raw); + } + } + + // Getters and Setters + getSize() { + return this.size !== undefined && this.size !== false ? this.size : 10; + } + + getFrom() { + return this.from || 0; + } + + addField(field) { + if (!this.fields.includes(field)) { + this.fields.push(field); + } + } + + setQueryString(params) { + let qs = params; + if (!(params instanceof es.QueryString)) { + if ($.isPlainObject(params)) { + qs = new es.QueryString(params); + } else { + qs = new es.QueryString({ queryString: params }); + } + } + this.queryString = qs; + } + + getQueryString() { + return this.queryString; + } + + removeQueryString() { + this.queryString = false; + } + + setSortBy(params) { + this.sort = []; + let sorts = Array.isArray(params) ? params : [params]; + sorts.forEach((sort) => this.addSortBy(sort)); + } + + addSortBy(params) { + let sort = params instanceof es.Sort ? params : new es.Sort(params); + if (!this.sort.some((existingSort) => existingSort.field === sort.field)) { + this.sort.push(sort); + } + } + + prependSortBy(params) { + let sort = params instanceof es.Sort ? params : new es.Sort(params); + this.removeSortBy(sort); + this.sort.unshift(sort); + } + + removeSortBy(params) { + let sort = params instanceof es.Sort ? params : new es.Sort(params); + this.sort = this.sort.filter( + (existingSort) => existingSort.field !== sort.field + ); + } + + getSortBy() { + return this.sort; + } + + setSourceFilters(params) { + this.source = this.source || { include: [], exclude: [] }; + if (params.include) { + this.source.include = params.include; + } + if (params.exclude) { + this.source.exclude = params.exclude; + } + } + + addSourceFilters(params) { + this.source = this.source || { include: [], exclude: [] }; + if (params.include) { + this.source.include.push(...params.include); + } + if (params.exclude) { + this.source.exclude.push(...params.exclude); + } + } + + getSourceIncludes() { + return this.source && this.source.include ? this.source.include : []; + } + + getSourceExcludes() { + return this.source && this.source.exclude ? this.source.exclude : []; + } + + // Aggregation Methods + getAggregation(params) { + return this.aggs.find((agg) => agg.name === params.name); + } + + addAggregation(agg, overwrite) { + if (overwrite) { + this.removeAggregation(agg.name); + } + this.aggs.push(agg); + } + + removeAggregation(name) { + this.aggs = this.aggs.filter((agg) => agg.name !== name); + } + + clearAggregations() { + this.aggs = []; + } + + listAggregations() { + return this.aggs; + } + + // Filter Methods + addMust(filter) { + if ( + !this.listMust().some((existingFilter) => { + return Object.keys(filter).every( + (key) => existingFilter[key] === filter[key] + ); + }) + ) { + this.must.push(filter); + } + } + + listMust() { + return this.must; + } + + removeMust(template) { + let removedCount = 0; + this.must = this.must.filter((filter) => { + // Check if filter values match the template values + const matches = Object.keys(template).every( + (key) => filter[key] === template[key] + ); + if (matches) { + removedCount++; + } + return !matches; + }); + return removedCount; + } + + // TODO: this is a patch code update it when proper fix is added + removeQueryStrings(template) { + let removedCount = 0; + + this.queryStrings = this.queryStrings.filter((queryItem) => { + // Check if the queryString matches the template value + const matches = queryItem.queryString === template.value; + + if (matches) { + removedCount++; + } + + return !matches; + }); + + return removedCount; + } + + clearMust() { + this.must = []; + } + + addMustNot(filter) { + if ( + !this.listMustNot().some((existingFilter) => { + return Object.keys(filter).every( + (key) => existingFilter[key] === filter[key] + ); + }) + ) { + this.mustNot.push(filter); + } + } + + listMustNot() { + return this.mustNot; + } + + removeMustNot(template) { + let removedCount = 0; + this.mustNot = this.mustNot.filter((filter) => { + const matches = Object.keys(template).every( + (key) => filter[key] === template[key] + ); + if (matches) { + removedCount++; + } + return !matches; + }); + return removedCount; + } + + clearMustNot() { + this.mustNot = []; + } + + addShould(filter) { + if ( + !this.listShould().some((existingFilter) => { + return Object.keys(filter).every( + (key) => existingFilter[key] === filter[key] + ); + }) + ) { + this.should.push(filter); + } + } + + listShould() { + return this.should; + } + + removeShould(template) { + let removedCount = 0; + this.should = this.should.filter((filter) => { + const matches = Object.keys(template).every( + (key) => filter[key] === template[key] + ); + if (matches) { + removedCount++; + } + return !matches; + }); + return removedCount; + } + + clearShould() { + this.should = []; + } + + // Interrogative Methods + hasFilters() { + return ( + this.must.length > 0 || this.should.length > 0 || this.mustNot.length > 0 + ); + } + + listFilters(params) { + const { boolType, template } = params; + const matchesTemplate = (filter) => { + return Object.keys(template).every( + (key) => filter[key] === template[key] + ); + }; + + switch (boolType) { + case "must": + return this.listMust().filter(matchesTemplate); + case "should": + return this.listShould().filter(matchesTemplate); + case "must_not": + return this.listMustNot().filter(matchesTemplate); + default: + return []; + } + } + + // Parsing and Serialization + merge(source) { + this.filtered = source.filtered; + if (source.size) { + this.size = source.size; + } + if (source.from) { + this.from = source.from; + } + if (source.fields && source.fields.length > 0) { + source.fields.forEach((field) => this.addField(field)); + } + source.aggs.forEach((agg) => this.addAggregation(agg, true)); + source.must.forEach((filter) => this.addMust(filter)); + source.mustNot.forEach((filter) => this.addMustNot(filter)); + source.should.forEach((filter) => this.addShould(filter)); + if (source.minimumShouldMatch !== false) { + this.minimumShouldMatch = source.minimumShouldMatch; + } + if (source.getQueryString()) { + this.setQueryString(source.getQueryString()); + } + if (source.sort && source.sort.length > 0) { + source.sort.reverse().forEach((sort) => this.prependSortBy(sort)); + } + if (source.source) { + this.addSourceFilters({ + include: source.getSourceIncludes(), + exclude: source.getSourceExcludes(), + }); + } + } + + objectify(params) { + params = params || {}; + const { + include_query_string = true, + include_filters = true, + include_paging = true, + include_sort = true, + include_fields = true, + include_aggregations = true, + include_source_filters = true, + } = params; + + console.log(`${include_query_string} ${include_filters}`); + + const query_part = {}; + const bool = {}; + + if (this.queryString && include_query_string) { + Object.assign(query_part, this.queryString.objectify()); + } + + if (include_filters) { + if (this.must.length > 0) { + bool.must = this.must.map((filter) => filter.objectify()); + } + if (this.mustNot.length > 0) { + bool.must_not = this.mustNot.map((filter) => filter.objectify()); + } + if (this.should.length > 0) { + bool.should = this.should.map((filter) => filter.objectify()); + } + if (this.minimumShouldMatch !== false) { + bool.minimum_should_match = this.minimumShouldMatch; + } + } + + if ( + Object.keys(query_part).length === 0 && + Object.keys(bool).length === 0 + ) { + query_part.match_all = {}; + } else if ( + Object.keys(query_part).length === 0 && + Object.keys(bool).length > 0 + ) { + query_part.bool = bool; + } + + const obj = { + query: query_part, + }; + + if (include_paging) { + obj.from = this.getFrom(); + obj.size = this.getSize(); + } + + if (include_sort && this.sort.length > 0) { + obj.sort = this.sort.map((sort) => sort.objectify()); + } + + if (include_fields && this.fields.length > 0) { + obj.fields = this.fields.slice(); // Shallow copy of fields array + } + + if (include_aggregations && this.aggs.length > 0) { + obj.aggs = this.aggs.map((agg) => agg.objectify()); + } + + if (include_source_filters && this.source) { + obj._source = {}; + if (this.source.include && this.source.include.length > 0) { + obj._source.includes = this.source.include.slice(); // Shallow copy of include array + } + if (this.source.exclude && this.source.exclude.length > 0) { + obj._source.excludes = this.source.exclude.slice(); // Shallow copy of exclude array + } + } + console.log("Returning obj", obj); + return obj; + } + + clone() { + const cloneParams = { + size: this.size, + from: this.from, + fields: [...this.fields], // Shallow copy of fields array + aggs: this.aggs.map((agg) => ({ ...agg })), // Shallow copy of aggs array + must: this.must.map((filter) => ({ ...filter })), // Shallow copy of must array + mustNot: this.mustNot.map((filter) => ({ ...filter })), // Shallow copy of mustNot array + should: this.should.map((filter) => ({ ...filter })), // Shallow copy of should array + minimumShouldMatch: this.minimumShouldMatch, + queryString: this.queryString ? { ...this.queryString } : null, // Shallow copy of queryString if present + queryStrings: this.queryStrings + ? this.queryStrings.map((qs) => ({ ...qs })) // Shallow copy of queryStrings array if present + : false, // Default to false if queryStrings is not present + sort: this.sort.map((sort) => ({ ...sort })), // Shallow copy of sort array + source: this.source + ? { + include: [...this.source.include], // Shallow copy of include array + exclude: [...this.source.exclude], // Shallow copy of exclude array + } + : null, + query: this.query ? { ...this.query } : null, + partialFields: this.partialFields, + scriptFields: this.scriptFields, + // Add any other properties that need to be cloned + }; + + return new es.Query(cloneParams); + } +}; + +es.QueryString = class { + constructor(params) { + this.queryString = params.queryString || false; + this.defaultField = params.defaultField || false; + this.defaultOperator = params.defaultOperator || "OR"; + + this.fuzzify = params.fuzzify || false; // * or ~ + this.escapeSet = params.escapeSet || es.specialCharsSubSet; + this.pairs = params.pairs || es.characterPairs; + this.unEscapeSet = params.unEscapeSet || es.specialChars; + + if (params.raw) { + this.parse(params.raw); + } + } + + objectify() { + const qs = this._escape(this._fuzzify(this.queryString)); + const obj = { q: qs }; + if (this.defaultOperator) { + obj["q.op"] = this.defaultOperator; + } + if (this.defaultField) { + obj["df"] = this.defaultField; + } + return obj; + } + + clone() { + return new es.QueryString({ + queryString: this.queryString, + defaultField: this.defaultField, + defaultOperator: this.defaultOperator, + fuzzify: this.fuzzify, + escapeSet: this.escapeSet.slice(), // Shallow copy of escapeSet array + pairs: this.pairs.slice(), // Shallow copy of pairs array + unEscapeSet: this.unEscapeSet.slice(), // Shallow copy of unEscapeSet array + }); + } + + parse(obj) { + if (obj.q) { + this.queryString = this._unescape(obj.q); + } + if (obj["q.op"]) { + this.defaultOperator = obj["q.op"]; + } + if (obj.df) { + this.defaultField = obj.df; + } + } + + _fuzzify(str) { + if (!this.fuzzify || !(this.fuzzify === "*" || this.fuzzify === "~")) { + return str; + } + + if (!(str.includes("*") || str.includes("~") || str.includes(":"))) { + return str; + } + + let pq = ""; + const optparts = str.split(" "); + for (let i = 0; i < optparts.length; i++) { + let oip = optparts[i]; + if (oip.length > 0) { + oip += this.fuzzify; + if (this.fuzzify === "*") { + oip = "*" + oip; + } + pq += oip + " "; + } + } + return pq.trim(); + } + + _escapeRegExp(string) { + return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + } + + _replaceAll(string, find, replace) { + return string.replace(new RegExp(this._escapeRegExp(find), "g"), replace); + } + + _unReplaceAll(string, find) { + return string.replace( + new RegExp("\\\\" + this._escapeRegExp(find), "g"), + find + ); + } + + _paired(string, pair) { + const matches = + string.match(new RegExp(this._escapeRegExp(pair), "g")) || []; + return matches.length % 2 === 0; + } + + _escape(str) { + let scs = this.escapeSet.slice(); // Make a copy of escapeSet + for (let i = 0; i < this.pairs.length; i++) { + const char = this.pairs[i]; + if (!this._paired(str, char)) { + scs.push(char); + } + } + for (let i = 0; i < scs.length; i++) { + const char = scs[i]; + str = this._replaceAll(str, char, "\\" + char); + } + return str; + } + + _unescape(str) { + for (let i = 0; i < this.unEscapeSet.length; i++) { + const char = this.unEscapeSet[i]; + str = this._unReplaceAll(str, char); + } + return str; + } +}; + +// Factories +es.aggregationFactory = function (type, params) { + for (const [key, value] of Object.entries(es)) { + if (es._classExtends(es[key], es.Aggregation)) { + if (es[key].type === type) { + // Convert Elasticsearch specific parameters to es if needed + if (type === "terms") { + params.field = params.field || false; + params.size = params.size || 10; // Use 'rows' for Solr, mapped from 'size' + params.orderBy = params.orderBy || "_count"; + if (params.orderBy[0] !== "_") { + params.orderBy = "_" + params.orderBy; + } + params.orderDir = params.orderDir || "desc"; + } + return new es[key](params); + } + } + } + throw new Error(`Unknown aggregation type: ${type}`); +}; + +es.filterFactory = function (type, params) { + // query string is a special case + if (type === "query_string") { + return new es.QueryString(params); + } + + // otherwise auto-detect + for (const [key, value] of Object.entries(es)) { + if (es._classExtends(es[key], es.Filter)) { + if (es[key].type === type) { + // Convert Elasticsearch specific parameters to Solr if needed + if (type === "terms") { + params.field = params.field || false; + params.values = params.values || []; + params.execution = params.execution || false; + } + return new es[key](params); + } + } + } + throw new Error(`Unknown filter type: ${type}`); +}; + +// Filter extended classes starts here +es.TermFilter = class extends es.Filter { + static type = "term"; + + constructor(params) { + super(params); + // this.filter handled by superclass + this.value = params.value || false; + + if (params.raw) { + this.parse(params.raw); + } + } + + matches(other) { + // ask the parent object first + let pm = this._baseMatch(other); + if (!pm) { + return false; + } + // value (if set) must match + if (other.value && other.value !== this.value) { + return false; + } + + return true; + } + + objectify() { + // Solr-specific object structure + var obj = {}; + obj[this.field] = this.value; + return obj; + } + + parse(obj) { + // Solr-specific parsing + this.field = Object.keys(obj)[0]; + this.value = obj[this.field]; + } +}; + +es.TermsFilter = class extends es.Filter { + static type = "terms"; + + constructor(params) { + super(params); + // this.field handled by superclass + this.values = params.values || false; + this.execution = params.execution || false; + + if (params.raw) { + this.parse(params.raw); + } + } + + matches(other) { + // ask the parent object first + let pm = this._baseMatch(other); + if (!pm) { + return false; + } + + // values (if set) must be the same list + if (other.values) { + if (other.values.length !== this.values.length) { + return false; + } + for (var i = 0; i < other.values.length; i++) { + if ($.inArray(other.values[i], this.values) === -1) { + return false; + } + } + } + + return true; + } + + objectify() { + var val = this.values || []; + var filterQuery = val.map((value) => `${this.field}:${value}`).join(" OR "); + return { fq: filterQuery }; + } + + parse(obj) { + if (obj.fq) { + let terms = obj.fq.split(" OR "); + let field = terms[0].split(":")[0]; + let values = terms.map((term) => term.split(":")[1]); + + this.field = field; + this.values = values; + } + } + + add_term(term) { + if (!this.values) { + this.values = []; + } + if ($.inArray(term, this.values) === -1) { + this.values.push(term); + } + } + + has_term(term) { + if (!this.values) { + return false; + } + return $.inArray(term, this.values) >= 0; + } + + remove_term(term) { + if (!this.values) { + return; + } + var idx = $.inArray(term, this.values); + if (idx >= 0) { + this.values.splice(idx, 1); + } + } + + has_terms() { + return this.values !== false && this.values.length > 0; + } + + term_count() { + return this.values === false ? 0 : this.values.length; + } + + clear_terms() { + this.values = false; + } +}; + +// Aggregation extended classes starts here +es.TermsAggregation = class extends es.Aggregation { + static type = "terms"; + + constructor(params) { + super(params); + this.field = params.field || false; + this.size = params.size || 10; // 'size' in Elasticsearch, will convert to 'rows' for Solr + + // set the ordering for the first time + this.orderBy = "_count"; + if (params.orderBy) { + this.orderBy = params.orderBy; + if (this.orderBy[0] !== "_") { + this.orderBy = "_" + this.orderBy; + } + } + this.orderDir = params.orderDir || "desc"; + + if (params.raw) { + this.parse(params.raw); + } + } + + // provide a method to set and normalize the ordering in future + setOrdering(orderBy, orderDir) { + this.orderBy = orderBy; + if (this.orderBy[0] !== "_") { + this.orderBy = "_" + this.orderBy; + } + this.orderDir = orderDir; + } + + objectify() { + // Solr facets configuration + const body = { + field: this.field, + rows: this.size, // Convert 'size' to 'rows' for Solr + order: {}, + }; + + // Translate Elasticsearch orderBy to Solr sort + let solrSort = ""; + if (this.orderBy === "_count") { + solrSort = "count"; + } else if (this.orderBy === "_term") { + solrSort = "index"; + } + body.order[solrSort] = this.orderDir; + + return this._make_aggregation(es.TermsAggregation.type, body); + } + + parse(obj) { + const body = this._parse_wrapper(obj, es.TermsAggregation.type); + this.field = body.field; + if (body.rows) { + this.size = body.rows; // Convert 'rows' to 'size' + } + if (body.order) { + const solrSort = Object.keys(body.order)[0]; + this.orderDir = body.order[solrSort]; + + // Translate Solr sort back to Elasticsearch orderBy + if (solrSort === "count") { + this.orderBy = "_count"; + } else if (solrSort === "index") { + this.orderBy = "_term"; + } + } + } +}; + +es.doQuery = (params) => { + const { success, error, complete, search_url, query, datatype } = params; + + const solrArgs = this._es2solr({ query: query }); + const searchUrl = search_url; + // Generate the Solr query URL + const fullUrl = this._args2URL({ baseUrl: searchUrl, args: solrArgs }); + var error_callback = es.queryError(error); + var success_callback = es.querySuccess(success, error_callback); + + // Execution of solr query + $.get({ + url: fullUrl, + datatype: datatype ? datatype : "jsonp", + success: success_callback, + error: error_callback, + jsonp: "json.wrf", + }); +}; + +es.querySuccess = function (callback, error_callback) { + return function (data) { + if (data.hasOwnProperty("error")) { + error_callback(data); + return; + } + + var result = new es.Result({ raw: data }); + callback(result); + }; +}; + +es.queryError = function (callback) { + return function (data) { + if (callback) { + callback(data); + } else { + throw new Error(data); + } + }; +}; + +es.Result = class { + constructor(params) { + if (typeof params.raw == "string") { + this.data = JSON.parse(params.raw); + } else { + this.data = params.raw; + } + } + + buckets(facet_name) { + if (this.data.facet_counts) { + if ( + this.data.facet_counts.facet_fields && + this.data.facet_counts.facet_fields[facet_name] + ) { + return this._convertFacetToBuckets( + this.data.facet_counts.facet_fields[facet_name] + ); + } else if ( + this.data.facet_counts.facet_queries && + this.data.facet_counts.facet_queries[facet_name] + ) { + return this._convertFacetToBuckets( + this.data.facet_counts.facet_queries[facet_name] + ); + } + } + return []; + } + + _convertFacetToBuckets(facet) { + let buckets = []; + for (let i = 0; i < facet.length; i += 2) { + buckets.push({ + key: facet[i], + doc_count: facet[i + 1], + }); + } + return buckets; + } + + aggregation(facet_name) { + return { + buckets: this.buckets(facet_name), + }; + } + + results() { + var res = []; + if (this.data.response && this.data.response.docs) { + for (var i = 0; i < this.data.response.docs.length; i++) { + res.push(this.data.response.docs[i]); + } + } + return res; + } + + total() { + if (this.data.response && this.data.response.numFound !== undefined) { + return parseInt(this.data.response.numFound); + } + return false; + } +}; + +// Helper functions +// Method to convert es query to Solr query +function _es2solr({ query }) { + const solrQuery = {}; + let solrFacets = []; + + // Handle the query part + if (Object.entries(query.query).length > 0) { + const queryPart = query.query; + if (queryPart.match) { + const field = Object.keys(queryPart.match)[0]; + const value = queryPart.match[field]; + solrQuery.q = `${field}:${value}`; + } else if (queryPart.range) { + const fields = Object.keys(queryPart.range); + const rangeQueries = fields.map((field) => { + const range = queryPart.range[field]; + return `${field}:[${range.gte || "*"} TO ${range.lte || "*"}]`; + }); + + // Join the range queries with OR if there are multiple + solrQuery.q = + rangeQueries.length > 1 + ? `(${rangeQueries.join(" OR ")})` + : rangeQueries[0]; + } else if (queryPart.match_all) { + solrQuery.q = `*:*`; + } + } else { + solrQuery.q = `*:*`; + } + + // Handle pagination + if (query.from !== undefined) { + if (typeof query.from == "boolean" && !query.from) { + solrQuery.start = 0; + } else { + solrQuery.start = query.from; + } + } + if (query.size !== undefined) { + if (typeof query.size == "boolean" && !query.size) { + solrQuery.rows = 10; + } else { + solrQuery.rows = query.size; + } + } + + // Handle sorting + if (query && query.sort && query.sort.length > 0) { + solrQuery.sort = query.sort + .map((sortOption) => { + const sortField = sortOption.field; + const sortOrder = sortOption.order === "desc" ? "desc" : "asc"; + return `${sortField} ${sortOrder}`; + }) + .join(", "); + } + + if (query.queryStrings && query.queryStrings.length > 0) { + query.queryStrings.forEach((query, queryIndex) => { + const esQueryString = query.queryString; + const fields = query.fields; + + if (typeof esQueryString == "boolean") { + throw new Error("Search string needs to be a string, got boolean"); + } + + if (esQueryString !== "" && Array.isArray(fields) && fields.length > 0) { + let queryPart = ""; // To hold the query part for this set of fields + + fields.forEach((fieldConfig, index) => { + const { field, operator = "OR" } = fieldConfig; + + // Cleaning solr query only in case of *:* + if (solrQuery.q == "*:*") { + solrQuery.q = ""; + } + + if (queryPart) { + queryPart += ` ${operator} ${field}:"${esQueryString}"`; + } else { + queryPart = `${field}:"${esQueryString}"`; + } + }); + + // If there are multiple fields, wrap the query in parentheses + if (fields.length > 1) { + queryPart = `(${queryPart})`; + } + + // Append to the existing solrQuery.q + if (solrQuery.q) { + solrQuery.q += ` AND ${queryPart}`; + } else { + solrQuery.q = queryPart; + } + } + }); + } + + if (query.queryString && query.queryString.queryString) { + const esQueryString = query.queryString.queryString; + const searchField = query.queryString.defaultField; + let operator = query.queryString.defaultOperator; + + if (typeof esQueryString == "boolean") { + throw new Error("Search string needs to be string got boolean"); + } + + if (operator == "") { + operator = "OR"; + } + + if (esQueryString != "") { + if (typeof searchField == "boolean") { + solrQuery.q = `${solrQuery.q} ${operator} ${esQueryString}`; + } else { + solrQuery.q = `${solrQuery.q} ${operator} ${searchField}:${esQueryString}`; + } + } + } + + if (query && query.aggs && query.aggs.length > 0) { + solrQuery.facets = query.aggs + .filter((agg) => { + // Log the field value + return typeof agg.field === "string" && agg.field; // Filter condition + }) + .map((agg) => agg.field); + + solrQuery.facet = true; + } + + if (query && query.must && query.must.length > 0) { + query.must.forEach((mustQuery) => { + let term, + field, + value = ""; + if (mustQuery.term) { + term = mustQuery.term; + field = Object.keys(term)[0]; + value = term[field]; + } else { + field = mustQuery.field; + value = mustQuery.value; + } + + if (solrQuery.q == "*:*") { + solrQuery.q = ""; + } + + if (solrQuery.q) { + solrQuery.q += ` AND ${field}:${value}`; + } else { + solrQuery.q = `${field}:${value}`; + } + }); + } + + if (query && query.mustNot && query.mustNot.length > 0) { + query.mustNot.forEach((mustNotq) => { + const term = mustQuery.term; + const field = Object.keys(term)[0]; + const value = term[field]; + + solrQuery.q = `-${field}:${value}`; + }); + } + + if (query && query.should && query.should.length > 0) { + query.should.forEach((shouldQ, index) => { + const term = mustQuery.term; + const field = Object.keys(term)[0]; + const value = term[field]; + + solrQuery.q = `${field}:${value}^1.0`; + }); + } + + solrQuery.wt = "json"; + + return solrQuery; +} + +function _args2URL({ baseUrl, args }) { + const qParts = Object.keys(args).flatMap((k) => { + const v = args[k]; + if (Array.isArray(v)) { + if (k === "facets") { + const result = v + .map((item) => `facet.field=${encodeURIComponent(item)}`) + .join("&"); + + return result; + } else { + return v.map( + (item) => `${encodeURIComponent(k)}=${encodeURIComponent(item)}` + ); + } + } + return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`; + }); + + const qs = qParts.join("&"); + return `${baseUrl}?${qs}`; +} + +function _convertAffLimitAndSortToFacet(agg, solrQuery) { + const field = agg.field; + const size = agg.size || 10; // default size if not specified + // const order = agg.orderBy === "_count" ? "count" : "index"; // mapping orderBy to Solr + // const direction = agg.orderDir === "desc" ? "desc" : "asc"; // default direction if not specified + + solrQuery[`f.${field}.facet.limit`] = size; + // solrQuery[`f.${field}.facet.sort`] = `${order}|${direction}`; +} + +es.getParam = function (value, def) { + return value !== undefined ? value : def; +}; diff --git a/src/edges.js b/src/edges.js index 376920b..37f0ecc 100644 --- a/src/edges.js +++ b/src/edges.js @@ -1,1088 +1,966 @@ // requires: $ // requires: es -if (!window.hasOwnProperty("edges")) { edges = {}} -if (!edges.hasOwnProperty("util")) { edges.util = {}} -if (!edges.hasOwnProperty("es")) { edges.es = {}} +if (!window.hasOwnProperty("edges")) { + edges = {}; +} +if (!edges.hasOwnProperty("util")) { + edges.util = {}; +} +if (!edges.hasOwnProperty("es")) { + edges.es = {}; +} ////////////////////////////////////////////////////////////////// // Main edge class edges.Edge = class { - constructor(params) { - ///////////////////////////////////////////// - // parameters that can be set via params arg - - // the jquery selector for the element where the edge will be deployed - this.selector = edges.util.getParam(params, "selector", "body"); - - // the base search url which will respond to elasticsearch queries. Generally ends with _search - this.searchUrl = edges.util.getParam(params, "searchUrl", false); - - // datatype for ajax requests to use - overall recommend using jsonp and proxying ES requests - // through a back end that can provide that - this.datatype = edges.util.getParam(params, "datatype", "jsonp"); - - // dictionary of queries to be run before the primary query is executed - // {"" : new es.Query(....)} - // results will appear with the same ids in this.preflightResults - // preflight queries are /not/ subject to the base query - this.preflightQueries = edges.util.getParam(params, "preflightQueries", false); - - // query that forms the basis of all queries that are assembled and run - // Note that baseQuery is inviolable - it's requirements will always be enforced - this.baseQuery = edges.util.getParam(params, "baseQuery", false); - - // query to use to initialise the search. Use this to set your opening - // values for things like page size, initial search terms, etc. Any request to - // reset the interface will return to this query - this.openingQuery = edges.util.getParam(params, "openingQuery", () => typeof es !== 'undefined' ? new es.Query() : false); - - // dictionary of functions that will generate secondary queries which also need to be - // run at the point that cycle() is called. These functions and their resulting - // queries will be run /after/ the primary query (so can take advantage of the - // results). Their results will be stored in this.secondaryResults. - // secondary queries are not subject the base query, although the functions - // may of course apply the base query too if they wish - // {"" : function() } - this.secondaryQueries = edges.util.getParam(params, "secondaryQueries", false); - - // dictionary mapping keys to urls that will be used for search. These should be - // the same keys as used in secondaryQueries, if those secondary queries should be - // issued against different urls than the primary search_url. - this.secondaryUrls = edges.util.getParam(params, "secondaryUrls", false); - - // should the init process do a search - this.initialSearch = edges.util.getParam(params, "initialSearch", true); - - // list of static files (e.g. data files) to be loaded at startup, and made available - // on the object for use by components - // {"id" : "", "url" : "", "processor" : edges.csv.newObjectByRow, "datatype" : "text", "opening" : } - this.staticFiles = edges.util.getParam(params, "staticFiles", []); - - // should the search url be synchronised with the browser's url bar after search - // and should queries be retrieved from the url on init - this.manageUrl = edges.util.getParam(params, "manageUrl", false); - - // query parameter in which the query for this edge instance will be stored - this.urlQuerySource = edges.util.getParam(params, "urlQuerySource", "source"); - - // options to be passed to es.Query.objectify when prepping the query to be placed in the URL - this.urlQueryOptions = edges.util.getParam(params, "urlQueryOptions", false); - - // template object that will be used to draw the frame for the edge. May be left - // blank, in which case the edge will assume that the elements are already rendered - // on the page by the caller - this.template = edges.util.getParam(params, "template", false); - - // list of all the components that are involved in this edge - this.components = edges.util.getParam(params, "components", []); - - // the query adapter - this.queryAdapter = edges.util.getParam(params, "queryAdapter", () => new edges.es.ESQueryAdapter()); - - // list of callbacks to be run synchronously with the edge instance as the argument - // (these bind at the same points as all the events are triggered, and are keyed the same way) - this.callbacks = edges.util.getParam(params, "callbacks", {}); - - ///////////////////////////////////////////// - // operational properties - - // the query most recently read from the url - this.urlQuery = false; + constructor(params) { + ///////////////////////////////////////////// + // parameters that can be set via params arg + + // the jquery selector for the element where the edge will be deployed + this.selector = edges.util.getParam(params, "selector", "body"); + + // the base search url which will respond to elasticsearch queries. Generally ends with _search + this.searchUrl = edges.util.getParam(params, "searchUrl", false); + + // datatype for ajax requests to use - overall recommend using jsonp and proxying ES requests + // through a back end that can provide that + this.datatype = edges.util.getParam(params, "datatype", "jsonp"); + + // dictionary of queries to be run before the primary query is executed + // {"" : new es.Query(....)} + // results will appear with the same ids in this.preflightResults + // preflight queries are /not/ subject to the base query + this.preflightQueries = edges.util.getParam( + params, + "preflightQueries", + false + ); + + // query that forms the basis of all queries that are assembled and run + // Note that baseQuery is inviolable - it's requirements will always be enforced + this.baseQuery = edges.util.getParam(params, "baseQuery", false); + + // query to use to initialise the search. Use this to set your opening + // values for things like page size, initial search terms, etc. Any request to + // reset the interface will return to this query + this.openingQuery = edges.util.getParam(params, "openingQuery", () => + typeof es !== "undefined" ? new es.Query() : false + ); + + // dictionary of functions that will generate secondary queries which also need to be + // run at the point that cycle() is called. These functions and their resulting + // queries will be run /after/ the primary query (so can take advantage of the + // results). Their results will be stored in this.secondaryResults. + // secondary queries are not subject the base query, although the functions + // may of course apply the base query too if they wish + // {"" : function() } + this.secondaryQueries = edges.util.getParam( + params, + "secondaryQueries", + false + ); + + // dictionary mapping keys to urls that will be used for search. These should be + // the same keys as used in secondaryQueries, if those secondary queries should be + // issued against different urls than the primary search_url. + this.secondaryUrls = edges.util.getParam(params, "secondaryUrls", false); + + // should the init process do a search + this.initialSearch = edges.util.getParam(params, "initialSearch", true); + + // list of static files (e.g. data files) to be loaded at startup, and made available + // on the object for use by components + // {"id" : "", "url" : "", "processor" : edges.csv.newObjectByRow, "datatype" : "text", "opening" : } + this.staticFiles = edges.util.getParam(params, "staticFiles", []); + + // should the search url be synchronised with the browser's url bar after search + // and should queries be retrieved from the url on init + this.manageUrl = edges.util.getParam(params, "manageUrl", false); + + // query parameter in which the query for this edge instance will be stored + this.urlQuerySource = edges.util.getParam( + params, + "urlQuerySource", + "source" + ); + + // options to be passed to es.Query.objectify when prepping the query to be placed in the URL + this.urlQueryOptions = edges.util.getParam( + params, + "urlQueryOptions", + false + ); + + // template object that will be used to draw the frame for the edge. May be left + // blank, in which case the edge will assume that the elements are already rendered + // on the page by the caller + this.template = edges.util.getParam(params, "template", false); + + // list of all the components that are involved in this edge + this.components = edges.util.getParam(params, "components", []); + + // the query adapter + this.queryAdapter = edges.util.getParam( + params, + "queryAdapter", + () => new edges.es.ESQueryAdapter() + ); + + // list of callbacks to be run synchronously with the edge instance as the argument + // (these bind at the same points as all the events are triggered, and are keyed the same way) + this.callbacks = edges.util.getParam(params, "callbacks", {}); + + ///////////////////////////////////////////// + // operational properties + + // the query most recently read from the url + this.urlQuery = false; + + // original url parameters + this.urlParams = {}; + + // the short url for this page + this.shortUrl = false; + + // the last primary ES query object that was executed + this.currentQuery = false; + + // the last result object from the ES layer + this.result = false; + + // the results of the preflight queries, keyed by their id + this.preflightResults = {}; + + // the actual secondary queries derived from the functions in this.secondaryQueries; + this.realisedSecondaryQueries = {}; + + // results of the secondary queries, keyed by their id + this.secondaryResults = {}; + + // if the search is currently executing + this.searching = false; + + // jquery object that represents the selected element + this.context = false; + + // raw access to this.staticFiles loaded resources, keyed by id + this.static = {}; - // original url parameters - this.urlParams = {}; + // access to processed static files, keyed by id + this.resources = {}; - // the short url for this page - this.shortUrl = false; + // list of static resources where errors were encountered + this.errorLoadingStatic = []; - // the last primary ES query object that was executed - this.currentQuery = false; + ////////////////////////////////////////// + // now kick off the edge + this.startup(); + } - // the last result object from the ES layer - this.result = false; + ////////////////////////////////////////////////// + // Startup - // the results of the preflight queries, keyed by their id - this.preflightResults = {}; + startup() { + // obtain the jquery context for all our operations + this.context = $(this.selector); - // the actual secondary queries derived from the functions in this.secondaryQueries; - this.realisedSecondaryQueries = {}; + // trigger the edges:init event + this.trigger("edges:pre-init"); - // results of the secondary queries, keyed by their id - this.secondaryResults = {}; - - // if the search is currently executing - this.searching = false; - - // jquery object that represents the selected element - this.context = false; - - // raw access to this.staticFiles loaded resources, keyed by id - this.static = {}; - - // access to processed static files, keyed by id - this.resources = {}; - - // list of static resources where errors were encountered - this.errorLoadingStatic = []; - - - ////////////////////////////////////////// - // now kick off the edge - this.startup(); + // if we are to manage the URL, attempt to pull a query from it + if (this.manageUrl) { + var urlParams = edges.util.getUrlParams(); + if (this.urlQuerySource in urlParams) { + this.urlQuery = new es.Query({ raw: urlParams[this.urlQuerySource] }); + delete urlParams[this.urlQuerySource]; + } + this.urlParams = urlParams; } - ////////////////////////////////////////////////// - // Startup - - startup() { - // obtain the jquery context for all our operations - this.context = $(this.selector); - - // trigger the edges:init event - this.trigger("edges:pre-init"); - - // if we are to manage the URL, attempt to pull a query from it - if (this.manageUrl) { - var urlParams = edges.util.getUrlParams(); - if (this.urlQuerySource in urlParams) { - this.urlQuery = new es.Query({raw : urlParams[this.urlQuerySource]}); - delete urlParams[this.urlQuerySource]; - } - this.urlParams = urlParams; - } - - // render the template if necessary - if (this.template) { - this.template.draw(this); - } - - // call each of the components to initialise themselves - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.init(this); - } - - // now call each component to render itself (pre-search) - this.draw(); - - // load any static files - this will happen asynchronously, so afterwards - // we call finaliseStartup to finish the process - // var onward = edges.edges.util.objClosure(this, "startupPart2"); - let onward = () => this.startupPart2() - this.loadStaticsAsync(onward); + // render the template if necessary + if (this.template) { + this.template.draw(this); } - startupPart2() { - // FIXME: at this point we should check whether the statics all loaded correctly - // var onward = edges.edges.util.objClosure(this, "startupPart3"); - let onward = () => this.startupPart3() - this.runPreflightQueries(onward); - }; - - startupPart3() { - - // determine whether to initialise with either the openingQuery or the urlQuery - var requestedQuery = this.openingQuery; - if (this.urlQuery) { - // if there is a URL query, then we open with that, and then forget it - requestedQuery = this.urlQuery; - this.urlQuery = false - } - - // request the components to contribute to the query - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.contrib(requestedQuery); - } - - // finally push the query, which will reconcile it with the baseQuery - this.pushQuery(requestedQuery); - - // trigger the edges:post-init event - this.trigger("edges:post-init"); - - // now issue a query - this.cycle(); - }; - - //////////////////////////////////////////////////// - // Cycle - - cycle() { - // if a search is currently executing, don't do anything, else turn it on - // FIXME: should we queue them up? - see the d3 map for an example of how to do this - if (this.searching) { - return; - } - this.searching = true; - - // invalidate the short url - this.shortUrl = false; - - // pre query event - this.trigger("edges:pre-query"); - - // if we are managing the url space, use pushState to set it - if (this.manageUrl) { - this.updateUrl(); - } - - // if there's a search url, do a query, otherwise call synchronise and draw directly - if (this.searchUrl) { - // var onward = edges.edges.util.objClosure(this, "cyclePart2"); - let onward = () => this.cyclePart2(); - this.doPrimaryQuery(onward); - } else { - this.cyclePart2(); - } + // call each of the components to initialise themselves + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.init(this); } - cyclePart2() { - // var onward = edges.edges.util.objClosure(this, "cyclePart3"); - let onward = () => this.cyclePart3(); - this.runSecondaryQueries(onward); - } + // now call each component to render itself (pre-search) + this.draw(); - cyclePart3() { - this.synchronise(); + // load any static files - this will happen asynchronously, so afterwards + // we call finaliseStartup to finish the process + // var onward = edges.edges.util.objClosure(this, "startupPart2"); + let onward = () => this.startupPart2(); + this.loadStaticsAsync(onward); + } - // pre-render trigger - this.trigger("edges:pre-render"); - // render - this.draw(); - // post render trigger - this.trigger("edges:post-render"); + startupPart2() { + // FIXME: at this point we should check whether the statics all loaded correctly + // var onward = edges.edges.util.objClosure(this, "startupPart3"); + let onward = () => this.startupPart3(); + this.runPreflightQueries(onward); + } - // searching has completed, so flip the switch back - this.searching = false; + startupPart3() { + // determine whether to initialise with either the openingQuery or the urlQuery + var requestedQuery = this.openingQuery; + if (this.urlQuery) { + // if there is a URL query, then we open with that, and then forget it + requestedQuery = this.urlQuery; + this.urlQuery = false; } - //////////////////////////////////////////////////// - // utilities required during startup - - loadStaticsAsync(callback) { - if (!this.staticFiles || this.staticFiles.length === 0) { - this.trigger("edges:post-load-static"); - callback(); - return; - } - - // FIXME: this could be done with a Promise.all - var that = this; - var pg = new edges.util.AsyncGroup({ - list: this.staticFiles, - action: function(params) { - var entry = params.entry; - var success = params.success_callback; - var error = params.error_callback; - - var id = entry.id; - var url = entry.url; - var datatype = edges.util.getParam(entry.datatype, "text"); - - $.ajax({ - type: "get", - url: url, - dataType: datatype, - success: success, - error: error - }) - }, - successCallbackArgs: ["data"], - success: function(params) { - var data = params.data; - var entry = params.entry; - if (entry.processor) { - var processed = entry.processor({data : data}); - that.resources[entry.id] = processed; - if (entry.opening) { - entry.opening({resource : processed, edge: that}); - } - } - that.static[entry.id] = data; - }, - errorCallbackArgs : ["data"], - error: function(params) { - that.errorLoadingStatic.push(params.entry.id); - that.trigger("edges:error-load-static"); - }, - carryOn: function() { - that.trigger("edges:post-load-static"); - callback(); - } - }); - - pg.process(); + // request the components to contribute to the query + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.contrib(requestedQuery); } - runPreflightQueries(callback) { - if (!this.preflightQueries || Object.keys(this.preflightQueries).length === 0) { - callback(); - return; - } + // finally push the query, which will reconcile it with the baseQuery + this.pushQuery(requestedQuery); - this.trigger("edges:pre-preflight"); + // trigger the edges:post-init event + this.trigger("edges:post-init"); - var entries = []; - var ids = Object.keys(this.preflightQueries); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - entries.push({id: id, query: this.preflightQueries[id]}); - } + // now issue a query + this.cycle(); + } - var that = this; - var pg = new edges.util.AsyncGroup({ - list: entries, - action: function(params) { - var entry = params.entry; - var success = params.success_callback; - var error = params.error_callback; - - es.doQuery({ - search_url: that.searchUrl, - queryobj: entry.query.objectify(), - datatype: that.datatype, - success: success, - error: error - }); - }, - successCallbackArgs: ["result"], - success: function(params) { - var result = params.result; - var entry = params.entry; - that.preflightResults[entry.id] = result; - }, - errorCallbackArgs : ["result"], - error: function(params) { - that.trigger("edges:error-preflight"); - }, - carryOn: function() { - that.trigger("edges:post-preflight"); - callback(); - } - }); + //////////////////////////////////////////////////// + // Cycle - pg.process(); + cycle() { + // if a search is currently executing, don't do anything, else turn it on + // FIXME: should we queue them up? - see the d3 map for an example of how to do this + if (this.searching) { + return; } + this.searching = true; - /////////////////////////////////////////////////// - // Utilities required during cycle + // invalidate the short url + this.shortUrl = false; - doPrimaryQuery(callback) { - var context = {"callback" : callback}; + // pre query event + this.trigger("edges:pre-query"); - this.queryAdapter.doQuery({ - edge: this, - success: edges.util.objClosure(this, "querySuccess", ["result"], context), - error: edges.util.objClosure(this, "queryFail", ["response"], context) - }); + // if we are managing the url space, use pushState to set it + if (this.manageUrl) { + this.updateUrl(); } - runSecondaryQueries(callback) { - this.realisedSecondaryQueries = {}; - if (!this.secondaryQueries || Object.keys(this.secondaryQueries).length === 0) { - callback(); - return; - } - - // generate the query objects to be executed - var entries = []; - for (var key in this.secondaryQueries) { - var entry = {}; - entry["query"] = this.secondaryQueries[key](this); - entry["id"] = key; - entry["searchUrl"] = this.searchUrl; - if (this.secondaryUrls !== false && this.secondaryUrls.hasOwnProperty(key)) { - entry["searchUrl"] = this.secondaryUrls[key] - } - entries.push(entry); - this.realisedSecondaryQueries[key] = entry.query; - } - - var that = this; - var pg = new edges.util.AsyncGroup({ - list: entries, - action: function(params) { - var entry = params.entry; - var success = params.success_callback; - var error = params.error_callback; - - es.doQuery({ - search_url: entry.searchUrl, - queryobj: entry.query.objectify(), - datatype: that.datatype, - success: success, - complete: false - }); - }, - successCallbackArgs: ["result"], - success: function(params) { - var result = params.result; - var entry = params.entry; - that.secondaryResults[entry.id] = result; - }, - errorCallbackArgs : ["result"], - error: function(params) { - // FIXME: not really sure what to do about this - }, - carryOn: function() { - callback(); - } + // if there's a search url, do a query, otherwise call synchronise and draw directly + if (this.searchUrl) { + // var onward = edges.edges.util.objClosure(this, "cyclePart2"); + let onward = () => this.cyclePart2(); + this.doPrimaryQuery(onward); + } else { + this.cyclePart2(); + } + } + + cyclePart2() { + // var onward = edges.edges.util.objClosure(this, "cyclePart3"); + let onward = () => this.cyclePart3(); + this.runSecondaryQueries(onward); + } + + cyclePart3() { + this.synchronise(); + + // pre-render trigger + this.trigger("edges:pre-render"); + // render + this.draw(); + // post render trigger + this.trigger("edges:post-render"); + + // searching has completed, so flip the switch back + this.searching = false; + } + + //////////////////////////////////////////////////// + // utilities required during startup + + loadStaticsAsync(callback) { + if (!this.staticFiles || this.staticFiles.length === 0) { + this.trigger("edges:post-load-static"); + callback(); + return; + } + + // FIXME: this could be done with a Promise.all + var that = this; + var pg = new edges.util.AsyncGroup({ + list: this.staticFiles, + action: function (params) { + var entry = params.entry; + var success = params.success_callback; + var error = params.error_callback; + + var id = entry.id; + var url = entry.url; + var datatype = edges.util.getParam(entry.datatype, "text"); + + $.ajax({ + type: "get", + url: url, + dataType: datatype, + success: success, + error: error, }); + }, + successCallbackArgs: ["data"], + success: function (params) { + var data = params.data; + var entry = params.entry; + if (entry.processor) { + var processed = entry.processor({ data: data }); + that.resources[entry.id] = processed; + if (entry.opening) { + entry.opening({ resource: processed, edge: that }); + } + } + that.static[entry.id] = data; + }, + errorCallbackArgs: ["data"], + error: function (params) { + that.errorLoadingStatic.push(params.entry.id); + that.trigger("edges:error-load-static"); + }, + carryOn: function () { + that.trigger("edges:post-load-static"); + callback(); + }, + }); - pg.process(); - } - - //////////////////////////////////////////////////// - // functions for working with the queries + pg.process(); + } - cloneQuery() { - if (this.currentQuery) { - return this.currentQuery.clone(); - } - return false; + runPreflightQueries(callback) { + if ( + !this.preflightQueries || + Object.keys(this.preflightQueries).length === 0 + ) { + callback(); + return; } - pushQuery(query) { - if (this.baseQuery) { - query.merge(this.baseQuery); - } - this.currentQuery = query; - } + this.trigger("edges:pre-preflight"); - cloneBaseQuery() { - if (this.baseQuery) { - return this.baseQuery.clone(); - } - return new es.Query(); + var entries = []; + var ids = Object.keys(this.preflightQueries); + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + entries.push({ id: id, query: this.preflightQueries[id] }); } - cloneOpeningQuery() { - if (this.openingQuery) { - return this.openingQuery.clone(); - } - return new es.Query(); - } + var that = this; + var pg = new edges.util.AsyncGroup({ + list: entries, + action: function (params) { + var entry = params.entry; + var success = params.success_callback; + var error = params.error_callback; - queryFail(params) { - var callback = params.callback; - var response = params.response; - this.trigger("edges:query-fail"); - if (response.hasOwnProperty("responseText")) { - console.log("ERROR: query fail: " + response.responseText); - } - if (response.hasOwnProperty("error")) { - console.log("ERROR: search execution fail: " + response.error); - } + es.doQuery({ + search_url: that.searchUrl, + queryobj: entry.query.objectify(), + datatype: that.datatype, + success: success, + error: error, + }); + }, + successCallbackArgs: ["result"], + success: function (params) { + var result = params.result; + var entry = params.entry; + that.preflightResults[entry.id] = result; + }, + errorCallbackArgs: ["result"], + error: function (params) { + that.trigger("edges:error-preflight"); + }, + carryOn: function () { + that.trigger("edges:post-preflight"); callback(); - }; + }, + }); - querySuccess(params) { - this.result = params.result; - var callback = params.callback; + pg.process(); + } - // success trigger - this.trigger("edges:query-success"); - callback(); - }; - - ////////////////////////////////////////////////// - // URL Management + /////////////////////////////////////////////////// + // Utilities required during cycle - updateUrl() { - var currentQs = window.location.search; - var qs = "?" + this.fullUrlQueryString(); + doPrimaryQuery(callback) { + var context = { callback: callback }; - if (currentQs === qs) { - return; // no need to push the state - } + this.queryAdapter.doQuery({ + edge: this, + success: edges.util.objClosure(this, "querySuccess", ["result"], context), + error: edges.util.objClosure(this, "queryFail", ["response"], context), + }); + } + + runSecondaryQueries(callback) { + this.realisedSecondaryQueries = {}; + + if ( + !this.secondaryQueries || + Object.keys(this.secondaryQueries).length === 0 + ) { + callback(); + return; + } + + // generate the query objects to be executed + var entries = []; + for (var key in this.secondaryQueries) { + var entry = {}; + entry["query"] = this.secondaryQueries[key](this); + entry["id"] = key; + entry["searchUrl"] = this.searchUrl; + if ( + this.secondaryUrls !== false && + this.secondaryUrls.hasOwnProperty(key) + ) { + entry["searchUrl"] = this.secondaryUrls[key]; + } + entries.push(entry); + this.realisedSecondaryQueries[key] = entry.query; + } + + var that = this; + var pg = new edges.util.AsyncGroup({ + list: entries, + action: function (params) { + var entry = params.entry; + var success = params.success_callback; + var error = params.error_callback; - var url = new URL(window.location.href); - url.search = qs; + es.doQuery({ + search_url: entry.searchUrl, + queryobj: entry.query.objectify(), + datatype: that.datatype, + success: success, + complete: false, + }); + }, + successCallbackArgs: ["result"], + success: function (params) { + var result = params.result; + var entry = params.entry; + that.secondaryResults[entry.id] = result; + }, + errorCallbackArgs: ["result"], + error: function (params) { + // FIXME: not really sure what to do about this + }, + carryOn: function () { + callback(); + }, + }); - if (currentQs === "") { - window.history.replaceState("", "", url.toString()); - } else { - window.history.pushState("", "", url.toString()); - } - } + pg.process(); + } - fullUrl() { - var args = this.fullQueryArgs(); - var fragment = ""; - if (args["#"]) { - fragment = "#" + args["#"]; - delete args["#"]; - } - var wloc = window.location.toString(); - var bits = wloc.split("?"); - var url = bits[0] + "?" + this._makeUrlQuery(args) + fragment; - return url; - }; + //////////////////////////////////////////////////// + // functions for working with the queries - fullUrlQueryString() { - return this._makeUrlQuery(this.fullQueryArgs()) + cloneQuery() { + if (this.currentQuery) { + return this.currentQuery.clone(); } + return false; + } - fullQueryArgs() { - var args = $.extend(true, {}, this.urlParams); - $.extend(args, this.urlQueryArg()); - return args; - }; - - urlQueryArg(objectify_options) { - if (!objectify_options) { - if (this.urlQueryOptions) { - objectify_options = this.urlQueryOptions - } else { - objectify_options = { - include_query_string : true, - include_filters : true, - include_paging : true, - include_sort : true, - include_fields : false, - include_aggregations : false - } - } - } - var q = JSON.stringify(this.currentQuery.objectify(objectify_options)); - var obj = {}; - obj[this.urlQuerySource] = encodeURIComponent(q); - return obj; + pushQuery(query) { + if (this.baseQuery) { + query.merge(this.baseQuery); } + this.currentQuery = query; + } - _makeUrlQuery(args) { - var keys = Object.keys(args); - var entries = []; - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var val = args[key]; - entries.push(key + "=" + val); // NOTE we do not escape - this should already be done - } - return entries.join("&"); + cloneBaseQuery() { + if (this.baseQuery) { + return this.baseQuery.clone(); } + return new es.Query(); + } - ///////////////////////////////////////////////// - // lifecycle functions - - synchronise() { - // ask the components to synchronise themselves with the latest state - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.synchronise() - } + cloneOpeningQuery() { + if (this.openingQuery) { + return this.openingQuery.clone(); } + return new es.Query(); + } - draw() { - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.draw(this); - } - }; + queryFail(params) { + var callback = params.callback; + var response = params.response; + this.trigger("edges:query-fail"); + if (response.hasOwnProperty("responseText")) { + console.log("ERROR: query fail: " + response.responseText); + } + if (response.hasOwnProperty("error")) { + console.log("ERROR: search execution fail: " + response.error); + } + callback(); + } - reset() { - // tell the world we're about to reset - this.trigger("edges:pre-reset"); + querySuccess(params) { + this.result = params.result; + var callback = params.callback; - // clone from the opening query - var requestedQuery = this.cloneOpeningQuery(); + // success trigger + this.trigger("edges:query-success"); + callback(); + } - // request the components to contribute to the query - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.contrib(requestedQuery); - } + ////////////////////////////////////////////////// + // URL Management - // push the query, which will reconcile it with the baseQuery - this.pushQuery(requestedQuery); + updateUrl() { + var currentQs = window.location.search; + const urlString = this.fullUrlQueryString(); + var qs = urlString ? `?${urlString}` : ""; - // tell the world that we've done the reset - this.trigger("edges:post-reset"); - - // now execute the query - // this.doQuery(); - this.cycle(); - }; - - sleep() { - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.sleep(); - } - }; + if (currentQs === qs) { + return; // no need to push the state + } - wake() { - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - component.wake(); - } - }; + var url = new URL(window.location.href); + url.search = qs; - trigger(event_name) { - if (event_name in this.callbacks) { - this.callbacks[event_name](this); + if (currentQs === "") { + window.history.replaceState("", "", url.toString()); + } else { + window.history.pushState("", "", url.toString()); + } + } + + fullUrl() { + var args = this.fullQueryArgs(); + var fragment = ""; + if (args["#"]) { + fragment = "#" + args["#"]; + delete args["#"]; + } + var wloc = window.location.toString(); + var bits = wloc.split("?"); + var url = bits[0] + "?" + this._makeUrlQuery(args) + fragment; + return url; + } + + fullUrlQueryString() { + return this._makeUrlQuery(this.fullQueryArgs()); + } + + fullQueryArgs() { + var args = $.extend(true, {}, this.urlParams); + $.extend(args, this.urlQueryArg()); + return args; + } + + urlQueryArg(objectify_options) { + if (this.urlQuerySource) { + if (!objectify_options) { + if (this.urlQueryOptions) { + objectify_options = this.urlQueryOptions; + } else { + objectify_options = { + include_query_string: true, + include_filters: true, + include_paging: true, + include_sort: true, + include_fields: false, + include_aggregations: false, + }; } - this.context.trigger(event_name); - }; - - //////////////////////////////////////////// - // accessors + } + var q = JSON.stringify(this.currentQuery.objectify(objectify_options)); + var obj = {}; + obj[this.urlQuerySource] = encodeURIComponent(q); + return obj; + } + + return {}; + } - getComponent(params) { - var id = params.id; - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - if (component.id === id) { - return component; - } - } - return false; - }; + _makeUrlQuery(args) { + var keys = Object.keys(args); + var entries = []; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var val = args[key]; + entries.push(key + "=" + val); // NOTE we do not escape - this should already be done + } + return entries.join("&"); + } + + ///////////////////////////////////////////////// + // lifecycle functions + + synchronise() { + // ask the components to synchronise themselves with the latest state + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.synchronise(); + } + } + + draw() { + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.draw(this); + } + } + + reset() { + // tell the world we're about to reset + this.trigger("edges:pre-reset"); + + // clone from the opening query + var requestedQuery = this.cloneOpeningQuery(); + + // request the components to contribute to the query + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.contrib(requestedQuery); + } + + // push the query, which will reconcile it with the baseQuery + this.pushQuery(requestedQuery); + + // tell the world that we've done the reset + this.trigger("edges:post-reset"); + + // now execute the query + // this.doQuery(); + this.cycle(); + } + + sleep() { + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.sleep(); + } + } + + wake() { + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + component.wake(); + } + } + + trigger(event_name) { + if (event_name in this.callbacks) { + this.callbacks[event_name](this); + } + this.context.trigger(event_name); + } + + //////////////////////////////////////////// + // accessors + + getComponent(params) { + var id = params.id; + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + if (component.id === id) { + return component; + } + } + return false; + } - // return components in the requested category - category(cat) { - var comps = []; - for (var i = 0; i < this.components.length; i++) { - var component = this.components[i]; - if (component.category === cat) { - comps.push(component); - } - } - return comps; - }; + // return components in the requested category + category(cat) { + var comps = []; + for (var i = 0; i < this.components.length; i++) { + var component = this.components[i]; + if (component.category === cat) { + comps.push(component); + } + } + return comps; + } - jq(selector) { - return $(selector, this.context); - }; -} + jq(selector) { + return $(selector, this.context); + } +}; ////////////////////////////////////////////////////////////////// // Framework superclasses edges.QueryAdapter = class { - doQuery(params) {}; -} + doQuery(params) {} +}; edges.Template = class { - draw(edge) {} -} + draw(edge) {} +}; edges.Component = class { - constructor(params) { - this.id = edges.util.getParam(params, "id"); - this.renderer = edges.util.getParam(params, "renderer"); - this.category = edges.util.getParam(params, "category", false); - } + constructor(params) { + this.id = edges.util.getParam(params, "id"); + this.renderer = edges.util.getParam(params, "renderer"); + this.category = edges.util.getParam(params, "category", false); + } - init(edge) { - this.edge = edge; - this.context = this.edge.jq("#" + this.id); - if (this.renderer) { - this.renderer.init(this); - } + init(edge) { + this.edge = edge; + this.context = this.edge.jq("#" + this.id); + if (this.renderer) { + this.renderer.init(this); } + } - draw() { - if (this.renderer) { - this.renderer.draw(); - } + draw() { + if (this.renderer) { + this.renderer.draw(); } + } - sleep() { - if (this.renderer) { - this.renderer.sleep(); - } + sleep() { + if (this.renderer) { + this.renderer.sleep(); } + } - wake() { - if (this.renderer) { - this.renderer.wake(); - } - }; - - // convenience method for any renderer rendering a component - jq(selector) { - return this.edge.jq(selector); + wake() { + if (this.renderer) { + this.renderer.wake(); } + } - // methods to be implemented by subclasses - contrib(query) {} - synchronise() {} -} + // convenience method for any renderer rendering a component + jq(selector) { + return this.edge.jq(selector); + } + + // methods to be implemented by subclasses + contrib(query) {} + synchronise() {} +}; edges.Renderer = class { - init(component) { - this.component = component - } + init(component) { + this.component = component; + } - draw() {}; - sleep() {}; - wake() {} -} + draw() {} + sleep() {} + wake() {} +}; ////////////////////////////////////////////////////////////////// // Event binding utilities -edges.on = function(selector, event, caller, targetFunction, delay, conditional, preventDefault) { - if (preventDefault === undefined) { - preventDefault = true; - } - // if the caller has an inner component (i.e. it is a Renderer), use the component's id - // otherwise, if it has a namespace (which is true of Renderers or Templates) use that - if (caller.component && caller.component.id) { - event = event + "." + caller.component.id; - } else if (caller.namespace) { - event = event + "." + caller.namespace; - } - - // create the closure to be called on the event - var clos = edges.util.eventClosure(caller, targetFunction, conditional, preventDefault); - - if (delay) { - clos = edges.util.delayer(clos, delay); - } - - // now bind the closure directly or with delay - // if the caller has an inner component (i.e. it is a Renderer) use the components jQuery selector - // otherwise, if it has an inner, use the selector on that. - // if (delay) { - // if (caller.component) { - // caller.component.jq(selector).bindWithDelay(event, clos, delay); - // } else if (caller.edge) { - // caller.edge.jq(selector).bindWithDelay(event, clos, delay); - // } else { - // $(selector).bindWithDelay(event, clos, delay); - // } - // } else { - if (caller.component) { - var element = caller.component.jq(selector); - element.off(event); - element.on(event, clos); - } else if (caller.edge) { - var element = caller.edge.jq(selector); - element.off(event); - element.on(event, clos); - } else { - var element = $(selector); - element.off(event); - element.on(event, clos); - } - // } -} - -edges.off = function(selector, event, caller) { - // if the caller has an inner component (i.e. it is a Renderer), use the component's id - // otherwise, if it has a namespace (which is true of Renderers or Templates) use that - if (caller.component && caller.component.id) { - event = event + "." + caller.component.id; - } else if (caller.namespace) { - event = event + "." + caller.namespace; - } - - if (caller.component) { - var element = caller.component.jq(selector); - element.off(event); - } else if (caller.edge) { - var element = caller.edge.jq(selector); - element.off(event); - } else { - var element = $(selector); - element.off(event); - } -} +edges.on = function ( + selector, + event, + caller, + targetFunction, + delay, + conditional, + preventDefault +) { + if (preventDefault === undefined) { + preventDefault = true; + } + // if the caller has an inner component (i.e. it is a Renderer), use the component's id + // otherwise, if it has a namespace (which is true of Renderers or Templates) use that + if (caller.component && caller.component.id) { + event = event + "." + caller.component.id; + } else if (caller.namespace) { + event = event + "." + caller.namespace; + } + + // create the closure to be called on the event + var clos = edges.util.eventClosure( + caller, + targetFunction, + conditional, + preventDefault + ); + + if (delay) { + clos = edges.util.delayer(clos, delay); + } + + // now bind the closure directly or with delay + // if the caller has an inner component (i.e. it is a Renderer) use the components jQuery selector + // otherwise, if it has an inner, use the selector on that. + // if (delay) { + // if (caller.component) { + // caller.component.jq(selector).bindWithDelay(event, clos, delay); + // } else if (caller.edge) { + // caller.edge.jq(selector).bindWithDelay(event, clos, delay); + // } else { + // $(selector).bindWithDelay(event, clos, delay); + // } + // } else { + if (caller.component) { + var element = caller.component.jq(selector); + element.off(event); + element.on(event, clos); + } else if (caller.edge) { + var element = caller.edge.jq(selector); + element.off(event); + element.on(event, clos); + } else { + var element = $(selector); + element.off(event); + element.on(event, clos); + } + // } +}; + +edges.off = function (selector, event, caller) { + // if the caller has an inner component (i.e. it is a Renderer), use the component's id + // otherwise, if it has a namespace (which is true of Renderers or Templates) use that + if (caller.component && caller.component.id) { + event = event + "." + caller.component.id; + } else if (caller.namespace) { + event = event + "." + caller.namespace; + } + + if (caller.component) { + var element = caller.component.jq(selector); + element.off(event); + } else if (caller.edge) { + var element = caller.edge.jq(selector); + element.off(event); + } else { + var element = $(selector); + element.off(event); + } +}; ////////////////////////////////////////////////////////////////// // Common/default implementations of framework classes edges.es.ESQueryAdapter = class extends edges.QueryAdapter { - doQuery(params) { - var edge = params.edge; - var query = params.query; - var success = params.success; - var error = params.error; - - if (!query) { - query = edge.currentQuery; - } - - es.doQuery({ - search_url: edge.searchUrl, - queryobj: query.objectify(), - datatype: edge.datatype, - success: success, - error: error - }); - }; -} + doQuery(params) { + var edge = params.edge; + var query = params.query; + var success = params.success; + var error = params.error; + + if (!query) { + query = edge.currentQuery; + } + + es.doQuery({ + search_url: edge.searchUrl, + queryobj: query.objectify(), + datatype: edge.datatype, + success: success, + error: error, + }); + } +}; // Solr query adapter edges.es.SolrQueryAdapter = class extends edges.QueryAdapter { - doQuery(params) { - var edge = params.edge; - var query = params.query; - var success = params.success; - var error = params.error; - - if (!query) { - query = edge.currentQuery; - } - - const args = this._es2solr({ query : query }); - - // Execute the Solr query - this._solrQuery({ edge, success, error, solrArgs: args }); - }; - - // Method to execute the Solr query - _solrQuery({ solrArgs, edge, success, error }) { - const searchUrl = edge.searchUrl; - - // Generate the Solr query URL - const fullUrl = this._args2URL({ baseUrl: searchUrl, args: solrArgs }); - - var error_callback = this._queryError(error); - var success_callback = this._querySuccess(success, error_callback); - - // Perform the HTTP GET request to Solr - $.get({ - url: fullUrl, - datatype: edge ? edge.datatype : "jsonp", - success: success_callback, - error: error_callback, - jsonp: 'json.wrf' - }); - } - - // Method to convert es query to Solr query - _es2solr({ query }) { - const solrQuery = {}; - let solrFacets = [] - - // Handle the query part - if (query.query) { - const queryPart = query.query; - if (queryPart.match) { - const field = Object.keys(queryPart.match)[0]; - const value = queryPart.match[field]; - solrQuery.q = `${field}:${value}`; - } else if (queryPart.range) { - const field = Object.keys(queryPart.range)[0]; - const range = queryPart.range[field]; - const rangeQuery = `${field}:[${range.gte || '*'} TO ${range.lte || '*'}]`; - solrQuery.fq = rangeQuery; - } else if (queryPart.match_all) { - solrQuery.q = `*:*`; - } - } else { - solrQuery.q = `*:*`; - } - - // Handle pagination - if (query.from !== undefined) { - if (typeof query.from == "boolean" && !query.from) { - solrQuery.start = 0 - } else { - solrQuery.start = query.from; - } - } - if (query.size !== undefined) { - if (typeof query.size == "boolean" && !query.size) { - solrQuery.rows = 10 - } else { - solrQuery.rows = query.size; - } - - } - - // Handle sorting - if (query && query.sort.length > 0) { - solrQuery.sort = query.sort.map(sortOption => { - const sortField = sortOption.field; - const sortOrder = sortOption.order === "desc" ? "desc" : "asc"; - return `${sortField} ${sortOrder}`; - }).join(', '); - } - - if (query && query.aggs.length > 0) { - let facets = query.aggs.map(agg => this._convertAggToFacet(agg)); - solrQuery.factes = facets.join(','); - } - - solrQuery.wt = "json" - - return solrQuery; - } - - _args2URL({ baseUrl, args }) { - const qParts = Object.keys(args).flatMap(k => { - const v = args[k]; - if (Array.isArray(v)) { - return v.map(item => `${encodeURIComponent(k)}=${encodeURIComponent(item)}`); - } - return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`; - }); - - const qs = qParts.join("&"); - return `${baseUrl}?${qs}`; - } - - _convertAggToFacet(agg) { - const field = agg.field; - const name = agg.name; - const size = agg.size || 10; // default size if not specified - const order = agg.orderBy === "_count" ? "count" : "index"; // mapping orderBy to Solr - const direction = agg.orderDir === "desc" ? "desc" : "asc"; // default direction if not specified - - return `facet.field={!key=${name}}${field}&f.${field}.facet.limit=${size}&f.${field}.facet.sort=${order} ${direction}`; - } - - _querySuccess(callback, error_callback) { - return function(data) { - if (data.hasOwnProperty("error")) { - error_callback(data); - return; - } - - var result = new SolrResult({raw: data}); - callback(result); - } - } - - _queryError(callback) { - return function(data) { - if (callback) { - callback(data); - } else { - throw new Error(data); - } - } - } -} - -// Result class for solr -class SolrResult { - constructor(params) { - this.data = JSON.parse(params.raw); - } - - buckets(facet_name) { - if (this.data.facet_counts) { - if (this.data.facet_counts.facet_fields && this.data.facet_counts.facet_fields[facet_name]) { - return this._convertFacetToBuckets(this.data.facet_counts.facet_fields[facet_name]); - } else if (this.data.facet_counts.facet_queries && this.data.facet_counts.facet_queries[facet_name]) { - return this._convertFacetToBuckets(this.data.facet_counts.facet_queries[facet_name]); - } - } - return []; - } - - _convertFacetToBuckets(facet) { - let buckets = []; - for (let i = 0; i < facet.length; i += 2) { - buckets.push({ - key: facet[i], - doc_count: facet[i + 1] - }); - } - return buckets; - } - - aggregation(facet_name) { - return { - buckets: this.buckets(facet_name) - }; - } - - results() { - var res = []; - if (this.data.response && this.data.response.docs) { - for (var i = 0; i < this.data.response.docs.length; i++) { - res.push(this.data.response.docs[i]); - } - } - - return res; - } - - total() { - if (this.data.response && this.data.response.numFound !== undefined) { - return parseInt(this.data.response.numFound); - } - return false; - } -} + doQuery(params) { + var edge = params.edge; + var query = params.query; + var success = params.success; + var error = params.error; + + if (!query) { + query = edge.currentQuery; + } + + es.doQuery({ + search_url: edge.searchUrl, + query: query, + datatype: edge.datatype, + success: success, + error: error, + }); + } +}; ////////////////////////////////////////////////////////////////// // utilities -edges.util.getParam = function(params, key, def) { - function _getDefault() { - if (typeof def === 'function') { - return def(); - } - return def; - } - - if (!params) { - return _getDefault(); - } - - if (!params.hasOwnProperty(key)) { - return _getDefault(); - } - - return params[key]; -} - -edges.util.getUrlParams = function() { - var params = {}; - var url = window.location.href; - var fragment = false; - - // break the anchor off the url - if (url.indexOf("#") > -1) { - fragment = url.slice(url.indexOf('#')); - url = url.substring(0, url.indexOf('#')); - } - - // extract and split the query args - var args = url.slice(url.indexOf('?') + 1).split('&'); - - for (var i = 0; i < args.length; i++) { - var kv = args[i].split('='); - if (kv.length === 2) { - var key = kv[0].replace(/\+/g, "%20"); - key = decodeURIComponent(key); - var val = kv[1].replace(/\+/g, "%20"); - val = decodeURIComponent(val); - if (val[0] === "[" || val[0] === "{") { - // if it looks like a JSON object in string form... - // remove " (double quotes) at beginning and end of string to make it a valid - // representation of a JSON object, or the parser will complain - val = val.replace(/^"/,"").replace(/"$/,""); - val = JSON.parse(val); - } - params[key] = val; - } - } - - // record the fragment identifier if required - if (fragment) { - params['#'] = fragment; - } - - return params; -} - -edges.util.isEmptyObject = function(obj) { - for(var key in obj) { - if(obj.hasOwnProperty(key)) - return false; - } - return true; -} +edges.util.getParam = function (params, key, def) { + function _getDefault() { + if (typeof def === "function") { + return def(); + } + return def; + } + + if (!params) { + return _getDefault(); + } + + if (!params.hasOwnProperty(key)) { + return _getDefault(); + } + + return params[key]; +}; + +edges.util.getUrlParams = function () { + var params = {}; + var url = window.location.href; + var fragment = false; + + // break the anchor off the url + if (url.indexOf("#") > -1) { + fragment = url.slice(url.indexOf("#")); + url = url.substring(0, url.indexOf("#")); + } + + // extract and split the query args + var args = url.slice(url.indexOf("?") + 1).split("&"); + + for (var i = 0; i < args.length; i++) { + var kv = args[i].split("="); + if (kv.length === 2) { + var key = kv[0].replace(/\+/g, "%20"); + key = decodeURIComponent(key); + var val = kv[1].replace(/\+/g, "%20"); + val = decodeURIComponent(val); + if (val[0] === "[" || val[0] === "{") { + // if it looks like a JSON object in string form... + // remove " (double quotes) at beginning and end of string to make it a valid + // representation of a JSON object, or the parser will complain + val = val.replace(/^"/, "").replace(/"$/, ""); + val = JSON.parse(val); + } + params[key] = val; + } + } + + // record the fragment identifier if required + if (fragment) { + params["#"] = fragment; + } + + return params; +}; + +edges.util.isEmptyObject = function (obj) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) return false; + } + return true; +}; ////////////////////////////////////////////////////////////////// // Closures for integrating the object with other modules @@ -1107,351 +985,378 @@ edges.util.isEmptyObject = function(obj) { // results in a call to // this.function({one: arg1, two: arg2}) // -edges.util.objClosure = function(obj, fn, args, context_params) { - return function() { - if (args) { - var params = {}; - for (var i = 0; i < args.length; i++) { - if (arguments.length > i) { - params[args[i]] = arguments[i]; - } - } - if (context_params) { - params = $.extend(params, context_params); - } - obj[fn](params); - } else { - var slice = Array.prototype.slice; - var theArgs = slice.apply(arguments); - if (context_params) { - theArgs.push(context_params); - } - obj[fn].apply(obj, theArgs); - } - } -} +edges.util.objClosure = function (obj, fn, args, context_params) { + return function () { + if (args) { + var params = {}; + for (var i = 0; i < args.length; i++) { + if (arguments.length > i) { + params[args[i]] = arguments[i]; + } + } + if (context_params) { + params = $.extend(params, context_params); + } + obj[fn](params); + } else { + var slice = Array.prototype.slice; + var theArgs = slice.apply(arguments); + if (context_params) { + theArgs.push(context_params); + } + obj[fn].apply(obj, theArgs); + } + }; +}; + +edges.util.eventClosure = function (obj, fn, conditional, preventDefault) { + if (preventDefault === undefined) { + preventDefault = true; + } + return function (event) { + if (conditional) { + if (!conditional(event)) { + return; + } + } + if (preventDefault) { + event.preventDefault(); + } + obj[fn](event.currentTarget, event); + }; +}; + +edges.util.delayer = function (fn, delay, timeout) { + let wait = null; + + return function (event) { + // var e = $.extend(true, { }, arguments[0]); + var throttler = function () { + wait = null; + fn(event); + }; -edges.util.eventClosure = function(obj, fn, conditional, preventDefault) { - if (preventDefault === undefined) { - preventDefault = true; - } - return function(event) { - if (conditional) { - if (!conditional(event)) { - return; - } - } - if (preventDefault) { - event.preventDefault(); - } - obj[fn](event.currentTarget, event); + if (!timeout) { + clearTimeout(wait); } -} - -edges.util.delayer = function(fn, delay, timeout) { - let wait = null; - - return function(event) { - // var e = $.extend(true, { }, arguments[0]); - var throttler = function() { - wait = null; - fn(event); - }; - - if (!timeout) { clearTimeout(wait); } - if (!timeout || !wait) { wait = setTimeout(throttler, delay); } + if (!timeout || !wait) { + wait = setTimeout(throttler, delay); } -} + }; +}; /////////////////////////////////////////////////// // Group of asynchronous operations edges.util.AsyncGroup = class { - constructor(params) { - this.list = edges.util.getParam(params, "list"); - this.successCallbackArgs = edges.util.getParam(params, "successCallbackArgs"); - this.errorCallbackArgs = edges.util.getParam(params, "errorCallbackArgs"); - - this.functions = { - action: edges.util.getParam(params, "action"), - success: edges.util.getParam(params, "success"), - carryOn: edges.util.getParam(params, "carryOn"), - error: edges.util.getParam(params, "error") - }; - - this.checkList = []; - - this.finished = false; - - for (let i = 0; i < this.list.length; i++) { - this.checkList.push(0); - } - } - - process(params) { - if (this.list.length === 0) { - this.functions.carryOn(); - } - - for (let i = 0; i < this.list.length; i++) { - let context = {index: i}; - - let success_callback = edges.util.objClosure(this, "_actionSuccess", this.successCallbackArgs, context); - let error_callback = edges.util.objClosure(this, "_actionError", this.successCallbackArgs, context); - let complete_callback = false; - - this.functions.action({entry: this.list[i], - success_callback: success_callback, - error_callback: error_callback, - complete_callback: complete_callback - }); - } + constructor(params) { + this.list = edges.util.getParam(params, "list"); + this.successCallbackArgs = edges.util.getParam( + params, + "successCallbackArgs" + ); + this.errorCallbackArgs = edges.util.getParam(params, "errorCallbackArgs"); + + this.functions = { + action: edges.util.getParam(params, "action"), + success: edges.util.getParam(params, "success"), + carryOn: edges.util.getParam(params, "carryOn"), + error: edges.util.getParam(params, "error"), }; - _actionSuccess(params) { - let index = params.index; - delete params.index; - - params["entry"] = this.list[index]; - this.functions.success(params); - this.checkList[index] = 1; - - if (this._isComplete()) { - this._finalise(); - } - }; + this.checkList = []; - _actionError(params) { - let index = params.index; - delete params.index; + this.finished = false; - params["entry"] = this.list[index]; - this.functions.error(params); - this.checkList[index] = -1; + for (let i = 0; i < this.list.length; i++) { + this.checkList.push(0); + } + } - if (this._isComplete()) { - this._finalise(); - } - }; + process(params) { + if (this.list.length === 0) { + this.functions.carryOn(); + } - _actionComplete(params) {}; + for (let i = 0; i < this.list.length; i++) { + let context = { index: i }; - _isComplete() { - return $.inArray(0, this.checkList) === -1; - }; + let success_callback = edges.util.objClosure( + this, + "_actionSuccess", + this.successCallbackArgs, + context + ); + let error_callback = edges.util.objClosure( + this, + "_actionError", + this.successCallbackArgs, + context + ); + let complete_callback = false; - _finalise = function() { - if (this.finished) { - return; - } - this.finished = true; - this.functions.carryOn(); - }; -} + this.functions.action({ + entry: this.list[i], + success_callback: success_callback, + error_callback: error_callback, + complete_callback: complete_callback, + }); + } + } -/////////////////////////////////////////////////// -// Style/CSS/HTML ID related functions + _actionSuccess(params) { + let index = params.index; + delete params.index; -edges.util.bem = function(block, element, modifier) { - let bemClass = block; - if (element) { - bemClass += "__" + element; - } - if (modifier) { - bemClass += "--" + modifier; - } - return bemClass; -} + params["entry"] = this.list[index]; + this.functions.success(params); + this.checkList[index] = 1; -edges.util.styleClasses = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - let cl = namespace; - if (field) { - cl += "_" + field - } - if (instance_name) { - let second = namespace + "_" + instance_name; - if (field) { - second += "_" + field; - } - cl += " " + second; + if (this._isComplete()) { + this._finalise(); } - return cl; -} + } -edges.util.jsClasses = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - let styles = edges.util.styleClasses(namespace, field, instance_name) - let jsClasses = ""; - let bits = styles.split(" ") - for (let i = 0; i < bits.length; i++) { - let bit = bits[i]; - jsClasses += " js-" + bit; - } - return jsClasses; -} + _actionError(params) { + let index = params.index; + delete params.index; -edges.util.allClasses = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - let styles = edges.util.styleClasses(namespace, field, instance_name); - let js = edges.util.jsClasses(namespace, field, instance_name); - return styles + " " + js; -} + params["entry"] = this.list[index]; + this.functions.error(params); + this.checkList[index] = -1; -edges.util.jsClassSelector = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - let sel = ".js-" + namespace; - if (instance_name) { - sel += "_" + instance_name; - } - if (field) { - sel += "_" + field; + if (this._isComplete()) { + this._finalise(); } - return sel; -} + } -edges.util.htmlID = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - let id = namespace; - if (instance_name) { - id += "_" + instance_name; - } - if (field) { - id += "_" + field; - } - return id; -} + _actionComplete(params) {} -edges.util.idSelector = function(namespace, field, instance_name) { - instance_name = edges.util._normaliseInstanceName(instance_name); - return "#" + edges.util.htmlID(namespace, field, instance_name); -} + _isComplete() { + return $.inArray(0, this.checkList) === -1; + } -edges.util._normaliseInstanceName = function(instance_name) { - if (typeof instance_name === "string") { - return instance_name; + _finalise = function () { + if (this.finished) { + return; } + this.finished = true; + this.functions.carryOn(); + }; +}; - if (instance_name instanceof edges.Component) { - return instance_name.id; - } +/////////////////////////////////////////////////// +// Style/CSS/HTML ID related functions - if (instance_name instanceof edges.Renderer) { - return instance_name.component.id; - } -} +edges.util.bem = function (block, element, modifier) { + let bemClass = block; + if (element) { + bemClass += "__" + element; + } + if (modifier) { + bemClass += "--" + modifier; + } + return bemClass; +}; + +edges.util.styleClasses = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + let cl = namespace; + if (field) { + cl += "_" + field; + } + if (instance_name) { + let second = namespace + "_" + instance_name; + if (field) { + second += "_" + field; + } + cl += " " + second; + } + return cl; +}; + +edges.util.jsClasses = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + let styles = edges.util.styleClasses(namespace, field, instance_name); + let jsClasses = ""; + let bits = styles.split(" "); + for (let i = 0; i < bits.length; i++) { + let bit = bits[i]; + jsClasses += " js-" + bit; + } + return jsClasses; +}; + +edges.util.allClasses = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + let styles = edges.util.styleClasses(namespace, field, instance_name); + let js = edges.util.jsClasses(namespace, field, instance_name); + return styles + " " + js; +}; + +edges.util.jsClassSelector = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + let sel = ".js-" + namespace; + if (instance_name) { + sel += "_" + instance_name; + } + if (field) { + sel += "_" + field; + } + return sel; +}; + +edges.util.htmlID = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + let id = namespace; + if (instance_name) { + id += "_" + instance_name; + } + if (field) { + id += "_" + field; + } + return id; +}; + +edges.util.idSelector = function (namespace, field, instance_name) { + instance_name = edges.util._normaliseInstanceName(instance_name); + return "#" + edges.util.htmlID(namespace, field, instance_name); +}; + +edges.util._normaliseInstanceName = function (instance_name) { + if (typeof instance_name === "string") { + return instance_name; + } + + if (instance_name instanceof edges.Component) { + return instance_name.id; + } + + if (instance_name instanceof edges.Renderer) { + return instance_name.component.id; + } +}; //////////////////////////////////////////////////// // content wrangling -edges.util.escapeHtml = function(unsafe, def) { - if (def === undefined) { - def = ""; - } - if (unsafe === undefined || unsafe == null) { - return def; - } - try { - if (typeof unsafe.replace !== "function") { - return unsafe - } - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } catch(err) { - return def; +edges.util.escapeHtml = function (unsafe, def) { + if (def === undefined) { + def = ""; + } + if (unsafe === undefined || unsafe == null) { + return def; + } + try { + if (typeof unsafe.replace !== "function") { + return unsafe; + } + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } catch (err) { + return def; + } +}; + +edges.util.toHtmlEntities = function (str) { + return str.replace(/./gm, function (s) { + // return "&#" + s.charCodeAt(0) + ";"; + return s.match(/[a-z0-9\s]+/i) ? s : "&#" + s.charCodeAt(0) + ";"; + }); +}; + +edges.util.fromHtmlEntities = function (string) { + return (string + "").replace(/&#\d+;/gm, function (s) { + return String.fromCharCode(s.match(/\d+/gm)[0]); + }); +}; + +edges.util.safeId = function (unsafe) { + return unsafe + .replace(/&/g, "_") + .replace(//g, "_") + .replace(/"/g, "_") + .replace(/'/g, "_") + .replace(/\./gi, "_") + .replace(/\:/gi, "_") + .replace(/\s/gi, "_"); +}; + +edges.util.numFormat = function (params) { + var reflectNonNumbers = edges.util.getParam( + params, + "reflectNonNumbers", + false + ); + var prefix = edges.util.getParam(params, "prefix", ""); + var zeroPadding = edges.util.getParam(params, "zeroPadding", false); + var decimalPlaces = edges.util.getParam(params, "decimalPlaces", false); + var thousandsSeparator = edges.util.getParam( + params, + "thousandsSeparator", + false + ); + var decimalSeparator = edges.util.getParam(params, "decimalSeparator", "."); + var suffix = edges.util.getParam(params, "suffix", ""); + + return function (number) { + // ensure this is really a number + var num = parseFloat(number); + if (isNaN(num)) { + if (reflectNonNumbers) { + return number; + } else { + return num; + } + } + + // first off we need to convert the number to a string, which we can do directly, or using toFixed if that + // is suitable here + if (decimalPlaces !== false) { + num = num.toFixed(decimalPlaces); + } else { + num = num.toString(); } -} -edges.util.toHtmlEntities = function(str) { - return str.replace(/./gm, function(s) { - // return "&#" + s.charCodeAt(0) + ";"; - return (s.match(/[a-z0-9\s]+/i)) ? s : "&#" + s.charCodeAt(0) + ";"; - }); -} - -edges.util.fromHtmlEntities = function(string) { - return (string+"").replace(/&#\d+;/gm,function(s) { - return String.fromCharCode(s.match(/\d+/gm)[0]); - }) -} - -edges.util.safeId = function(unsafe) { - return unsafe.replace(/&/g, "_") - .replace(//g, "_") - .replace(/"/g, "_") - .replace(/'/g, "_") - .replace(/\./gi,'_') - .replace(/\:/gi,'_') - .replace(/\s/gi,"_"); -} - -edges.util.numFormat = function(params) { - var reflectNonNumbers = edges.util.getParam(params, "reflectNonNumbers", false); - var prefix = edges.util.getParam(params, "prefix", ""); - var zeroPadding = edges.util.getParam(params, "zeroPadding", false); - var decimalPlaces = edges.util.getParam(params, "decimalPlaces", false); - var thousandsSeparator = edges.util.getParam(params, "thousandsSeparator", false); - var decimalSeparator = edges.util.getParam(params, "decimalSeparator", "."); - var suffix = edges.util.getParam(params, "suffix", ""); - - return function(number) { - // ensure this is really a number - var num = parseFloat(number); - if (isNaN(num)) { - if (reflectNonNumbers) { - return number; - } else { - return num; - } - } - - // first off we need to convert the number to a string, which we can do directly, or using toFixed if that - // is suitable here - if (decimalPlaces !== false) { - num = num.toFixed(decimalPlaces); - } else { - num = num.toString(); - } + // now "num" is a string containing the formatted number that we can work on - // now "num" is a string containing the formatted number that we can work on + var bits = num.split("."); - var bits = num.split("."); - - if (zeroPadding !== false) { - var zeros = zeroPadding - bits[0].length; - var pad = ""; - for (var i = 0; i < zeros; i++) { - pad += "0"; - } - bits[0] = pad + bits[0]; - } + if (zeroPadding !== false) { + var zeros = zeroPadding - bits[0].length; + var pad = ""; + for (var i = 0; i < zeros; i++) { + pad += "0"; + } + bits[0] = pad + bits[0]; + } - if (thousandsSeparator !== false) { - bits[0] = bits[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator); - } + if (thousandsSeparator !== false) { + bits[0] = bits[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator); + } - if (bits.length === 1) { - return prefix + bits[0] + suffix; - } else { - return prefix + bits[0] + decimalSeparator + bits[1] + suffix; - } + if (bits.length === 1) { + return prefix + bits[0] + suffix; + } else { + return prefix + bits[0] + decimalSeparator + bits[1] + suffix; } -} + }; +}; -edges.util.numParse = function(params) { - var commaRx = new RegExp(",", "g"); +edges.util.numParse = function (params) { + var commaRx = new RegExp(",", "g"); - return function(num) { - num = num.trim(); - num = num.replace(commaRx, ""); - if (num === "") { - return 0.0; - } - return parseFloat(num); + return function (num) { + num = num.trim(); + num = num.replace(commaRx, ""); + if (num === "") { + return 0.0; } -} \ No newline at end of file + return parseFloat(num); + }; +}; diff --git a/src/renderers/bs3/RefiningANDTermSelector.js b/src/renderers/bs3/RefiningANDTermSelector.js index 9bd2249..8db7fd7 100644 --- a/src/renderers/bs3/RefiningANDTermSelector.js +++ b/src/renderers/bs3/RefiningANDTermSelector.js @@ -85,7 +85,9 @@ edges.renderers.bs3.RefiningANDTermSelector = class extends edges.Renderer { // get the terms of the filters that have already been set var filterTerms = []; for (var i = 0; i < ts.filters.length; i++) { - filterTerms.push(ts.filters[i].term.toString()); + if(ts.filters[i].term){ + filterTerms.push(ts.filters[i].term.toString()); + } } // render each value, if it is not also a filter that has been set