├── .gitignore ├── css ├── autocomplete-styles.css ├── normalize.css └── style.css ├── img ├── ajax-loader.gif └── burst_tiny.png ├── index.html ├── js ├── app.js └── jquery.autocomplete.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | PracticeKnockoutProject -------------------------------------------------------------------------------- /css/autocomplete-styles.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; font-size: 14px; line-height: 1.6em; margin: 0; padding: 0; } 2 | .container { width: 800px; margin: 0 auto; } 3 | 4 | .autocomplete-suggestions { border: 1px solid #999; background: #FFF; cursor: default; overflow: auto; -webkit-box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); -moz-box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); } 5 | .autocomplete-suggestion { padding: 2px 5px; white-space: nowrap; overflow: hidden; } 6 | .autocomplete-no-suggestion { padding: 2px 5px;} 7 | .autocomplete-selected { background: #F0F0F0; } 8 | .autocomplete-suggestions strong { font-weight: bold; color: #000; } 9 | .autocomplete-group { padding: 2px 5px; } 10 | .autocomplete-group strong { font-weight: bold; font-size: 16px; color: #000; display: block; border-bottom: 1px solid #000; } 11 | 12 | -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v1.0.0 | MIT License | git.io/normalize */ 2 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.75em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:1em 40px}dfn{font-style:italic}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:75%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0} -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | html, body, #map-canvas { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | #infowindow { 8 | height: 200px; 9 | } 10 | 11 | ::-webkit-scrollbar { 12 | -webkit-appearance: none; 13 | width: 7px; 14 | } 15 | ::-webkit-scrollbar-thumb { 16 | background-color: RGBA(42, 100, 150, .8); 17 | } 18 | 19 | #infowindow img { 20 | float: left; 21 | margin-right: 10px; 22 | } 23 | 24 | body { 25 | position: relative; 26 | font-size: 14px; 27 | } 28 | 29 | .searchbtn, .locbtn { 30 | display: none; 31 | color: black; 32 | background-color: rgba(255, 255, 255, .9); 33 | position: absolute; 34 | top: 10px; 35 | right: 78px; 36 | cursor: pointer; 37 | padding: 7px 6px; 38 | border-radius: 4px; 39 | border: 2px solid #9ac2c3; 40 | } 41 | 42 | .locbtn-large { 43 | color: black; 44 | background-color: rgba(255, 255, 255, .9); 45 | position: absolute; 46 | top: 34px; 47 | left: 319px; 48 | cursor: pointer; 49 | padding: 7px 6px; 50 | border: 1px solid gray; 51 | } 52 | 53 | .locbtn { 54 | right: 45px; 55 | } 56 | 57 | .searchbar { 58 | position: absolute; 59 | top: 25px; 60 | left: 10px; 61 | background-color: rgba(255,255,255, 0.9); 62 | border: 1px solid gray; 63 | padding: 15px 10px; 64 | } 65 | 66 | .searchbar input { 67 | width: 175px; 68 | padding: 5px; 69 | } 70 | 71 | .resultsform #submit { 72 | background: green; 73 | } 74 | 75 | .clearbutton { 76 | cursor: pointer; 77 | text-decoration: underline; 78 | color: #2a6496; 79 | } 80 | 81 | #submit { 82 | width: 80px; 83 | padding: 7px; 84 | border: none; 85 | background: #2A6496; 86 | color: white; 87 | } 88 | 89 | .results { 90 | -webkit-box-sizing: border-box; 91 | box-sizing: border-box; 92 | position: absolute; 93 | top: 25px; 94 | right: 10px; 95 | background: rgba(255,255,255, 0.9); 96 | width: 25%; 97 | border: 1px solid gray; 98 | } 99 | 100 | 101 | .toggler { 102 | position: absolute; 103 | top: 6px; 104 | right: 14px; 105 | font-size: 13px; 106 | font-style: italic; 107 | color: #a6a6a6; 108 | cursor: pointer; 109 | } 110 | 111 | .status-msg { 112 | -webkit-box-sizing: border-box; 113 | box-sizing: border-box; 114 | font-size: 18px; 115 | padding: 9px 20px; 116 | color: #7b7b7b; 117 | margin-bottom: 0; 118 | } 119 | 120 | .waiting { 121 | text-align: center; 122 | } 123 | 124 | #results-list { 125 | padding: 0; 126 | list-style-type: none; 127 | overflow: auto; 128 | max-height: 450px; 129 | } 130 | 131 | #results-list li { 132 | border-top: 1px solid #e0e0e0; 133 | padding: 0 24px; 134 | cursor: pointer; 135 | } 136 | 137 | #results-list li h4 { 138 | margin-bottom: 0; 139 | color: #2A6496; 140 | } 141 | 142 | #results-list .address { 143 | font-size: 12px; 144 | line-height: 1.4; 145 | margin-top: 4px; 146 | } 147 | 148 | #results-list p { 149 | font-size: 14px; 150 | } 151 | 152 | .rating { 153 | color: #810000; 154 | font-weight: bold; 155 | } 156 | 157 | .rating span { 158 | font-size: 12px; 159 | font-style: italic; 160 | color: black; 161 | } 162 | 163 | .mobilebutton { 164 | display: none; 165 | color: #2A6496; 166 | background-color: rgba(255, 255, 255, .9); 167 | position: absolute; 168 | top: 10px; 169 | right: 13px; 170 | cursor: pointer; 171 | padding: 7px 6px; 172 | border-radius: 4px; 173 | border: 2px solid #9ac2c3 ; 174 | } 175 | 176 | .mobile-close { 177 | display: inline-block; 178 | padding: 5px 10px; 179 | color: gray; 180 | cursor: pointer; 181 | margin-left: 315px; 182 | } 183 | 184 | .mobileresults { 185 | position: fixed; 186 | bottom: 0px; 187 | right: 10px; 188 | background-color: rgba(250,250,250, .9); 189 | max-width: 490px; 190 | } 191 | 192 | @media only screen and (max-width: 775px) { 193 | .mobilebutton { 194 | display: block; 195 | } 196 | 197 | .searchbar { 198 | top: 10px; 199 | padding: 7px; 200 | } 201 | 202 | .results { 203 | display: none; 204 | } 205 | 206 | #results-list { 207 | max-height: 400px; 208 | } 209 | 210 | .searchbar input { 211 | width: 108px; 212 | font-size: 12px; 213 | } 214 | 215 | #submit { 216 | width: 45px; 217 | } 218 | 219 | .status-msg { 220 | padding: 0 18px; 221 | font-size: 16px; 222 | } 223 | 224 | .searchbtn, .locbtn { 225 | display: inline; 226 | } 227 | 228 | .locbtn-large { 229 | display: none; 230 | } 231 | } 232 | 233 | @media only screen and (max-width: 499px) { 234 | .mobileresults { 235 | right: 10%; 236 | width: 65%; 237 | } 238 | } 239 | 240 | 241 | /*Autocomplete styles*/ 242 | .autocomplete-suggestions { border: 1px solid #999; background: #FFF; cursor: default; overflow: auto; -webkit-box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); } 243 | .autocomplete-suggestion { padding: 2px 5px; white-space: nowrap; overflow: hidden; } 244 | .autocomplete-no-suggestion { padding: 2px 5px;} 245 | .autocomplete-selected { background: #F0F0F0; } 246 | .autocomplete-suggestions strong { font-weight: bold; color: #19C1CF; } 247 | .autocomplete-group { padding: 2px 5px; } 248 | .autocomplete-group strong { font-weight: bold; font-size: 16px; color: #000; display: block; border-bottom: 1px solid #000; } -------------------------------------------------------------------------------- /img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheryllun/Project5-NeighborhoodMap/23f3c39bd3571ce68eeaec7fd2b2d979f0c00ff1/img/ajax-loader.gif -------------------------------------------------------------------------------- /img/burst_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheryllun/Project5-NeighborhoodMap/23f3c39bd3571ce68eeaec7fd2b2d979f0c00ff1/img/burst_tiny.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CheapSheet - Find Deals Around You! 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 |

34 | 35 |
36 |
37 |

38 | 46 |
47 |
48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 |

66 |
67 |
68 |

69 |
    70 |
  • 71 |

    72 |

    73 |

    74 |

    75 |
  • 76 |
77 |
78 |
79 | 80 | 81 | 91 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | function appViewModel() { 2 | var self = this; 3 | var map, city, infowindow; 4 | var grouponLocations = []; 5 | var grouponReadableNames = []; 6 | 7 | this.grouponDeals = ko.observableArray([]); //initial list of deals 8 | this.filteredList = ko.observableArray([]); //list filtered by search keyword 9 | this.mapMarkers = ko.observableArray([]); //holds all map markers 10 | this.dealStatus = ko.observable('Searching for deals nearby...'); 11 | this.searchStatus = ko.observable(); 12 | this.searchLocation = ko.observable('Washington DC'); 13 | this.loadImg = ko.observable(); 14 | this.numDeals = ko.computed(function() { 15 | return self.filteredList().length; 16 | }); 17 | 18 | //Holds value for list togglings 19 | this.toggleSymbol = ko.observable('hide'); 20 | 21 | //Hold the current location's lat & lng - useful for re-centering map 22 | this.currentLat = ko.observable(38.906830); 23 | this.currentLng = ko.observable(-77.038599); 24 | 25 | // When a deal on the list is clicked, go to corresponding marker and open its info window. 26 | this.goToMarker = function(clickedDeal) { 27 | var clickedDealName = clickedDeal.dealName; 28 | for(var key in self.mapMarkers()) { 29 | if(clickedDealName === self.mapMarkers()[key].marker.title) { 30 | map.panTo(self.mapMarkers()[key].marker.position); 31 | map.setZoom(14); 32 | infowindow.setContent(self.mapMarkers()[key].content); 33 | infowindow.open(map, self.mapMarkers()[key].marker); 34 | map.panBy(0, -150); 35 | self.mobileShow(false); 36 | self.searchStatus(''); 37 | } 38 | } 39 | }; 40 | 41 | // Handle the input given when user searches for deals in a location 42 | this.processLocationSearch = function() { 43 | //Need to use a jQuery selector instead of KO binding because this field is affected by the autocomplete plugin. The value inputted does not seem to register via KO. 44 | self.searchStatus(''); 45 | self.searchStatus('Searching...'); 46 | var newAddress = $('#autocomplete').val(); 47 | 48 | //newGrouponId will hold the Groupon-formatted ID of the inputted city. 49 | var newGrouponId, newLat, newLng; 50 | for(var i = 0; i < 171; i++) { 51 | var name = grouponLocations.divisions[i].name; 52 | if(newAddress == name) { 53 | newGrouponId = grouponLocations.divisions[i].id; 54 | self.currentLat(grouponLocations.divisions[i].lat); 55 | self.currentLng(grouponLocations.divisions[i].lng); 56 | } 57 | } 58 | //Form validation - if user enters an invalid location, return error. 59 | if(!newGrouponId) { 60 | return self.searchStatus('Not a valid location, try again.'); 61 | } else { 62 | //Replace current location with new (human-formatted) location for display in other KO bindings. 63 | self.searchLocation(newAddress); 64 | 65 | //clear our current deal and marker arrays 66 | clearMarkers(); 67 | self.grouponDeals([]); 68 | self.filteredList([]); 69 | self.dealStatus('Loading...'); 70 | self.loadImg(''); 71 | //perform new groupon search and center map to new location 72 | getGroupons(newGrouponId); 73 | map.panTo({lat: self.currentLat(), lng: self.currentLng()}); 74 | } 75 | }; 76 | 77 | 78 | this.filterKeyword = ko.observable(''); 79 | 80 | //Compare search keyword against names and dealTags of existing deals. Return a filtered list and map markers of request. 81 | 82 | this.filterResults = function() { 83 | var searchWord = self.filterKeyword().toLowerCase(); 84 | var array = self.grouponDeals(); 85 | if(!searchWord) { 86 | return; 87 | } else { 88 | //first clear out all entries in the filteredList array 89 | self.filteredList([]); 90 | //Loop through the grouponDeals array and see if the search keyword matches 91 | //with any venue name or dealTags in the list, if so push that object to the filteredList 92 | //array and place the marker on the map. 93 | for(var i=0; i < array.length; i++) { 94 | if(array[i].dealName.toLowerCase().indexOf(searchWord) != -1) { 95 | self.mapMarkers()[i].marker.setMap(map); 96 | self.filteredList.push(array[i]); 97 | } else{ 98 | for(var j = 0; j < array[i].dealTags.length; j++) { 99 | if(array[i].dealTags[j].name.toLowerCase().indexOf(searchWord) != -1) { 100 | self.mapMarkers()[i].marker.setMap(map); 101 | self.filteredList.push(array[i]); 102 | //otherwise hide all other markers from the map 103 | } else { 104 | self.mapMarkers()[i].marker.setMap(null); 105 | } 106 | } 107 | self.dealStatus(self.numDeals() + ' deals found for ' + self.filterKeyword()); 108 | } 109 | } 110 | } 111 | }; 112 | 113 | //Clear keyword from filter and show all deals in current location again. 114 | this.clearFilter = function() { 115 | self.filteredList(self.grouponDeals()); 116 | self.dealStatus(self.numDeals() + ' food and drink deals found near ' + self.searchLocation()); 117 | self.filterKeyword(''); 118 | for(var i = 0; i < self.mapMarkers().length; i++) { 119 | self.mapMarkers()[i].marker.setMap(map); 120 | } 121 | }; 122 | 123 | //toggles the list view 124 | this.listToggle = function() { 125 | if(self.toggleSymbol() === 'hide') { 126 | self.toggleSymbol('show'); 127 | } else { 128 | self.toggleSymbol('hide'); 129 | } 130 | }; 131 | 132 | //Error handling if Google Maps fails to load 133 | this.mapRequestTimeout = setTimeout(function() { 134 | $('#map-canvas').html('We had trouble loading Google Maps. Please refresh your browser and try again.'); 135 | }, 8000); 136 | 137 | // Initialize Google map, perform initial deal search on a city. 138 | function mapInitialize() { 139 | city = new google.maps.LatLng(38.906830, -77.038599); 140 | map = new google.maps.Map(document.getElementById('map-canvas'), { 141 | center: city, 142 | zoom: 10, 143 | zoomControlOptions: { 144 | position: google.maps.ControlPosition.LEFT_CENTER, 145 | style: google.maps.ZoomControlStyle.SMALL 146 | }, 147 | streetViewControlOptions: { 148 | position: google.maps.ControlPosition.LEFT_BOTTOM 149 | }, 150 | mapTypeControl: false, 151 | panControl: false 152 | }); 153 | clearTimeout(self.mapRequestTimeout); 154 | 155 | google.maps.event.addDomListener(window, "resize", function() { 156 | var center = map.getCenter(); 157 | google.maps.event.trigger(map, "resize"); 158 | map.setCenter(center); 159 | }); 160 | 161 | infowindow = new google.maps.InfoWindow({maxWidth: 300}); 162 | getGroupons('washington-dc'); 163 | getGrouponLocations(); 164 | } 165 | 166 | // Use API to get deal data and store the info as objects in an array 167 | function getGroupons(location) { 168 | var grouponUrl = "https://partner-api.groupon.com/deals.json?tsToken=US_AFF_0_203644_212556_0&filters=category:food-and-drink&limit=30&offset=0&division_id="; 169 | var divId = location; 170 | 171 | $.ajax({ 172 | url: grouponUrl + divId, 173 | dataType: 'jsonp', 174 | success: function(data) { 175 | //console.log(data); 176 | var len = data.deals.length; 177 | for(var i = 0; i < len; i++) { 178 | var venueLocation = data.deals[i].options[0].redemptionLocations[0]; 179 | 180 | //this line filters out deals that don't have a physical location to redeem 181 | if (data.deals[i].options[0].redemptionLocations[0] === undefined) continue; 182 | 183 | var venueName = data.deals[i].merchant.name; 184 | venueLat = venueLocation.lat, 185 | venueLon = venueLocation.lng, 186 | gLink = data.deals[i].dealUrl, 187 | gImg = data.deals[i].mediumImageUrl, 188 | blurb = data.deals[i].pitchHtml, 189 | address = venueLocation.streetAddress1, 190 | city = venueLocation.city, 191 | state = venueLocation.state, 192 | zip = venueLocation.postalCode, 193 | shortBlurb = data.deals[i].announcementTitle, 194 | tags = data.deals[i].tags; 195 | 196 | // Some venues have a Yelp rating included. If there is no rating, 197 | //function will stop running because the variable is undefined. 198 | //This if statement handles that scenario by setting rating to an empty string. 199 | var rating; 200 | if((data.deals[i].merchant.ratings == null) || data.deals[i].merchant.ratings[0] === undefined ) { rating = ''; 201 | } else { 202 | var num = data.deals[i].merchant.ratings[0].rating; 203 | var decimal = num.toFixed(1); 204 | rating = ' ' + decimal + ' out of 5'; 205 | } 206 | 207 | self.grouponDeals.push({ 208 | dealName: venueName, 209 | dealLat: venueLat, 210 | dealLon: venueLon, 211 | dealLink: gLink, 212 | dealImg: gImg, 213 | dealBlurb: blurb, 214 | dealAddress: address + "
" + city + ", " + state + " " + zip, 215 | dealShortBlurb: shortBlurb, 216 | dealRating: rating, 217 | dealTags: tags 218 | }); 219 | 220 | } 221 | self.filteredList(self.grouponDeals()); 222 | mapMarkers(self.grouponDeals()); 223 | self.searchStatus(''); 224 | self.loadImg(''); 225 | }, 226 | error: function() { 227 | self.dealStatus('Oops, something went wrong, please refresh and try again.'); 228 | self.loadImg(''); 229 | } 230 | }); 231 | } 232 | 233 | // Create and place markers and info windows on the map based on data from API 234 | function mapMarkers(array) { 235 | $.each(array, function(index, value) { 236 | var latitude = value.dealLat, 237 | longitude = value.dealLon, 238 | geoLoc = new google.maps.LatLng(latitude, longitude), 239 | thisRestaurant = value.dealName; 240 | 241 | var contentString = '
' + 242 | '' + 243 | '

' + value.dealName + '

' + 244 | '

' + value.dealAddress + '

' + 245 | '

' + value.dealRating + '

' + 246 | '

Click to view deal

' + 247 | '

' + value.dealBlurb + '

'; 248 | 249 | var marker = new google.maps.Marker({ 250 | position: geoLoc, 251 | title: thisRestaurant, 252 | map: map 253 | }); 254 | 255 | self.mapMarkers.push({marker: marker, content: contentString}); 256 | 257 | self.dealStatus(self.numDeals() + ' food and drink deals found near ' + self.searchLocation()); 258 | 259 | //generate infowindows for each deal 260 | google.maps.event.addListener(marker, 'click', function() { 261 | self.searchStatus(''); 262 | infowindow.setContent(contentString); 263 | map.setZoom(14); 264 | map.setCenter(marker.position); 265 | infowindow.open(map, marker); 266 | map.panBy(0, -150); 267 | }); 268 | }); 269 | } 270 | 271 | // Clear markers from map and array 272 | function clearMarkers() { 273 | $.each(self.mapMarkers(), function(key, value) { 274 | value.marker.setMap(null); 275 | }); 276 | self.mapMarkers([]); 277 | } 278 | 279 | // Groupon's deal locations have a separate ID than the human-readable name 280 | //(eg washington-dc instead of Washington DC). This ajax call uses the Groupon 281 | //Division API to pull a list of IDs and their corresponding names to use for 282 | //validation in the search bar. 283 | 284 | function getGrouponLocations() { 285 | $.ajax({ 286 | url: 'https://partner-api.groupon.com/division.json', 287 | dataType: 'jsonp', 288 | success: function(data) { 289 | grouponLocations = data; 290 | for(var i = 0; i < 171; i++) { 291 | var readableName = data.divisions[i].name; 292 | grouponReadableNames.push(readableName); 293 | } 294 | 295 | $('#autocomplete').autocomplete({ 296 | lookup: grouponReadableNames, 297 | showNoSuggestionNotice: true, 298 | noSuggestionNotice: 'Sorry, no matching results', 299 | }); 300 | }, 301 | error: function() { 302 | self.dealStatus('Oops, something went wrong, please reload the page and try again.'); 303 | self.loadImg(''); 304 | } 305 | }); 306 | } 307 | 308 | 309 | //Manages the toggling of the list view, location centering, and search bar on a mobile device. 310 | 311 | this.mobileShow = ko.observable(false); 312 | this.searchBarShow = ko.observable(true); 313 | 314 | this.mobileToggleList = function() { 315 | if(self.mobileShow() === false) { 316 | self.mobileShow(true); 317 | } else { 318 | self.mobileShow(false); 319 | } 320 | }; 321 | 322 | this.searchToggle = function() { 323 | if(self.searchBarShow() === true) { 324 | self.searchBarShow(false); 325 | } else { 326 | self.searchBarShow(true); 327 | } 328 | }; 329 | 330 | //Re-center map to current city if you're viewing deals that are further away 331 | this.centerMap = function() { 332 | infowindow.close(); 333 | var currCenter = map.getCenter(); 334 | var cityCenter = new google.maps.LatLng(self.currentLat(), self.currentLng()); 335 | if(cityCenter === currCenter) { 336 | self.searchStatus('Map is already centered.'); 337 | } else { 338 | self.searchStatus(''); 339 | map.panTo(cityCenter); 340 | map.setZoom(10); 341 | } 342 | }; 343 | 344 | mapInitialize(); 345 | } 346 | 347 | //custom binding highlights the search text on focus 348 | 349 | ko.bindingHandlers.selectOnFocus = { 350 | update: function (element) { 351 | ko.utils.registerEventHandler(element, 'focus', function (e) { 352 | element.select(); 353 | }); 354 | } 355 | }; 356 | 357 | ko.applyBindings(new appViewModel()); -------------------------------------------------------------------------------- /js/jquery.autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ajax Autocomplete for jQuery, version %version% 3 | * (c) 2014 Tomas Kirda 4 | * 5 | * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. 6 | * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete 7 | */ 8 | 9 | /*jslint browser: true, white: true, plusplus: true, vars: true */ 10 | /*global define, window, document, jQuery, exports, require */ 11 | 12 | // Expose plugin as an AMD module if AMD loader is present: 13 | (function (factory) { 14 | 'use strict'; 15 | if (typeof define === 'function' && define.amd) { 16 | // AMD. Register as an anonymous module. 17 | define(['jquery'], factory); 18 | } else if (typeof exports === 'object' && typeof require === 'function') { 19 | // Browserify 20 | factory(require('jquery')); 21 | } else { 22 | // Browser globals 23 | factory(jQuery); 24 | } 25 | }(function ($) { 26 | 'use strict'; 27 | 28 | var 29 | utils = (function () { 30 | return { 31 | escapeRegExChars: function (value) { 32 | return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 33 | }, 34 | createNode: function (containerClass) { 35 | var div = document.createElement('div'); 36 | div.className = containerClass; 37 | div.style.position = 'absolute'; 38 | div.style.display = 'none'; 39 | return div; 40 | } 41 | }; 42 | }()), 43 | 44 | keys = { 45 | ESC: 27, 46 | TAB: 9, 47 | RETURN: 13, 48 | LEFT: 37, 49 | UP: 38, 50 | RIGHT: 39, 51 | DOWN: 40 52 | }; 53 | 54 | function Autocomplete(el, options) { 55 | var noop = function () { }, 56 | that = this, 57 | defaults = { 58 | ajaxSettings: {}, 59 | autoSelectFirst: false, 60 | appendTo: document.body, 61 | serviceUrl: null, 62 | lookup: null, 63 | onSelect: null, 64 | width: 'auto', 65 | minChars: 1, 66 | maxHeight: 300, 67 | deferRequestBy: 0, 68 | params: {}, 69 | formatResult: Autocomplete.formatResult, 70 | delimiter: null, 71 | zIndex: 9999, 72 | type: 'GET', 73 | noCache: false, 74 | onSearchStart: noop, 75 | onSearchComplete: noop, 76 | onSearchError: noop, 77 | preserveInput: false, 78 | containerClass: 'autocomplete-suggestions', 79 | tabDisabled: false, 80 | dataType: 'text', 81 | currentRequest: null, 82 | triggerSelectOnValidInput: true, 83 | preventBadQueries: true, 84 | lookupFilter: function (suggestion, originalQuery, queryLowerCase) { 85 | return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; 86 | }, 87 | paramName: 'query', 88 | transformResult: function (response) { 89 | return typeof response === 'string' ? $.parseJSON(response) : response; 90 | }, 91 | showNoSuggestionNotice: false, 92 | noSuggestionNotice: 'No results', 93 | orientation: 'bottom', 94 | forceFixPosition: false 95 | }; 96 | 97 | // Shared variables: 98 | that.element = el; 99 | that.el = $(el); 100 | that.suggestions = []; 101 | that.badQueries = []; 102 | that.selectedIndex = -1; 103 | that.currentValue = that.element.value; 104 | that.intervalId = 0; 105 | that.cachedResponse = {}; 106 | that.onChangeInterval = null; 107 | that.onChange = null; 108 | that.isLocal = false; 109 | that.suggestionsContainer = null; 110 | that.noSuggestionsContainer = null; 111 | that.options = $.extend({}, defaults, options); 112 | that.classes = { 113 | selected: 'autocomplete-selected', 114 | suggestion: 'autocomplete-suggestion' 115 | }; 116 | that.hint = null; 117 | that.hintValue = ''; 118 | that.selection = null; 119 | 120 | // Initialize and set options: 121 | that.initialize(); 122 | that.setOptions(options); 123 | } 124 | 125 | Autocomplete.utils = utils; 126 | 127 | $.Autocomplete = Autocomplete; 128 | 129 | Autocomplete.formatResult = function (suggestion, currentValue) { 130 | var pattern = '(' + utils.escapeRegExChars(currentValue) + ')'; 131 | 132 | return suggestion.value.replace(new RegExp(pattern, 'gi'), '$1<\/strong>'); 133 | }; 134 | 135 | Autocomplete.prototype = { 136 | 137 | killerFn: null, 138 | 139 | initialize: function () { 140 | var that = this, 141 | suggestionSelector = '.' + that.classes.suggestion, 142 | selected = that.classes.selected, 143 | options = that.options, 144 | container; 145 | 146 | // Remove autocomplete attribute to prevent native suggestions: 147 | that.element.setAttribute('autocomplete', 'off'); 148 | 149 | that.killerFn = function (e) { 150 | if ($(e.target).closest('.' + that.options.containerClass).length === 0) { 151 | that.killSuggestions(); 152 | that.disableKillerFn(); 153 | } 154 | }; 155 | 156 | // html() deals with many types: htmlString or Element or Array or jQuery 157 | that.noSuggestionsContainer = $('
') 158 | .html(this.options.noSuggestionNotice).get(0); 159 | 160 | that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); 161 | 162 | container = $(that.suggestionsContainer); 163 | 164 | container.appendTo(options.appendTo); 165 | 166 | // Only set width if it was provided: 167 | if (options.width !== 'auto') { 168 | container.width(options.width); 169 | } 170 | 171 | // Listen for mouse over event on suggestions list: 172 | container.on('mouseover.autocomplete', suggestionSelector, function () { 173 | that.activate($(this).data('index')); 174 | }); 175 | 176 | // Deselect active element when mouse leaves suggestions container: 177 | container.on('mouseout.autocomplete', function () { 178 | that.selectedIndex = -1; 179 | container.children('.' + selected).removeClass(selected); 180 | }); 181 | 182 | // Listen for click event on suggestions list: 183 | container.on('click.autocomplete', suggestionSelector, function () { 184 | that.select($(this).data('index')); 185 | }); 186 | 187 | that.fixPositionCapture = function () { 188 | if (that.visible) { 189 | that.fixPosition(); 190 | } 191 | }; 192 | 193 | $(window).on('resize.autocomplete', that.fixPositionCapture); 194 | 195 | that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); }); 196 | that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); }); 197 | that.el.on('blur.autocomplete', function () { that.onBlur(); }); 198 | that.el.on('focus.autocomplete', function () { that.onFocus(); }); 199 | that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); }); 200 | that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); }); 201 | }, 202 | 203 | onFocus: function () { 204 | var that = this; 205 | that.fixPosition(); 206 | if (that.options.minChars <= that.el.val().length) { 207 | that.onValueChange(); 208 | } 209 | }, 210 | 211 | onBlur: function () { 212 | this.enableKillerFn(); 213 | }, 214 | 215 | setOptions: function (suppliedOptions) { 216 | var that = this, 217 | options = that.options; 218 | 219 | $.extend(options, suppliedOptions); 220 | 221 | that.isLocal = $.isArray(options.lookup); 222 | 223 | if (that.isLocal) { 224 | options.lookup = that.verifySuggestionsFormat(options.lookup); 225 | } 226 | 227 | options.orientation = that.validateOrientation(options.orientation, 'bottom'); 228 | 229 | // Adjust height, width and z-index: 230 | $(that.suggestionsContainer).css({ 231 | 'max-height': options.maxHeight + 'px', 232 | 'width': options.width + 'px', 233 | 'z-index': options.zIndex 234 | }); 235 | }, 236 | 237 | 238 | clearCache: function () { 239 | this.cachedResponse = {}; 240 | this.badQueries = []; 241 | }, 242 | 243 | clear: function () { 244 | this.clearCache(); 245 | this.currentValue = ''; 246 | this.suggestions = []; 247 | }, 248 | 249 | disable: function () { 250 | var that = this; 251 | that.disabled = true; 252 | clearInterval(that.onChangeInterval); 253 | if (that.currentRequest) { 254 | that.currentRequest.abort(); 255 | } 256 | }, 257 | 258 | enable: function () { 259 | this.disabled = false; 260 | }, 261 | 262 | fixPosition: function () { 263 | // Use only when container has already its content 264 | 265 | var that = this, 266 | $container = $(that.suggestionsContainer), 267 | containerParent = $container.parent().get(0); 268 | // Fix position automatically when appended to body. 269 | // In other cases force parameter must be given. 270 | if (containerParent !== document.body && !that.options.forceFixPosition) { 271 | return; 272 | } 273 | 274 | // Choose orientation 275 | var orientation = that.options.orientation, 276 | containerHeight = $container.outerHeight(), 277 | height = that.el.outerHeight(), 278 | offset = that.el.offset(), 279 | styles = { 'top': offset.top, 'left': offset.left }; 280 | 281 | if (orientation === 'auto') { 282 | var viewPortHeight = $(window).height(), 283 | scrollTop = $(window).scrollTop(), 284 | topOverflow = -scrollTop + offset.top - containerHeight, 285 | bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); 286 | 287 | orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom'; 288 | } 289 | 290 | if (orientation === 'top') { 291 | styles.top += -containerHeight; 292 | } else { 293 | styles.top += height; 294 | } 295 | 296 | // If container is not positioned to body, 297 | // correct its position using offset parent offset 298 | if(containerParent !== document.body) { 299 | var opacity = $container.css('opacity'), 300 | parentOffsetDiff; 301 | 302 | if (!that.visible){ 303 | $container.css('opacity', 0).show(); 304 | } 305 | 306 | parentOffsetDiff = $container.offsetParent().offset(); 307 | styles.top -= parentOffsetDiff.top; 308 | styles.left -= parentOffsetDiff.left; 309 | 310 | if (!that.visible){ 311 | $container.css('opacity', opacity).hide(); 312 | } 313 | } 314 | 315 | // -2px to account for suggestions border. 316 | if (that.options.width === 'auto') { 317 | styles.width = (that.el.outerWidth() - 2) + 'px'; 318 | } 319 | 320 | $container.css(styles); 321 | }, 322 | 323 | enableKillerFn: function () { 324 | var that = this; 325 | $(document).on('click.autocomplete', that.killerFn); 326 | }, 327 | 328 | disableKillerFn: function () { 329 | var that = this; 330 | $(document).off('click.autocomplete', that.killerFn); 331 | }, 332 | 333 | killSuggestions: function () { 334 | var that = this; 335 | that.stopKillSuggestions(); 336 | that.intervalId = window.setInterval(function () { 337 | that.hide(); 338 | that.stopKillSuggestions(); 339 | }, 50); 340 | }, 341 | 342 | stopKillSuggestions: function () { 343 | window.clearInterval(this.intervalId); 344 | }, 345 | 346 | isCursorAtEnd: function () { 347 | var that = this, 348 | valLength = that.el.val().length, 349 | selectionStart = that.element.selectionStart, 350 | range; 351 | 352 | if (typeof selectionStart === 'number') { 353 | return selectionStart === valLength; 354 | } 355 | if (document.selection) { 356 | range = document.selection.createRange(); 357 | range.moveStart('character', -valLength); 358 | return valLength === range.text.length; 359 | } 360 | return true; 361 | }, 362 | 363 | onKeyPress: function (e) { 364 | var that = this; 365 | 366 | // If suggestions are hidden and user presses arrow down, display suggestions: 367 | if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) { 368 | that.suggest(); 369 | return; 370 | } 371 | 372 | if (that.disabled || !that.visible) { 373 | return; 374 | } 375 | 376 | switch (e.which) { 377 | case keys.ESC: 378 | that.el.val(that.currentValue); 379 | that.hide(); 380 | break; 381 | case keys.RIGHT: 382 | if (that.hint && that.options.onHint && that.isCursorAtEnd()) { 383 | that.selectHint(); 384 | break; 385 | } 386 | return; 387 | case keys.TAB: 388 | if (that.hint && that.options.onHint) { 389 | that.selectHint(); 390 | return; 391 | } 392 | if (that.selectedIndex === -1) { 393 | that.hide(); 394 | return; 395 | } 396 | that.select(that.selectedIndex); 397 | if (that.options.tabDisabled === false) { 398 | return; 399 | } 400 | break; 401 | case keys.RETURN: 402 | if (that.selectedIndex === -1) { 403 | that.hide(); 404 | return; 405 | } 406 | that.select(that.selectedIndex); 407 | break; 408 | case keys.UP: 409 | that.moveUp(); 410 | break; 411 | case keys.DOWN: 412 | that.moveDown(); 413 | break; 414 | default: 415 | return; 416 | } 417 | 418 | // Cancel event if function did not return: 419 | e.stopImmediatePropagation(); 420 | e.preventDefault(); 421 | }, 422 | 423 | onKeyUp: function (e) { 424 | var that = this; 425 | 426 | if (that.disabled) { 427 | return; 428 | } 429 | 430 | switch (e.which) { 431 | case keys.UP: 432 | case keys.DOWN: 433 | return; 434 | } 435 | 436 | clearInterval(that.onChangeInterval); 437 | 438 | if (that.currentValue !== that.el.val()) { 439 | that.findBestHint(); 440 | if (that.options.deferRequestBy > 0) { 441 | // Defer lookup in case when value changes very quickly: 442 | that.onChangeInterval = setInterval(function () { 443 | that.onValueChange(); 444 | }, that.options.deferRequestBy); 445 | } else { 446 | that.onValueChange(); 447 | } 448 | } 449 | }, 450 | 451 | onValueChange: function () { 452 | var that = this, 453 | options = that.options, 454 | value = that.el.val(), 455 | query = that.getQuery(value), 456 | index; 457 | 458 | if (that.selection && that.currentValue !== query) { 459 | that.selection = null; 460 | (options.onInvalidateSelection || $.noop).call(that.element); 461 | } 462 | 463 | clearInterval(that.onChangeInterval); 464 | that.currentValue = value; 465 | that.selectedIndex = -1; 466 | 467 | // Check existing suggestion for the match before proceeding: 468 | if (options.triggerSelectOnValidInput) { 469 | index = that.findSuggestionIndex(query); 470 | if (index !== -1) { 471 | that.select(index); 472 | return; 473 | } 474 | } 475 | 476 | if (query.length < options.minChars) { 477 | that.hide(); 478 | } else { 479 | that.getSuggestions(query); 480 | } 481 | }, 482 | 483 | findSuggestionIndex: function (query) { 484 | var that = this, 485 | index = -1, 486 | queryLowerCase = query.toLowerCase(); 487 | 488 | $.each(that.suggestions, function (i, suggestion) { 489 | if (suggestion.value.toLowerCase() === queryLowerCase) { 490 | index = i; 491 | return false; 492 | } 493 | }); 494 | 495 | return index; 496 | }, 497 | 498 | getQuery: function (value) { 499 | var delimiter = this.options.delimiter, 500 | parts; 501 | 502 | if (!delimiter) { 503 | return value; 504 | } 505 | parts = value.split(delimiter); 506 | return $.trim(parts[parts.length - 1]); 507 | }, 508 | 509 | getSuggestionsLocal: function (query) { 510 | var that = this, 511 | options = that.options, 512 | queryLowerCase = query.toLowerCase(), 513 | filter = options.lookupFilter, 514 | limit = parseInt(options.lookupLimit, 10), 515 | data; 516 | 517 | data = { 518 | suggestions: $.grep(options.lookup, function (suggestion) { 519 | return filter(suggestion, query, queryLowerCase); 520 | }) 521 | }; 522 | 523 | if (limit && data.suggestions.length > limit) { 524 | data.suggestions = data.suggestions.slice(0, limit); 525 | } 526 | 527 | return data; 528 | }, 529 | 530 | getSuggestions: function (q) { 531 | var response, 532 | that = this, 533 | options = that.options, 534 | serviceUrl = options.serviceUrl, 535 | params, 536 | cacheKey, 537 | ajaxSettings; 538 | 539 | options.params[options.paramName] = q; 540 | params = options.ignoreParams ? null : options.params; 541 | 542 | if (options.onSearchStart.call(that.element, options.params) === false) { 543 | return; 544 | } 545 | 546 | if ($.isFunction(options.lookup)){ 547 | options.lookup(q, function (data) { 548 | that.suggestions = data.suggestions; 549 | that.suggest(); 550 | options.onSearchComplete.call(that.element, q, data.suggestions); 551 | }); 552 | return; 553 | } 554 | 555 | if (that.isLocal) { 556 | response = that.getSuggestionsLocal(q); 557 | } else { 558 | if ($.isFunction(serviceUrl)) { 559 | serviceUrl = serviceUrl.call(that.element, q); 560 | } 561 | cacheKey = serviceUrl + '?' + $.param(params || {}); 562 | response = that.cachedResponse[cacheKey]; 563 | } 564 | 565 | if (response && $.isArray(response.suggestions)) { 566 | that.suggestions = response.suggestions; 567 | that.suggest(); 568 | options.onSearchComplete.call(that.element, q, response.suggestions); 569 | } else if (!that.isBadQuery(q)) { 570 | if (that.currentRequest) { 571 | that.currentRequest.abort(); 572 | } 573 | 574 | ajaxSettings = { 575 | url: serviceUrl, 576 | data: params, 577 | type: options.type, 578 | dataType: options.dataType 579 | }; 580 | 581 | $.extend(ajaxSettings, options.ajaxSettings); 582 | 583 | that.currentRequest = $.ajax(ajaxSettings).done(function (data) { 584 | var result; 585 | that.currentRequest = null; 586 | result = options.transformResult(data); 587 | that.processResponse(result, q, cacheKey); 588 | options.onSearchComplete.call(that.element, q, result.suggestions); 589 | }).fail(function (jqXHR, textStatus, errorThrown) { 590 | options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); 591 | }); 592 | } else { 593 | options.onSearchComplete.call(that.element, q, []); 594 | } 595 | }, 596 | 597 | isBadQuery: function (q) { 598 | if (!this.options.preventBadQueries){ 599 | return false; 600 | } 601 | 602 | var badQueries = this.badQueries, 603 | i = badQueries.length; 604 | 605 | while (i--) { 606 | if (q.indexOf(badQueries[i]) === 0) { 607 | return true; 608 | } 609 | } 610 | 611 | return false; 612 | }, 613 | 614 | hide: function () { 615 | var that = this; 616 | that.visible = false; 617 | that.selectedIndex = -1; 618 | clearInterval(that.onChangeInterval); 619 | $(that.suggestionsContainer).hide(); 620 | that.signalHint(null); 621 | }, 622 | 623 | suggest: function () { 624 | if (this.suggestions.length === 0) { 625 | if (this.options.showNoSuggestionNotice) { 626 | this.noSuggestions(); 627 | } else { 628 | this.hide(); 629 | } 630 | return; 631 | } 632 | 633 | var that = this, 634 | options = that.options, 635 | groupBy = options.groupBy, 636 | formatResult = options.formatResult, 637 | value = that.getQuery(that.currentValue), 638 | className = that.classes.suggestion, 639 | classSelected = that.classes.selected, 640 | container = $(that.suggestionsContainer), 641 | noSuggestionsContainer = $(that.noSuggestionsContainer), 642 | beforeRender = options.beforeRender, 643 | html = '', 644 | category, 645 | formatGroup = function (suggestion, index) { 646 | var currentCategory = suggestion.data[groupBy]; 647 | 648 | if (category === currentCategory){ 649 | return ''; 650 | } 651 | 652 | category = currentCategory; 653 | 654 | return '
' + category + '
'; 655 | }, 656 | index; 657 | 658 | if (options.triggerSelectOnValidInput) { 659 | index = that.findSuggestionIndex(value); 660 | if (index !== -1) { 661 | that.select(index); 662 | return; 663 | } 664 | } 665 | 666 | // Build suggestions inner HTML: 667 | $.each(that.suggestions, function (i, suggestion) { 668 | if (groupBy){ 669 | html += formatGroup(suggestion, value, i); 670 | } 671 | 672 | html += '
' + formatResult(suggestion, value) + '
'; 673 | }); 674 | 675 | this.adjustContainerWidth(); 676 | 677 | noSuggestionsContainer.detach(); 678 | container.html(html); 679 | 680 | if ($.isFunction(beforeRender)) { 681 | beforeRender.call(that.element, container); 682 | } 683 | 684 | that.fixPosition(); 685 | container.show(); 686 | 687 | // Select first value by default: 688 | if (options.autoSelectFirst) { 689 | that.selectedIndex = 0; 690 | container.scrollTop(0); 691 | container.children().first().addClass(classSelected); 692 | } 693 | 694 | that.visible = true; 695 | that.findBestHint(); 696 | }, 697 | 698 | noSuggestions: function() { 699 | var that = this, 700 | container = $(that.suggestionsContainer), 701 | noSuggestionsContainer = $(that.noSuggestionsContainer); 702 | 703 | this.adjustContainerWidth(); 704 | 705 | // Some explicit steps. Be careful here as it easy to get 706 | // noSuggestionsContainer removed from DOM if not detached properly. 707 | noSuggestionsContainer.detach(); 708 | container.empty(); // clean suggestions if any 709 | container.append(noSuggestionsContainer); 710 | 711 | that.fixPosition(); 712 | 713 | container.show(); 714 | that.visible = true; 715 | }, 716 | 717 | adjustContainerWidth: function() { 718 | var that = this, 719 | options = that.options, 720 | width, 721 | container = $(that.suggestionsContainer); 722 | 723 | // If width is auto, adjust width before displaying suggestions, 724 | // because if instance was created before input had width, it will be zero. 725 | // Also it adjusts if input width has changed. 726 | // -2px to account for suggestions border. 727 | if (options.width === 'auto') { 728 | width = that.el.outerWidth() - 2; 729 | container.width(width > 0 ? width : 300); 730 | } 731 | }, 732 | 733 | findBestHint: function () { 734 | var that = this, 735 | value = that.el.val().toLowerCase(), 736 | bestMatch = null; 737 | 738 | if (!value) { 739 | return; 740 | } 741 | 742 | $.each(that.suggestions, function (i, suggestion) { 743 | var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0; 744 | if (foundMatch) { 745 | bestMatch = suggestion; 746 | } 747 | return !foundMatch; 748 | }); 749 | 750 | that.signalHint(bestMatch); 751 | }, 752 | 753 | signalHint: function (suggestion) { 754 | var hintValue = '', 755 | that = this; 756 | if (suggestion) { 757 | hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length); 758 | } 759 | if (that.hintValue !== hintValue) { 760 | that.hintValue = hintValue; 761 | that.hint = suggestion; 762 | (this.options.onHint || $.noop)(hintValue); 763 | } 764 | }, 765 | 766 | verifySuggestionsFormat: function (suggestions) { 767 | // If suggestions is string array, convert them to supported format: 768 | if (suggestions.length && typeof suggestions[0] === 'string') { 769 | return $.map(suggestions, function (value) { 770 | return { value: value, data: null }; 771 | }); 772 | } 773 | 774 | return suggestions; 775 | }, 776 | 777 | validateOrientation: function(orientation, fallback) { 778 | orientation = $.trim(orientation || '').toLowerCase(); 779 | 780 | if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){ 781 | orientation = fallback; 782 | } 783 | 784 | return orientation; 785 | }, 786 | 787 | processResponse: function (result, originalQuery, cacheKey) { 788 | var that = this, 789 | options = that.options; 790 | 791 | result.suggestions = that.verifySuggestionsFormat(result.suggestions); 792 | 793 | // Cache results if cache is not disabled: 794 | if (!options.noCache) { 795 | that.cachedResponse[cacheKey] = result; 796 | if (options.preventBadQueries && result.suggestions.length === 0) { 797 | that.badQueries.push(originalQuery); 798 | } 799 | } 800 | 801 | // Return if originalQuery is not matching current query: 802 | if (originalQuery !== that.getQuery(that.currentValue)) { 803 | return; 804 | } 805 | 806 | that.suggestions = result.suggestions; 807 | that.suggest(); 808 | }, 809 | 810 | activate: function (index) { 811 | var that = this, 812 | activeItem, 813 | selected = that.classes.selected, 814 | container = $(that.suggestionsContainer), 815 | children = container.find('.' + that.classes.suggestion); 816 | 817 | container.find('.' + selected).removeClass(selected); 818 | 819 | that.selectedIndex = index; 820 | 821 | if (that.selectedIndex !== -1 && children.length > that.selectedIndex) { 822 | activeItem = children.get(that.selectedIndex); 823 | $(activeItem).addClass(selected); 824 | return activeItem; 825 | } 826 | 827 | return null; 828 | }, 829 | 830 | selectHint: function () { 831 | var that = this, 832 | i = $.inArray(that.hint, that.suggestions); 833 | 834 | that.select(i); 835 | }, 836 | 837 | select: function (i) { 838 | var that = this; 839 | that.hide(); 840 | that.onSelect(i); 841 | }, 842 | 843 | moveUp: function () { 844 | var that = this; 845 | 846 | if (that.selectedIndex === -1) { 847 | return; 848 | } 849 | 850 | if (that.selectedIndex === 0) { 851 | $(that.suggestionsContainer).children().first().removeClass(that.classes.selected); 852 | that.selectedIndex = -1; 853 | that.el.val(that.currentValue); 854 | that.findBestHint(); 855 | return; 856 | } 857 | 858 | that.adjustScroll(that.selectedIndex - 1); 859 | }, 860 | 861 | moveDown: function () { 862 | var that = this; 863 | 864 | if (that.selectedIndex === (that.suggestions.length - 1)) { 865 | return; 866 | } 867 | 868 | that.adjustScroll(that.selectedIndex + 1); 869 | }, 870 | 871 | adjustScroll: function (index) { 872 | var that = this, 873 | activeItem = that.activate(index); 874 | 875 | if (!activeItem) { 876 | return; 877 | } 878 | 879 | var offsetTop, 880 | upperBound, 881 | lowerBound, 882 | heightDelta = $(activeItem).outerHeight(); 883 | 884 | offsetTop = activeItem.offsetTop; 885 | upperBound = $(that.suggestionsContainer).scrollTop(); 886 | lowerBound = upperBound + that.options.maxHeight - heightDelta; 887 | 888 | if (offsetTop < upperBound) { 889 | $(that.suggestionsContainer).scrollTop(offsetTop); 890 | } else if (offsetTop > lowerBound) { 891 | $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta); 892 | } 893 | 894 | if (!that.options.preserveInput) { 895 | that.el.val(that.getValue(that.suggestions[index].value)); 896 | } 897 | that.signalHint(null); 898 | }, 899 | 900 | onSelect: function (index) { 901 | var that = this, 902 | onSelectCallback = that.options.onSelect, 903 | suggestion = that.suggestions[index]; 904 | 905 | that.currentValue = that.getValue(suggestion.value); 906 | 907 | if (that.currentValue !== that.el.val() && !that.options.preserveInput) { 908 | that.el.val(that.currentValue); 909 | } 910 | 911 | that.signalHint(null); 912 | that.suggestions = []; 913 | that.selection = suggestion; 914 | 915 | if ($.isFunction(onSelectCallback)) { 916 | onSelectCallback.call(that.element, suggestion); 917 | } 918 | }, 919 | 920 | getValue: function (value) { 921 | var that = this, 922 | delimiter = that.options.delimiter, 923 | currentValue, 924 | parts; 925 | 926 | if (!delimiter) { 927 | return value; 928 | } 929 | 930 | currentValue = that.currentValue; 931 | parts = currentValue.split(delimiter); 932 | 933 | if (parts.length === 1) { 934 | return value; 935 | } 936 | 937 | return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value; 938 | }, 939 | 940 | dispose: function () { 941 | var that = this; 942 | that.el.off('.autocomplete').removeData('autocomplete'); 943 | that.disableKillerFn(); 944 | $(window).off('resize.autocomplete', that.fixPositionCapture); 945 | $(that.suggestionsContainer).remove(); 946 | } 947 | }; 948 | 949 | // Create chainable jQuery plugin: 950 | $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { 951 | var dataKey = 'autocomplete'; 952 | // If function invoked without argument return 953 | // instance of the first matched element: 954 | if (arguments.length === 0) { 955 | return this.first().data(dataKey); 956 | } 957 | 958 | return this.each(function () { 959 | var inputElement = $(this), 960 | instance = inputElement.data(dataKey); 961 | 962 | if (typeof options === 'string') { 963 | if (instance && typeof instance[options] === 'function') { 964 | instance[options](args); 965 | } 966 | } else { 967 | // If instance already exists, destroy it: 968 | if (instance && instance.dispose) { 969 | instance.dispose(); 970 | } 971 | instance = new Autocomplete(this, options); 972 | inputElement.data(dataKey, instance); 973 | } 974 | }); 975 | }; 976 | })); 977 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | "CheapSheet" - Udacity Neighborhood Map Project 2 | -------- 3 | 4 | Do you like eating out but don't enjoy paying full price? Find deals on the fly, where-ever you live. Try it out here: [View CheapSheet!](http://sheryllun.github.io/Project5-NeighborhoodMap) Oh - and it's mobile friendly so keep it saved on your phone! Shortlink to site: http://goo.gl/YWNQ8j 5 | 6 | *** 7 | 8 | Features 9 | ------- 10 | 11 | * Autocomplete deal location search 12 | * Search produces top thirty results near your requested location 13 | * Filter deals by restaurant name or type (Please note, filtering by type is not extremely reliable as it uses the "deal tags" provided in the Groupon API response that aren't very descriptive. Try searching for tags such as "restaurants" and "pizza" for best results. Filtering by name should work great.) 14 | * Integrated list view links to corresponding map marker on click 15 | * Mobile friendly view hides list and search bar until you need it 16 | * Re-center your map if you stray too far 17 | * Venue ratings (if available) help you decide whether the restaurant is a go or a no! 18 | 19 | *** 20 | 21 | Resources Used 22 | ----- 23 | 24 | * StackOverflow 25 | * Udacity Javascript Design Patterns Course 26 | * Udacity Intro to AJAX Course 27 | * Knockout JS Documentation & Tutorials 28 | * Google Maps API Documentation 29 | * Groupon API Documentation 30 | * DesignShack Blog - [Autocomplete Plugin](http://designshack.net/articles/javascript/create-a-simple-autocomplete-with-html5-jquery/) 31 | * DevBridge [JQuery Autocomplete](https://github.com/devbridge/jQuery-Autocomplete) 32 | * [Select on Focus] (http://one-com.github.io/knockout-select-on-focus/) --------------------------------------------------------------------------------