Source:search-container.component.js

(function () {
	'use strict';
	// Search Container
	angular
		.module('mohistory')
		.component('searchContainer', {
			templateUrl: 'app/components/search-container/search-container.component.html',
			controller: searchContainerCtrl,
			controllerAs: 'searchContainer',
			bindings: {
				searchResults: '<',
				context: '<',
				calendar: '<',
				setFooterHidden: '<',
			}
		});
	searchContainerCtrl.$inject = ['$transitions', '$scope', 'searchFacets', 'keywordSearcher', '$state'];
	/**
	 * Wrapper for the search interfaces, i.e. general, blog, event, and collections. Contains logic
	 * to handle search facets, display data in various views, and determine what to do with faulty
	 * feedback from the server.
	 * @memberof mohistory
	 * @name searchContainer
	 * @ngdoc component
	 * @param {object} $transitions$ UI-router object
	 * @param {object} searchFacets Service that handles manipulating parameters in the state.
	 * @param {object} keywordSearcher Service that wraps calls to the search APIs
	 */
	function searchContainerCtrl($transitions, $scope, searchFacets, keywordSearcher, $state) {
		var vm = this;
		/* ----- Variables ----- */
		/* --- Controls how each search interface behaves, e.g. number/type of layouts available. --- */
		vm.searchConfig = {
			'search-collections': {
				layouts: [
					'grid',
					'thumb',
					'detail',
					'list',
				],
				defaultLayout: 'detail',
				hasDisclaimer: true,
				disclaimerContent: '<p>Many research materials are only available in person. If you do not find what you are looking for online <a href="http://mohistory.org/research/in-person-research">contact the reading room staff</a>.</p>',
				zeroResultContent: 'If you cannot find what you are looking for online, please consider visiting the <a class="theme-text" href="http://mohistory.org/research/in-person-research">Library & Research Center</a> in person or contacting staff for assistance.',
				errorSearchResults: {
					facets: {
						department: [{
							count: '',
							email: 'archives@mohistory.org',
							label: 'Archival Collections',
							phone: '314-746-4510',
							url: 'http://mohistory.org/research/documents-archives/',
						}, {
							count: '',
							email: 'photo@mohistory.org',
							label: 'Photos and Prints Collections',
							phone: '314-746-4511',
							url: 'http://mohistory.org/research/photographs-prints/',
						}, {
							count: '',
							email: 'objects@mohistory.org',
							label: 'Object Collections',
							phone: '314-746-4441',
							url: 'http://mohistory.org/research/objects-artifacts',
						}, {
							count: '',
							email: 'library@mohistory.org',
							label: 'Library Collections',
							phone: '314-746-4500',
							url: 'http://mohistory.org/research/library/',
						}, {
							count: '',
							email: 'movingimages@mohistory.org',
							label: 'Moving Image and Sound Collections',
							phone: '314-746-4585',
							url: 'http://mohistory.org/research/multimedia/',
						}],
						collection: [],
						decade: {},
						creator: [],
						type: [],
						subject: [],
						place: [],
					},
					items: [],
					total: 0,
				},
				isHelpVisible: true,
			},
			'search-blog': {
				layouts: [
					'grid',
					'detail',
				],
				defaultLayout: 'grid',
				hasDisclaimer: false,
				disclaimerContent: '',
				zeroResultContent: 'Currently there are no blog posts that match what you are looking for. Please modify your search and try again.',
				errorSearchResults: {
					facets: {
						author: [],
						category: [],
						subject: [],
						exhibit: [],
					},
					items: [],
					total: 0,
				},
				isHelpVisible: false,
			},
			'search-events': {
				layouts: [
					'detail',
				],
				defaultLayout: 'detail',
				hasDisclaimer: false,
				disclaimerContent: '',
				zeroResultContent: 'Currently no events match what you are looking for. Please try a new search or check back later.',
				errorSearchResults: {
					facets: {
						exhibit: [],
						location: [],
						series: [],
						type: [],
					},
					items: [],
					total: 0,
				},
				isHelpVisible: false,
			},
			'search-all': {
				layouts: [
					'detail',
					'list',
				],
				defaultLayout: 'detail',
				hasDisclaimer: false,
				disclaimerContent: '',
				zeroResultContent: 'No search results match what you are looking for. Please modify your search and try again.',
				errorSearchResults: {
					facets: {
						subject: [],
						audience: [],
						type: [],
						category: [],
						location: [],
						author: [],
					},
					items: [],
					total: 0,
				},
				isHelpVisible: false,
			}
		};
		vm.curSearchConfig = {};
		/* --- Information for child components --- */
		vm.searchQuery = '';
		vm.curLayout = 'detail';
		vm.flattenedFacets = [];
		vm.isDisclaimerVisible = true;
		vm.isRedirection = false;
		vm.apiError = false;
		vm.images = false;
		/* --- Events --- */
		vm.today = null;
		vm.dateToDisplay = null;
		vm.eventIcons = [{
			label: "Free program",
			count: 0
		}, {
			label: "Registration required",
			count: 0
		}, {
			label: "Paid program",
			count: 0
		}, {
			label: "Program with member benefits",
			count: 0
		}, {
			label: "Accessible program",
			count: 0
		}];
		vm.eventIconsToShow = [];
		// Date
		vm.monthNumToName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
			'September', 'October', 'November', 'December'
		];
		vm.monthNameToNum = {
			'January': 0,
			'February': 1,
			'March': 2,
			'April': 3,
			'May': 4,
			'June': 5,
			'July': 6,
			'August': 7,
			'September': 8,
			'October': 9,
			'November': 10,
			'December': 11,
		};
		// Information used to populate the drop down menus
		vm.yearsToShow = [];
		// By storing an index we don't have to keep copying the vm.monthNumToName array
		vm.monthsToShowStart = null;
		vm.daysToShow = [];
		// Selected date in the drop down menu
		vm.selectedYear = 'year';
		vm.selectedMonth = 'month';
		vm.selectedDay = 'day';
		// Information used for calendar display
		vm.displayYear = null;
		vm.displayMonth = null;
		/* --- Pagination --- */
		vm.setPage = setPage;
		vm.nextPage = nextPage;
		vm.prevPage = prevPage;
		/* --- Infinite Scroll --- */
		vm.infinList = [];
		vm.infinListToShow = [];
		vm.infinListStep = 4;
		vm.ISBusy = false;
		vm.endOfList = false;
		vm.infinTotalResults = 0;
		// UI-Router hook triggered when a transition is started
		var uiOnStart = $transitions.onStart({}, function ($transition$) {
			var stateName = $transition$.to().name;
			var prevStateName = $transition$.from().name;
			// This transition hook is triggered when transitioning from a search state to another state, so
			// we need to verify that we are transition to a search state.
			if ((stateName === prevStateName) && (stateName === 'main.collections' || stateName === 'main.blog' || stateName === 'main.events' || stateName === 'main.search')) {
				if (vm.curLayout !== 'thumb') {
					vm.setFooterHidden(false); // Clean up if we're transitioning from thumb view
					return keywordSearcher.getResultsForPage(vm.context, $transition$.params()).then(function (data) {
						if (data === 'error') {
							// If there is an error use the fallback data so the UI doesn't break and show an error message
							vm.searchResults = vm.curSearchConfig.errorSearchResults;
							vm.apiError = true;
						} else {
							vm.searchResults = data;
							vm.apiError = false;
						}
					});
				} else {
					vm.setFooterHidden(true); // Transitioning to thumb view so hide footer, until infinite scroll is done
					vm.ISBusy = true;
					return keywordSearcher.getInfinList(vm.context, $transition$.params(), 0).then(function (data) {
						if (data === 'error') {
							// If there is an error use the fallback data so the UI doesn't break and show an error message
							vm.searchResults = vm.curSearchConfig.errorSearchResults;
							vm.infinList = vm.searchResults.items;
							vm.infinListToShow = vm.infinList;
							vm.apiError = true;
							vm.setFooterHidden(false);
						} else {
							vm.searchResults = data;
							vm.apiError = false;
							if (vm.searchResults.items.length === 0) {
								vm.endOfList = true;
								vm.setFooterHidden(false);
							} else {
								vm.endOfList = false;
								vm.infinList = vm.searchResults.items;
								vm.infinTotalResults = vm.searchResults.total;
								vm.infinListToShow = vm.infinList;
							}
						}
						vm.ISBusy = false;
					});
				}
			}
		});
		// UI-Router hook triggered when a transition succeeds
		var uiOnSuccess = $transitions.onSuccess({}, function ($transition$) {
			var stateName = $transition$.to().name;
			var prevStateName = $transition$.from().name;
			// This transition hook is triggered when transitioning from a search state to another state, so
			// we need to verify that we are transitioning to a search state.
			if ((stateName === prevStateName) && (stateName === 'main.collections' || stateName === 'main.blog' || stateName === 'main.events' || stateName === 'main.search')) {
				// Update variables based on any changes
				vm.setupData();
			}
		});
		/* ----- Function Bindings ----- */
		vm.$onInit = onInit;
		vm.$onDestroy = onDestroy;
		vm.setupData = setupData;
		// Submit keyword search
		vm.onSearchSubmit = onSearchSubmit;
		/* --- Manipulating the search facet list --- */
		vm.addDateFacet = addDateFacet;
		vm.addFacet = addFacet;
		vm.removeFacet = removeFacet;
		vm.clearFacets = clearFacets;
		vm.toggleFacet = toggleFacet;
		/* --- Manipulating information in facet panels --- */
		vm.isAFacetSelected = isAFacetSelected;
		vm.isFacetSelected = isFacetSelected;
		/* --- Routing to layout views --- */
		vm.setViewLayout = setViewLayout;
		/* --- Handle Disclaimer --- */
		vm.onDisclaimerClose = onDisclaimerClose;
		/* --- Events --- */
		vm.setupCalendar = setupCalendar;
		vm.setupEventIcons = setupEventIcons;
		vm.setYearsToShow = setYearsToShow;
		vm.setDaysToShow = setDaysToShow;
		vm.setMonthsToShowStart = setMonthsToShowStart;
		vm.verifyDate = verifyDate;
		vm.nextMonth = nextMonth;
		vm.prevMonth = prevMonth;
		vm.isMonthDisabled = isMonthDisabled;
		vm.isDateDisabled = isDateDisabled;
		/* --- Infinite Scroll --- */
		vm.getInfiniteNextPage = getInfiniteNextPage;
		/* ----- Function Definitions ----- */
		/**
		 * Initialization code run every time the component is created, used to setup variables
		 * and data.
		 * @function onInit
		 * @memberof searchContainer
		 */
		function onInit() {
			vm.curSearchConfig = vm.searchConfig[vm.context];
			if (vm.searchResults === 'error' || vm.searchResults === null || vm.searchResults === undefined) {
				// If error use the errorSearchResults for the given context
				vm.searchResults = vm.curSearchConfig.errorSearchResults;
				vm.apiError = true;
			} else {
				vm.apiError = false;
				vm.setupData();
			}
		}
		/**
		 * Clean up function called when a component is removed.
		 * @function onDestroy
		 * @memberof searchContainer
		 */
		function onDestroy() {
			// Deregister UI-Router transition hooks
			uiOnStart();
			uiOnSuccess();
			// Make sure the footer is visible
			vm.setFooterHidden(false);
		}
		/**
		 * Process data before displaying it. Some of the setup is based on search type,
		 * where as, there are pieces that apply to all types.
		 * @function setupData
		 * @memberof searchContainer
		 */
		function setupData() {
			// Retrieve information from state / URL query parameters
			vm.searchQuery = searchFacets.getSearchFacets('text');
			vm.flattenedFacets = searchFacets.getFlattenedFacets();
			vm.isRedirection = searchFacets.getSearchFacets('redirect');
			if (vm.context === 'search-collections') {
				if (vm.searchResults.facets.department.length === 0) {
					// if API does not return any department information, use hard coded values
					vm.searchResults.facets.department = vm.curSearchConfig.errorSearchResults.facets.department;
				}
				// Results with images checkbox
				vm.images = searchFacets.getSearchFacets('images');
			}
			if (vm.context === 'search-events' && vm.calendar !== null && vm.calendar !== undefined) {
				vm.setupCalendar();
				vm.setupEventIcons();
			}
			if (vm.isDisclaimerVisible && vm.curSearchConfig['hasDisclaimer']) {
				// Any of the searches can have a disclaimer
				vm.isDisclaimerVisible = true;
			} else {
				vm.isDisclaimerVisible = false;
			}
			var layoutInHash = searchFacets.getSearchFacets('layout');
			if (vm.curSearchConfig['layouts'].indexOf(layoutInHash) >= 0) {
				// Layout in hash must be valid for the given context
				vm.curLayout = layoutInHash;
			} else {
				vm.setViewLayout(vm.curSearchConfig['defaultLayout']);
			}
			if (vm.curLayout === 'thumb') {
				// Infinite scroll
				vm.infinList = vm.searchResults.items;
				vm.infinListToShow = vm.infinList;
				vm.infinTotalResults = vm.searchResults.total;
			}
			// Ensure the facet names are always in the same order and if the API doesn't return
			// a facet it still shows up for the user
			var apiSearchFacets = vm.searchResults.facets;
			vm.searchResults.facets = {};
			for (var facet in vm.curSearchConfig.errorSearchResults.facets) {
				if (vm.curSearchConfig.errorSearchResults.facets.hasOwnProperty(facet)) {
					if (apiSearchFacets[facet] !== undefined && apiSearchFacets[facet] !== null) {
						vm.searchResults.facets[facet] = apiSearchFacets[facet];
					} else {
						vm.searchResults.facets[facet] = vm.curSearchConfig.errorSearchResults.facets[facet];
					}
				}
			}
		}
		/**
		 * Unlike other search facets, all event icons need to be visible
		 * regardless of if they were returned as viable facets from the API.
		 * This function generates a list of icons and counts to display based
		 * on the hard coded list of all possible icons and the icons returned
		 * from the API.
		 * @function setupEventIcons
		 * @memberof searchContainer
		 */
		function setupEventIcons() {
			var iconsFromAPI = vm.searchResults.facets['icon'];
			var label = null;
			var count = null;
			vm.eventIconsToShow = [];
			// Loop through the hard coded event icons
			for (var i = 0; i < vm.eventIcons.length; i++) {
				label = vm.eventIcons[i].label;
				count = vm.eventIcons[i].count;
				if (vm.isFacetSelected('icon', '-' + label)) {
					label = '-' + label;
				}
				// Loop through the icons returned from the API
				// and update the count if necessary
				for (var j = 0; j < iconsFromAPI.length; j++) {
					if (label === iconsFromAPI[j].label) {
						count = iconsFromAPI[j].count;
					}
				}
				vm.eventIconsToShow.push({
					label: label,
					count: count,
				});
			}
			// Remove the icon property on the facets object to prevent
			// it showing up in the tabbed area where most facets are displayed
			delete vm.searchResults.facets['icon'];
		}
		/**
		 * Handle the setup for the event search calendar. This means setting the
		 * variables used to display the calendar and validating the date facet.
		 * @function setupCalendar
		 * @memberof searchContainer
		 */
		function setupCalendar() {
			if (vm.today === null) {
				// If first load, initialize date with server time.
				vm.today = new Date(vm.searchResults.serverDate);
			}
			var dateFacet = searchFacets.getSearchFacets('date')[0];
			if (/^\d{4}(-\d{2}(-\d{2}(T.*)?)?)?$/.test(dateFacet)) {
				// If date is a facet, then verify it and set local variables
				var dateArr = dateFacet.split('T')[0].split('-');
				vm.selectedYear = dateArr[0]; // A year must be selected in order to pass the above regex
				vm.selectedMonth = vm.monthNumToName[Number(dateArr[1]) - 1] || 'month'; // set month from facet or use default
				vm.selectedDay = dateArr[2] || 'day'; // set day from facet or use default
				// Verify entered date.
				var hasChanged = verifyDate();
				if (hasChanged) {
					// If the date changed, then update the information stored in state
					vm.addFacet('date');
				}
			} else {
				// No date selected, set everything to defaults
				vm.selectedYear = 'year';
				vm.selectedMonth = 'month';
				vm.selectedDay = 'day';
			}
			// Based on vm.selectedYear, vm.selectedMonth, and vm.selectedDay set
			// the values displayed in the dropdown selection menus
			setYearsToShow();
			setMonthsToShowStart();
			setDaysToShow();
			// Display values are used for the calendar
			vm.displayYear = vm.selectedYear === 'year' ? vm.today.getFullYear() : vm.selectedYear;
			vm.displayMonth = vm.selectedMonth === 'month' ? vm.monthNumToName[vm.today.getMonth()] : vm.selectedMonth;
			// vm.dateToDisplay is passed to search-and-facet so it can be displayed under the result count
			vm.dateToDisplay = null;
			if (vm.selectedYear !== 'year' && vm.selectedMonth !== 'month' && vm.selectedDay !== 'day') {
				vm.dateToDisplay = vm.selectedMonth + ' ' + vm.selectedDay + ', ' + vm.selectedYear;
			} else if (vm.selectedYear !== 'year' && vm.selectedMonth !== 'month' && vm.selectedDay === 'day') {
				vm.dateToDisplay = vm.selectedMonth + ' ' + vm.selectedYear;
			} else if (vm.selectedYear !== 'year' && vm.selectedMonth === 'month' && vm.selectedDay === 'day') {
				vm.dateToDisplay = vm.selectedYear;
			}
		}
		/** 
		 * Fill the array used to populate the year dropdown menu.
		 * @function setYearsToShow
		 * @memberof searchContainer
		 */
		function setYearsToShow() {
			if (vm.yearsToShow.length === 0) {
				vm.yearsToShow = Object.keys(vm.calendar);
			}
		}
		/** 
		 * Determine if the months in the dropdown should start with January or
		 * the current month.
		 * @function setMonthsToShowStart
		 * @memberof searchContainer
		 */
		function setMonthsToShowStart() {
			// If current year is selected, then the monthsToShow needs to be
			// adjusted so nothing before the current month is accessible.
			// Using double equals because getFullYear returns a number and
			// selectedYear is stored as a string
			if (vm.selectedYear == vm.today.getFullYear()) {
				vm.monthsToShowStart = vm.today.getMonth();
			} else {
				vm.monthsToShowStart = 0;
			}
		}
		/**
		 * Populate the daysToShow array which is used to generate the day selector
		 * and logic for determining the accuracy of a date facet.
		 * MUST BE CALLED AFTER updating selectedYear and selectedMonth
		 * @function setDaysToShow
		 * @memberof searchContainer
		 */
		function setDaysToShow() {
			vm.daysToShow = []; // Clear out any old data
			if (vm.selectedYear !== 'year' && vm.selectedMonth !== 'month') {
				var monthData = vm.calendar[vm.selectedYear][vm.selectedMonth];
				// Loop over the weeks and days stored in the calendar for the selected year and month
				for (var week = 0; week < monthData.length; week++) {
					for (var day = 0; day < monthData[week].length; day++) {
						if (monthData[week][day] !== '00') {
							// Skip all blank days
							if (vm.selectedYear == vm.today.getFullYear() && vm.selectedMonth === vm.monthNumToName[vm.monthsToShowStart]) {
								if (monthData[week][day] >= vm.today.getDate()) {
									// If traversing current year and month only add today and preceding days.
									vm.daysToShow.push(monthData[week][day]);
								}
							} else {
								// If not current year and month, add all days
								vm.daysToShow.push(monthData[week][day]);
							}
						}
					}
				}
			} else {
				// There are a max of 31 days in any month
				for (var i = 1; i <= 31; i++) {
					vm.daysToShow.push(('0' + i).slice(-2));
				}
			}
		}
		/**
		 * Verify selected date. A date is valid if the selected values are in 
		 * allowed ranges and the selected values can be combined to form one of
		 * the following date combinations: yyyy-mm-dd, yyyy-mm, or yyyy-mm-dd.
		 * @function verifyDate
		 * @memberof searchContainer
		 * @return {boolean} true if a change was made, false otherwise
		 */
		function verifyDate() {
			var hasChanged = false;
			if (vm.selectedYear !== 'year') {
				// A year has been selected
				vm.setYearsToShow();
				if (vm.yearsToShow[0] > vm.selectedYear) {
					// selected year is already past
					vm.selectedYear = vm.yearsToShow[0];
					hasChanged = true;
				} else if (vm.yearsToShow[vm.yearsToShow.length - 1] < vm.selectedYear) {
					// selected year is more than five years in the future
					vm.selectedYear = vm.yearsToShow[vm.yearsToShow.length - 1];
					hasChanged = true;
				}
			}
			if (vm.selectedMonth !== 'month') {
				// A month has been selected
				if (vm.selectedYear === 'year') {
					// A year must be set to alter the month
					vm.selectedYear = String(vm.today.getFullYear());
					vm.setYearsToShow();
					hasChanged = true;
				}
				vm.setMonthsToShowStart();
				if (vm.monthNameToNum[vm.selectedMonth] < vm.monthsToShowStart) {
					vm.selectedMonth = vm.monthNumToName[vm.monthsToShowStart];
					hasChanged = true;
				} else if (vm.monthNameToNum[vm.selectedMonth] > vm.monthNumToName.length - 1) {
					vm.selectedMonth = vm.monthNumToName[vm.monthNumToName.length - 1];
					hasChanged = true;
				}
			}
			if (vm.selectedDay !== 'day') {
				// A day has been selected
				if (vm.selectedYear === 'year') {
					// A year must be set to alter the day
					vm.selectedYear = String(vm.today.getFullYear());
					vm.setYearsToShow();
					vm.setMonthsToShowStart();
					hasChanged = true;
				}
				if (vm.selectedMonth === 'month') {
					// A month must be set to alter the day
					vm.selectedMonth = vm.monthNumToName[vm.today.getMonth()];
					hasChanged = true;
				}
				vm.setDaysToShow();
				if (vm.selectedDay < vm.daysToShow[0]) {
					vm.selectedDay = vm.daysToShow[0];
					hasChanged = true;
				} else if (vm.selectedDay > vm.daysToShow[vm.daysToShow.length - 1]) {
					vm.selectedDay = vm.daysToShow[vm.daysToShow.length - 1];
					hasChanged = true;
				}
			}
			return hasChanged;
		}
		/**
		 * Navigate to the next month. If current month is December move to January
		 * of the next year. This function has no impact on the selected date.
		 * @function nextMonth
		 * @memberof searchContainer
		 */
		function nextMonth() {
			if (vm.displayMonth === vm.monthNumToName[vm.monthNumToName.length - 1]) {
				// Moving from December of the current year to January of the next year
				vm.displayYear = '' + (Number(vm.displayYear) + 1); // convert to a number, then back to string
				vm.displayMonth = vm.monthNumToName[0];
			} else {
				var monthIdx = vm.monthNameToNum[vm.displayMonth];
				vm.displayMonth = vm.monthNumToName[monthIdx + 1];
			}
		}
		/** 
		 * Navigate to the previous month. If current month is January move to
		 * December of the previous year. This function has no impact on the
		 * selected date.
		 * @function prevMonth
		 * @memberof searchContainer
		 */
		function prevMonth() {
			if (vm.displayMonth === vm.monthNumToName[0]) {
				// Moving from January of current year to December of the previous year
				vm.displayYear = '' + (Number(vm.displayYear) - 1); // convert to a number, then back to string
				vm.displayMonth = vm.monthNumToName[vm.monthNumToName.length - 1];
			} else {
				var monthIdx = vm.monthNameToNum[vm.displayMonth];
				vm.displayMonth = vm.monthNumToName[monthIdx - 1];
			}
		}
		/**
		 * Determine if a navigational arrows should be disabled for a given direction.
		 * By disabling those buttons, we can prevent navigation to months outside the
		 * allowed range.
		 * @function isMonthDisabled
		 * @memberof searchContainer
		 * @param {string} direction the direction to check
		 * @return {boolean} True if the month is not in the allowed range, false otherwise
		 */
		function isMonthDisabled(direction) {
			var monthIdx = vm.monthNameToNum[vm.displayMonth];
			if (direction === 'PREV') {
				if (monthIdx === 0) {
					// Is the previous month in the allowed range
					var prevYear = Number(vm.displayYear) - 1;
					return vm.calendar[prevYear] === undefined;
				} else {
					// Previous month has already past
					var prevMonth = monthIdx - 1;
					return vm.displayYear == vm.today.getFullYear() && prevMonth < vm.today.getMonth();
				}
			} else if (direction === 'NEXT') {
				if (monthIdx === (vm.monthNumToName.length) - 1) {
					// Is the next year in the allowed range
					var nextYear = Number(vm.displayYear) + 1;
					return vm.calendar[nextYear] === undefined;
				} else {
					// Any month less than 12 is in the allowed range for moving forward
					return false;
				}
			}
		}
		/**
		 * Determine if a day is a valid choice for selection. By disabling calendar
		 * day that have already past, we can prevent users from selecting a past date.
		 * @function isDateDisabled
		 * @memberof searchContainer
		 * @param {string} day The day to check
		 * @return {boolean} True if the day has already past, false otherwise
		 */
		function isDateDisabled(day) {
			if (day === '00') {
				return true;
			}
			if (vm.displayYear == vm.today.getFullYear()) {
				if (vm.monthNameToNum[vm.displayMonth] <= vm.today.getMonth()) {
					return day < vm.today.getDate();
				}
			}
			return false;
		}
		/**
		 * Called when a new keyword is entered. Updates search text and page
		 * number in the state parameters. By updating the parameters, a transition
		 * is triggered which runs setupData.
		 * @memberof searchContainer
		 * @function onSearchSubmit
		 * @param {string} q String representing the search parameter
		 */
		function onSearchSubmit(q) {
			searchFacets.addFacetsForKeywordSearch(q);
		}
		/**
		 * Update the selected values of year, month, or day. The new value is
		 * verified, then the new facet is added to the state.
		 * @function addDateFacet
		 * @memberof searchContainer
		 * @param {string} datePart Piece of the date that should be changed 
		 * (i.e. year, month, or day)
		 * @param {string} value The new value for the provided datePart 
		 */
		function addDateFacet(datePart, value) {
			if (datePart === 'day') {
				vm.selectedDay = value;
			} else if (datePart === 'month') {
				vm.selectedMonth = value;
			} else if (datePart === 'year') {
				vm.selectedYear = value;
			}
			var hasChanged = verifyDate();
			vm.addFacet('date');
		}
		/**
		 * Adds a search facet (a.k.a. filter) to the state, which triggers a transition
		 * which in turn triggers the `onStart` listener. Inside the `onStart` transition
		 * listener, an API call is made with the updated state parameters.
		 * @function addFacet
		 * @memberof searchContainer
		 * @param {string} facetType Major category of facet, e.g. 'department' or 'collection'
		 * @param {string} currentFacet Specific facet to filter by, e.g. 'Archival Collections' 
		 */
		function addFacet(facetType, currentFacet) {
			if (facetType === 'date' && !currentFacet) {
				if (vm.selectedYear !== 'year' && vm.selectedMonth !== 'month' && vm.selectedDay !== 'day') {
					searchFacets.addFacetToHash('date', vm.selectedYear + '-' + ('0' + (vm.monthNameToNum[vm.selectedMonth] + 1)).slice(-2) + '-' + vm.selectedDay);
				} else if (vm.selectedYear !== 'year' && vm.selectedMonth === 'month' && vm.selectedDay === 'day') {
					searchFacets.addFacetToHash('date', vm.selectedYear);
				} else if (vm.selectedYear !== 'year' && vm.selectedMonth !== 'month' && vm.selectedDay === 'day') {
					searchFacets.addFacetToHash('date', vm.selectedYear + '-' + ('0' + (vm.monthNameToNum[vm.selectedMonth] + 1)).slice(-2));
				} else {
					searchFacets.removeFacetFromHash('date');
				}
			} else {
				searchFacets.addFacetToHash(facetType, currentFacet);
			}
		}
		/**
		 * Removes a search facet (a.k.a. filter) from the state, which triggers a transition
		 * which in turn triggers the `onStart` listener. Inside the `onStart` transition 
		 * listener, an API call is made with the updated state parameters.
		 * @memberof searchContainer
		 * @function removeFacet
		 * @param {string} facetType Major category of facet, e.g. 'department' or 'collection'
		 * @param {string} currentFacet Specific facet to filter by, e.g. 'Archival Collections' 
		 */
		function removeFacet(facetType, currentFacet) {
			searchFacets.removeFacetFromHash(facetType, currentFacet);
		}
		/**
		 * If the facet's first character is '-' remove it, else add a minus to the front 
		 * of the string. Then, remove the old facet and add the new one.
		 * @memberof searchContainer
		 * @function toggleFacet
		 * @param {string} facetType Major category of facet, e.g. 'dept' or 'collection'
		 * @param {string} currentFacet Specific facet to filter by, e.g. 'Archival Collections'
		 */
		function toggleFacet(facetType, currentFacet) {
			searchFacets.toggleFacetInHash(facetType, currentFacet);
		}
		/** 
		 * Remove all facets for the state and by consequence the URL hash.
		 * @function clearFacets
		 * @memberof searchContainer
		 */
		function clearFacets() {
			searchFacets.removeAllFacetsFromHash();
		}
		/**
		 * Returns true if the provided facet type has at least one selected facet in the
		 * url hash.
		 * @memberof searchContainer
		 * @function isAFacetSelected
		 * @param {string} facetType Major category of facet, e.g. 'dept' or 'collection'
		 * @return {boolean} true if there is at least one facet of the given type in the 
		 * url hash, false otherwise.
		 */
		function isAFacetSelected(facetType) {
			return searchFacets.isAFacetSelected(facetType);
		}
		/**
		 * Returns true if the provided facet is in the url hash.
		 * @memberof searchContainer
		 * @function isFacetSelected
		 * @param {string} facetType Major category of facet, e.g. 'dept' or 'collection'
		 * @param {string} currentFacet Specific facet to filter by, e.g. 'Archival Collections'
		 * @return {boolean} True if the given facet is in the url hash, false otherwise
		 */
		function isFacetSelected(facetType, currentFacet) {
			return searchFacets.isFacetSelected(facetType, currentFacet);
		}
		/**
		 * Switch to given view layout
		 * @memberof searchContainer
		 * @function setViewLayout
		 * @param {string} layout 
		 */
		function setViewLayout(layout) {
			vm.curLayout = layout;
			searchFacets.addFacetToHash('layout', layout);
		}
		/** 
		 * Hide the disclaimer while this component is alive. If the user
		 * navigates away from the search interface then comes back, the
		 * disclaimer will be visible again.
		 * @function onDisclaimerClose
		 * @memberof searchContainer
		 */
		function onDisclaimerClose() {
			vm.isDisclaimerVisible = false;
		}
		/**
		 * Navigate to the specified page by transitioning to a new state.
		 * @function setPage
		 * @memberof searchContainer
		 * @param {number} num 
		 */
		function setPage(num) {
			$state.go('.', {
				page: num
			});
		}
		/** 
		 * Navigate to the next page by transitioning to a new state.
		 * @function nextPage
		 * @memberof searchContainer
		 */
		function nextPage() {
			$state.go('.', {
				page: vm.searchResults.curPage + 1
			});
		}
		/** 
		 * Navigate tot the previous page by transitioning to a new state.
		 * @function nextPage
		 * @memberof searchContainer
		 */
		function prevPage() {
			$state.go('.', {
				page: vm.searchResults.curPage - 1
			});
		}
		/** 
		 * Manage infinite scrolling for the thumbnail view. Every API call retrieves 20 items. Every time
		 * this function is called it adds four elements to those shown. If there are no more elements
		 * in memory, then call the API again.
		 * @function getInfiniteNextPage
		 * @memberof searchContainer
		 */
		function getInfiniteNextPage() {
			if (vm.ISBusy) {
				return;
			}
			if (vm.endOfList) {
				vm.setFooterHidden(false);
				vm.ISBusy = false;
				return;
			}
			vm.ISBusy = true;
			vm.setFooterHidden(true); // Hide the footer, because it interferes with the infinite scroll process
			if ((vm.infinListToShow.length + vm.infinListStep) < vm.infinList.length || vm.infinList.length === vm.infinTotalResults) {
				// Add more elements from those stored in memory
				vm.infinListToShow = vm.infinListToShow.concat(vm.infinList.slice(vm.infinListToShow.length, vm.infinListToShow.length + vm.infinListStep));
				if (vm.infinList.length === vm.infinTotalResults && vm.infinList.length === vm.infinListToShow.length) {
					// All elements have been scrolled through, so show the footer
					vm.endOfList = true;
					vm.setFooterHidden(false);
				}
				vm.ISBusy = false;
			} else if (vm.infinList.length !== vm.infinTotalResults) {
				// Add more elements after querying the API for another batch 
				var diff = vm.infinList.length - vm.infinListToShow.length;
				if (diff > 0) {
					// Just in case the shown list isn't the same as the list stored in memory, add the last few elements
					vm.infinListToShow = vm.infinListToShow.concat(vm.infinList.slice(vm.infinListToShow.length));
				}
				keywordSearcher.getInfinList(vm.context, searchFacets.getSearchFacets(), vm.infinList.length - 1).then(function (response) {
					if (response.items.length === 0) {
						// API has confirmed the end of the list
						vm.endOfList = true;
						vm.setFooterHidden(false);
					} else {
						// Add items from the API to those stored in memory
						vm.infinList = vm.infinList.concat(response.items);
						if (typeof response.total !== 'number') {
							// Remove comma if the total is returned as a string
							response.total = Number(response.total.replace(/,/g, ""));
						}
						vm.infinTotalResults = response.total;
						vm.infinListToShow = vm.infinListToShow.concat(vm.infinList.slice(vm.infinListToShow.length, diff))
					}
					vm.ISBusy = false;
				});
			} else {
				// @note: not sure if this is necessary 
				// Fall back
				vm.endOfList = true;
				vm.setFooterHidden(false);
				vm.ISBusy = false;
			}
		}
	}
})();