define(["jquery", "underscore", "backbone", "models/filters/Filter"], function (
$,
_,
Backbone,
Filter,
) {
/**
* @class DateFilter
* @classdesc A search filter whose search term is an exact date or date range
* @classcategory Models/Filters
* @constructs DateFilter
* @extends Filter
*/
var DateFilter = Filter.extend(
/** @lends DateFilter.prototype */ {
type: "DateFilter",
/**
* The Backbone Model attributes set on this DateFilter
* @type {object}
* @extends Filter#defaultts
* @property {Date} min - The minimum Date to use in the query for this filter
* @property {Date} max - The maximum Date to use in the query for this filter
* @property {Date} rangeMin - The earliest possible Date that 'min' can be
* @property {Date} rangeMax - The latest possible Date that 'max' can be
* @property {Boolean} matchSubstring - Will always be stet to false, since Dates don't have substrings
* @property {string} nodeName - The XML node name to use when serializing this model into XML
*/
defaults: function () {
return _.extend(Filter.prototype.defaults(), {
min: 0,
max: new Date().getUTCFullYear(),
rangeMin: 1800,
rangeMax: new Date().getUTCFullYear(),
matchSubstring: false,
nodeName: "dateFilter",
});
},
/**
* Parses the dateFilter XML node into JSON
*
* @param {Element} xml - The XML Element that contains all the DateFilter elements
* @return {JSON} - The JSON object literal to be set on the model
*/
parse: function (xml) {
try {
var modelJSON = Filter.prototype.parse.call(this, xml);
//Get the rangeMin and rangeMax nodes
var rangeMinNode = $(xml).find("rangeMin"),
rangeMaxNode = $(xml).find("rangeMax");
//Parse the range min
if (rangeMinNode.length) {
modelJSON.rangeMin = new Date(
rangeMinNode[0].textContent,
).getUTCFullYear();
}
//Parse the range max
if (rangeMaxNode.length) {
modelJSON.rangeMax = new Date(
rangeMaxNode[0].textContent,
).getUTCFullYear();
}
//If this Filter is in a filter group, don't parse the values
if (!this.get("isUIFilterType")) {
//Get the min, max, and value nodes
var minNode = $(xml).find("min"),
maxNode = $(xml).find("max"),
valueNode = $(xml).find("value");
//Parse the min value
if (minNode.length) {
modelJSON.min = new Date(minNode[0].textContent).getUTCFullYear();
}
//Parse the max value
if (maxNode.length) {
modelJSON.max = new Date(maxNode[0].textContent).getUTCFullYear();
}
//Parse the value
if (valueNode.length) {
modelJSON.values = [
new Date(valueNode[0].textContent).getUTCFullYear(),
];
}
}
//If a range min and max was given, or if a min and max value was given,
// then this DateFilter should be presented as a date range (rather than
// an exact date value).
if (
rangeMinNode.length ||
rangeMinNode.length ||
minNode ||
maxNode
) {
//Set the range attribute on the JSON
modelJSON.range = true;
} else {
//Set the range attribute on the JSON
modelJSON.range = false;
}
} catch (e) {
//If an error occured while parsing the XML, return a blank JS object
//(i.e. this model will just have the default values).
return {};
}
return modelJSON;
},
/**
* Builds a query string that represents this filter.
*
* @return {string} The query string to send to Solr
*/
getQuery: function () {
//Start the query string
var queryString = "";
//Only construct the query if the min or max is different than the default
if (
(this.get("min") != this.defaults().min &&
this.get("min") != this.get("rangeMin")) ||
(this.get("max") != this.defaults().max &&
this.get("max") != this.get("rangeMax"))
) {
//Iterate over each filter field and add to the query string
_.each(
this.get("fields"),
function (field, i, allFields) {
//Add the date range for this field to the query string
queryString +=
field + ":" + this.getRangeQuery().replace(/ /g, "%20");
//If there is another field, add an operator
if (allFields[i + 1]) {
queryString += "%20" + this.get("fieldsOperator") + "%20";
}
},
this,
);
//If there is more than one field, wrap the query in parenthesis
if (this.get("fields").length > 1) {
queryString = "(" + queryString + ")";
}
}
return queryString;
},
/**
* Constructs a subquery string from the minimum and maximum dates.
* @return {string} - THe subquery string
*/
getRangeQuery: function () {
//Get the minimum and maximum values
var max = this.get("max"),
min = this.get("min");
//If no min or max was set, but there is a value, construct an exact value match query
if (
!min &&
min !== 0 &&
!max &&
max !== 0 &&
(this.get("values")[0] || this.get("values")[0] === 0)
) {
return this.get("values")[0];
}
//If there is no min or max or value, set an empty query string
else if (
!min &&
min !== 0 &&
!max &&
max !== 0 &&
!this.get("values")[0] &&
this.get("values")[0] !== 0
) {
return "";
}
//If there is at least a min or max
else {
//If there's a min but no max, set the max to a wildcard (unbounded)
if ((min || min === 0) && !max) {
max = "*";
}
//If there's a max but no min, set the min to a wildcard (unbounded)
else if (!min && min !== 0 && max) {
min = "*";
}
//If the max is higher than the min, set the max to a wildcard (unbounded)
else if ((max || max === 0) && (min || min === 0) && max < min) {
max = "*";
}
if (min != "*") {
min = min + "-01-01T00:00:00Z";
}
if (max != "*") {
max = max + "-12-31T23:59:59Z";
}
//Add the range for this field to the query string
return "[" + min + "%20TO%20" + max + "]";
}
},
/**
* Updates the XML DOM with the new values from the model
*
* @inheritdoc
*/
updateDOM: function (options) {
var objectDOM = Filter.prototype.updateDOM.call(this, options);
//Date Filters don't use matchSubstring nodes, and the value node will be recreated later
$(objectDOM).children("matchSubstring, value").remove();
//Get a clone of the original DOM
var originalDOM;
if (this.get("objectDOM")) {
originalDOM = this.get("objectDOM").cloneNode(true);
}
if (typeof options == "undefined") {
var options = {};
}
// Get min and max dates
var dateData = {
min: this.get("min"),
max: this.get("max"),
value: this.get("values") ? this.get("values")[0] : null,
};
var isRange = false;
// Make subnodes <min> and <max> and append to DOM
_.map(
dateData,
function (value, nodeName) {
// dateFilters don't have a min or max when the values should range from
// a min to infinity, or from a max to infinity (e.g. "date is before...")
if (!value) {
return;
}
if (nodeName == "min") {
var dateTime = "-01-01T00:00:00Z";
} else {
var dateTime = "-12-31T23:59:59Z";
}
//If this value is the same as the default value, but it wasn't previously serialized,
if (
value == this.defaults()[nodeName] &&
(!$(originalDOM).children(nodeName).length ||
$(originalDOM).children(nodeName).text() != value + dateTime)
) {
return;
}
//Create an XML node
var nodeSerialized =
objectDOM.ownerDocument.createElement(nodeName);
//Add the date string to the XML node
$(nodeSerialized).text(value + dateTime);
$(objectDOM).append(nodeSerialized);
//If either a min or max was serialized, then mark this as a range
isRange = true;
},
this,
);
//If a value is set on this model,
if (!isRange && this.get("values").length) {
//Create a value XML node
var valueNode = $(objectDOM.ownerDocument.createElement("value"));
//Get a Date object for this value
var date = new Date();
date.setUTCFullYear(this.get("values")[0] + "-12-31T23:59:59Z");
//Set the text of the XML node to the date string
valueNode.text(date.toISOString());
$(objectDOM).append(valueNode);
}
if (this.get("isUIFilterType")) {
// Get new date data
var dateData = {
rangeMin: this.get("rangeMin"),
rangeMax: this.get("rangeMax"),
};
// Make subnodes <min> and <max> and append to DOM
_.map(
dateData,
function (value, nodeName) {
if (nodeName == "rangeMin") {
var dateTime = "-01-01T00:00:00Z";
} else {
var dateTime = "-12-31T23:59:59Z";
}
//If this value is the same as the default value, but it wasn't previously serialized,
if (
value == this.defaults()[nodeName] &&
(!$(originalDOM).children(nodeName).length ||
$(originalDOM).children(nodeName).text() != value + dateTime)
) {
return;
}
//Create an XML node
var nodeSerialized =
objectDOM.ownerDocument.createElement(nodeName);
//Add the date string to the XML node
$(nodeSerialized).text(value + dateTime);
//Remove existing nodes and add the new one
$(objectDOM).children(nodeName).remove();
$(objectDOM).append(nodeSerialized);
},
this,
);
//Move the filterOptions node to the end of the filter node
var filterOptionsNode = $(objectDOM).find("filterOptions");
filterOptionsNode.detach();
$(objectDOM).append(filterOptionsNode);
}
//Remove filterOptions for Date filters in collection definitions
else {
$(objectDOM).find("filterOptions").remove();
}
return objectDOM;
},
/**
* Creates a human-readable string that represents the value set on this model
* @return {string}
*/
getReadableValue: function () {
var readableValue = "";
var min = this.get("min"),
max = this.get("max"),
value = this.get("values")[0];
if (!value && value !== 0) {
//If there is a min and max
if ((min || min === 0) && (max || max === 0)) {
readableValue = min + " to " + max;
}
//If there is only a max
else if (max || max === 0) {
readableValue = "Before " + max;
} else {
readableValue = "After " + min;
}
} else {
readableValue = value;
}
return readableValue;
},
/**
* @inheritdoc
*/
hasChangedValues: function () {
return (
(this.get("min") > this.get("rangeMin") &&
this.get("min") !== this.defaults().min) ||
(this.get("max") < this.get("rangeMax") &&
this.get("max") !== this.defaults().max)
);
},
/**
* Checks if the values set on this model are valid and expected
* @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
*/
validate: function () {
//Validate most of the DateFilter attributes using the parent validate function
var errors = Filter.prototype.validate.call(this);
//If everything is valid so far, then we have to create a new object to store errors
if (typeof errors != "object") {
errors = {};
}
//Delete error messages for the attributes that are going to be validated specially for the DateFilter
delete errors.values;
delete errors.min;
delete errors.max;
// Check that there is a rangeMin and a rangeMax. If there isn't, then just set to
// the default rather than creating an error.
if (!this.get("rangeMin") && this.get("rangeMin") !== 0) {
this.set("rangeMin", this.defaults().rangeMin);
}
if (!this.get("rangeMax") && this.get("rangeMax") !== 0) {
this.set("rangeMax", this.defaults().rangeMax);
}
//Check that there aren't any negative numbers
if (this.get("min") < 0) {
errors.min = "The minimum year cannot be a negative number.";
}
if (this.get("max") < 0) {
errors.max = "The maximum year cannot be a negative number.";
}
if (this.get("rangeMin") < 0) {
errors.rangeMin =
"The range minimum year cannot be a negative number.";
}
if (this.get("rangeMax") < 0) {
errors.rangeMax =
"The range maximum year cannot be a negative number.";
}
//Check that the min and max values are in order, if the minimum is not the default value of 0
if (this.get("min") > this.get("max") && this.get("min") != 0) {
errors.min =
"The minimum year is after the maximum year. The minimum year must be a year before the maximum year of " +
this.get("max");
}
//Check that all the values are numbers
if (!errors.min && typeof this.get("min") != "number") {
errors.min = "The minimum year must be a number.";
}
if (!errors.max && typeof this.get("max") != "number") {
errors.max = "The maximum year must be a number.";
}
if (!errors.rangeMax && typeof this.get("rangeMax") != "number") {
errors.rangeMax =
"The maximum year in the date slider must be a number.";
}
if (!errors.rangeMin && typeof this.get("rangeMin") != "number") {
errors.rangeMin =
"The minimum year in the date slider must be a number.";
}
if (Object.keys(errors).length) return errors;
else {
return;
}
},
},
);
return DateFilter;
});