Server/js/SearchController.js

/*
See LICENSE.txt for this sample’s licensing information.
 
Abstract:
This class handles presenting the Search template example.
*/
 
class SearchController extends DocumentController {
 
    setupDocument(document) {
        super.setupDocument(document);
 
        // Obtain references to some useful elements in the searchTemplate
        const searchTemplateElem = document.getElementsByTagName('searchTemplate').item(0);
        const searchFieldElem = document.getElementsByTagName('searchField').item(0);
        const separatorElem = document.getElementsByTagName('separator').item(0);
        const messageElem = document.getElementById("message");
        const suggestionsElem = document.getElementById("suggestions");
        const defaultResultsElem = document.getElementById("defaultResults");
        const resultsModeElem = document.getElementById("resultsMode");
        const resultsListElem = document.getElementById("resultsList");
        const resultsShelfContainerElem = document.getElementById("resultsShelfContainer");
        const resultsGridContainerElem = document.getElementById("resultsGridContainer");
        const resultsSectionElem = document.getElementById("resultsSection");
 
        // Clean up the document for initial presentation
        let resultsContainerElem = resultsShelfContainerElem;
        resultsListElem.removeChild(resultsGridContainerElem);
        toggleDefaultResults(true);
 
        // Define some search-related constants
        const searchBaseURL = "https://itunes.apple.com/search?";
        const searchResultsLimit = 25;
        const searchMediaType = "movie";
        const searchMediaArtworkWidth = 250;
        const searchMediaArtworkAspectRatio = 1.5;
        const searchMediaTitleFunc = function(item) {
            return item.trackCensoredName || item.trackName ||
                   item.collectionCensoredName || item.collectionName;
        };
        const searchMediaItemIDFunc = function(item) {
            return item.trackId || item.collectionId;
        };
        const searchMediaArtworkURLFunc = function(item) {
            let result = item.artworkUrl60;
            if (typeof result === "string") {
                // WORKAROUND: Obtain high-resolution artwork
                result = result.replace("/60x60", "/600x600");
            }
            return result;
        };
 
        // Retain the XHR between searches so it can be cancelled mid-flight
        let searchRequest;
        // Retain the query between searches to determine changes
        let searchTextCache;
        // Retain the result items to handle selection events
        const searchResultsCache = [];
 
        const searchKeyboard = searchFieldElem.getFeature("Keyboard");
        // Register an event handler for search input changes
        searchKeyboard.onTextChange = performSearchRequest;
 
        // Register an event handler for search result mode selection
        resultsModeElem.addEventListener("highlight", function(event) {
            const selectedElement = event.target;
            const selectedMode = selectedElement.getAttribute("value");
            setResultsMode(selectedMode);
        });
 
        // Register an event handler for search suggestions selection
        suggestionsElem.addEventListener("select", function(event) {
            const selectedElement = event.target;
            const searchValue = selectedElement.getAttribute("value");
            searchKeyboard.text = searchValue;
            performSearchRequest();
        });
 
        // Register an event handler for search result selection
        resultsSectionElem.addEventListener("select", function(event) {
            const selectedElement = event.target;
            const resultIndex = selectedElement.getAttribute("resultIndex");
            const resultItem = searchResultsCache[resultIndex];
            handleSelectionForItem(resultItem);
        });
 
        /*
         * Show or hide the message in the search body.
         * Sets the content of the message if it is to be shown.
         */
        function toggleSearchMessage(bool, message) {
            if (bool) {
                // Set the message text
                if (message) {
                    messageElem.textContent = message;
                }
                // Show the message if it's hidden
                if (!messageElem.parentNode) {
                    searchTemplateElem.appendChild(messageElem);
                }
                toggleModeButtons(false);
            } else {
                // Hide the message if it's visible
                if (messageElem.parentNode) {
                    searchTemplateElem.removeChild(messageElem);
                }
                toggleModeButtons(true);
            }
        }
 
        function toggleSearchSuggestions(bool) {
            if (bool) {
                // Show the suggestions if they're hidden
                if (!suggestionsElem.parentNode) {
                    searchTemplateElem.appendChild(suggestionsElem);
                }
                toggleSearchMessage(false);
                toggleModeButtons(false);
            } else {
                // Hide the suggestions if they're visible
                if (suggestionsElem.parentNode) {
                    searchTemplateElem.removeChild(suggestionsElem);
                }
                toggleModeButtons(true);
            }
        }
 
        function toggleDefaultResults(bool) {
            if (bool) {
                // Swap the default results in for the container
                if (resultsContainerElem.parentNode) {
                    resultsListElem.removeChild(resultsContainerElem);
                    resultsListElem.appendChild(defaultResultsElem);
                }
                toggleSearchMessage(false);
                toggleSearchSuggestions(false);
                toggleModeButtons(false);
            } else {
                // Swap the default results out and the container in
                if (!resultsContainerElem.parentNode) {
                    resultsListElem.removeChild(defaultResultsElem);
                    resultsListElem.appendChild(resultsContainerElem);
                }
                toggleModeButtons(true);
            }
        }
 
        function toggleModeButtons(bool) {
            if (bool) {
                if (!separatorElem.parentNode) {
                    searchTemplateElem.appendChild(separatorElem);
                }
            } else {
                if (separatorElem.parentNode) {
                    searchTemplateElem.removeChild(separatorElem);
                }
            }
        }
 
        function setResultsMode(mode) {
            // Remove existing results container
            while (resultsListElem.firstChild) {
                resultsListElem.removeChild(resultsListElem.firstChild);
            }
            // Determine the new results container element
            if (mode === "shelf") {
                resultsContainerElem = resultsShelfContainerElem;
            }
            if (mode === "grid") {
                resultsContainerElem = resultsGridContainerElem;
            }
            // Show the new results container in the collectionList
            resultsListElem.appendChild(resultsContainerElem);
            resultsContainerElem.appendChild(resultsSectionElem);
        }
 
        function performSearchRequest() {
            // Strip leading, trailing, and multiple whitespaces from the query
            const searchText = searchKeyboard.text.trim().replace(/\s+/g, " ");
 
            // Do nothing if the query hasn't meaningfully changed
            if (searchTextCache && searchText === searchTextCache) {
                return;
            }
            // Retain this query for the next time the input changes
            searchTextCache = searchText;
 
            // If there's already a search in-progress, cancel it.
            if (searchRequest && searchRequest.readyState !== XMLHttpRequest.DONE) {
                searchRequest.abort();
            }
 
            // Show the initial message and stop if there's no search query
            if (searchText.length === 0) {
                toggleDefaultResults(true);
                return;
            }
 
            // Build the URL for the search query
            const searchParams = [
                `media=${searchMediaType}`,
                `limit=${searchResultsLimit}`,
                `term=${encodeURIComponent(searchText)}`
            ].join("&");
            const searchURL = searchBaseURL + searchParams;
 
            // Perform the search request
            searchRequest = new XMLHttpRequest();
            searchRequest.open("GET", searchURL);
            searchRequest.responseType = "json";
            searchRequest.onload = showSearchResponse;
            searchRequest.onerror = showSearchError;
            searchRequest.send();
 
            searchFieldElem.setAttribute("showSpinner", true);
        }
 
        /*
         * Show a generic error message in the search body
         */
        function showSearchError() {
            toggleSearchMessage(true, "An error occurred during your search.");
            searchFieldElem.setAttribute("showSpinner", false);
        }
 
        /*
         * Parse the XHR response and show the results or a message
         */
        function showSearchResponse() {
            // Prepare the document for new search results
            toggleDefaultResults(false);
            toggleSearchSuggestions(false);
            clearSearchResults();
            searchFieldElem.setAttribute("showSpinner", false);
 
            // Show the results (or lack thereof)
            const searchResponse = searchRequest.response;
            const searchResults = searchResponse.results;
            if (searchResults.length > 0) {
                appendSearchResults(searchResults);
                toggleSearchMessage(false);
            } else {
                if (searchTextCache.length > 3) {
                    toggleSearchMessage(true, `No results for ${searchTextCache}.`);
                } else {
                    toggleSearchSuggestions(true);
                }
            }
        }
 
        /*
         * Empty the results cache and remove all results lockup elements.
         */
        function clearSearchResults() {
            searchResultsCache.length = 0;
            // Remove all existing search results
            while (resultsSectionElem.firstChild) {
                resultsSectionElem.removeChild(resultsSectionElem.firstChild);
            }
        }
 
        /*
         * Create lockup elements for the search results and cache
         * the data to be referenced by the selection handler.
         */
        function appendSearchResults(results) {
            const startIndex = searchResultsCache.length;
            // Create new lockups for the results
            results.forEach(function(item, index) {
                index += startIndex;
                // Create item lockup element
                const lockupElem = document.createElement("lockup");
                // Set the result array index on the lockup
                lockupElem.setAttribute("resultIndex", index);
                // Populate the lockup element details
                populateLockupWithItem(lockupElem, item);
                // Add the lockup to the results collection
                resultsSectionElem.appendChild(lockupElem);
                // Add the item to the search results cache
                searchResultsCache.push(item);
            });
        }
 
        /*
         * Inserts elements containing data from a search result into the
         * empty lockup element created to display the result.
         * Shows an image, title, and subtitle in the lockup.
         */
        function populateLockupWithItem(lockupElem, item) {
            // Determine the lockup image attributes
            const titleText = searchMediaTitleFunc(item);
            const itemID = searchMediaItemIDFunc(item);
            const imgURL = searchMediaArtworkURLFunc(item);
            const imgWidth = searchMediaArtworkWidth;
            const imgHeight = imgWidth * searchMediaArtworkAspectRatio;
            // Create the child nodes of the lockup element
            const imgElem = document.createElement("img");
            const titleElem = document.createElement("title");
            // Set the lockup image attributes
            imgElem.setAttribute("src", imgURL);
            imgElem.setAttribute("width", imgWidth);
            imgElem.setAttribute("height", imgHeight);
            // Set the lockup element text from the item
            titleElem.setAttribute("class", "showTextOnHighlight");
            titleElem.textContent = titleText;
            // Put the child nodes into the lockup
            lockupElem.setAttribute("itemID", itemID);
            lockupElem.appendChild(imgElem);
            lockupElem.appendChild(titleElem);
        }
 
        /*
         * Called when a search result is selected, passing in the
         * JSON Object that was returned by the API for the result.
         */
        function handleSelectionForItem(item) {
            console.log("handleSelectionForItem: " + JSON.stringify(item));
            // Do something more interesting than logging
        }
    }
 
}
 
registerAttributeName('searchDocumentURL', SearchController);