1angular.module("searchApp",['btorfs.multiselect']); 2(function ( $ ) { 3 4 function qs(key) { 5 key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, "\\$&"); // escape RegEx meta chars 6 var match = location.search.match(new RegExp("[?&]"+key+"=([^&]+)(&|$)")); 7 return match && decodeURIComponent(match[1].replace(/\+/g, " ")); 8 } 9 10 // Get all values for a query param (for multi-value params like tags, authors) 11 function qsAll(key) { 12 var results = []; 13 var urlParams = new URLSearchParams(location.search); 14 urlParams.getAll(key).forEach(function(val) { 15 if (val) results.push(val); 16 }); 17 return results; 18 } 19 20 // Extract userid from email format (e.g., "[email protected]" -> "hensonba") 21 // If not in email format, returns the input unchanged 22 function extractUserId(username) { 23 if (!username) return username; 24 var atIndex = username.indexOf('@'); 25 if (atIndex > 0) { 26 return username.substring(0, atIndex); 27 } 28 return username; 29 } 30 31 // Check if URL has any search-related query parameters 32 function hasSearchQueryParams() { 33 var searchParams = ["term", "tags", "authors", "displayDateStart", "displayDateEnd", "showEvents", "force-search", "submitted"]; 34 for (var i = 0; i < searchParams.length; i++) { 35 if (qs(searchParams[i]) != null) { 36 return true; 37 } 38 } 39 return false; 40 } 41 42 angular.module("searchApp") 43 .controller("searchController", ["$timeout","$scope", function($timeout, $scope) { 44 //disabled angular for many search components 45 if($(".component.newssearch").size() > 1) { 46 47 } else { 48 49 var multiselectOptionsFieldNames = ["authors"]; 50 var namespaces = $(".component.newssearch").attr("data-namespaces").split(","); 51 52 multiselectOptionsFieldNames = multiselectOptionsFieldNames.concat(namespaces); 53 54 $(".taglist").hide(); 55 56 // Track if user has interacted with search 57 var firstLoad = true; 58 59 $scope.init = function () { 60 //facets hold the data that is sent to the search 61 $scope.facets = { 62 term: qs("term"), 63 displayDateStart: (qs("displayDateStart") != null ? (new Date(parseInt(qs("displayDateStart"),10))).toDateString() : null), 64 displayDateEnd: (qs("displayDateEnd") != null ? (new Date(parseInt(qs("displayDateEnd"),10))).toDateString() : null), 65 showEvents: qs("showEvents"), 66 authors: [] 67 }; 68 //add tags top each array 69 namespaces.forEach(function(ns) { 70 $scope.facets[ns] = []; 71 }); 72 73 //msoptions holds the options for the multiselect dropdowns 74 $scope.msOptions = {}; 75 // Get URL params for tags and authors 76 // Support both generic "tags" param and namespace-specific params (e.g., "units", "colleges") 77 var urlTags = qsAll("tags"); 78 var urlAuthors = qsAll("authors"); 79 var urlTagsByNamespace = {}; 80 namespaces.forEach(function(ns) { 81 urlTagsByNamespace[ns] = qsAll(ns); 82 }); 83 84 multiselectOptionsFieldNames.forEach(function(type) { 85 if(typeof $scope.msOptions[type] === "undefined") { 86 $scope.msOptions[type] = []; 87 } 88 $("#"+type+"-options button").each(function(index,element){ 89 let dataValue = $(element).attr("data-value"); 90 let option = { 91 id: (type === "authors") ? extractUserId(dataValue) : dataValue, 92 display: $(element).attr("data-display"), 93 category: type 94 }; 95 // Check if option is in URL params OR has data-selected attribute
96 // Support both ?tags=units:online and ?units=units:online formats 97 var isUrlSelected = (type === "authors") 98 ? urlAuthors.indexOf(option.id) > -1 99 : (urlTags.indexOf(option.id) > -1 || (urlTagsByNamespace[type] || []).indexOf(option.id) > -1); 100 if($(element).is('[data-selected]') || isUrlSelected){ 101 $scope.facets[type].push(option); 102 } 103 $scope.msOptions[type].push(option); 104 }); 105 106 // Add URL tags/authors that don't have matching UI buttons 107 // Combine generic tags param with namespace-specific param 108 var urlValues = (type === "authors") 109 ? urlAuthors 110 : urlTags.concat(urlTagsByNamespace[type] || []); 111 urlValues.forEach(function(urlVal) { 112 // For tags, only add to matching namespace (e.g., "topics:foo" goes to "topics") 113 if (type !== "authors") { 114 var tagNamespace = urlVal.split(":")[0]; 115 if (tagNamespace !== type) { 116 return; // Skip - wrong namespace for this type 117 } 118 } 119 var alreadyAdded = $scope.facets[type].some(function(f) { return f.id === urlVal; }); 120 if (!alreadyAdded) { 121 var phantomOption = { 122 id: urlVal, 123 display: urlVal.split("/").pop(), // Use last segment as display 124 category: type 125 }; 126 $scope.facets[type].push(phantomOption); 127 // Also add to msOptions so the multiselect directive recognizes it 128 // and doesn't strip it from the selected values 129 $scope.msOptions[type].push(phantomOption); 130 } 131 }); 132 }); 133 134 $("form.component.newssearch.update-live").on("submit",function(event){ 135 event.preventDefault(); 136 }); 137 } 138 139 $scope.init(); 140 141 // Reload with URL params to apply filters 142 // Use $timeout to defer until after searchlist.js has added reloadWithUrlSearchParams method 143 if (hasSearchQueryParams()) { 144 $timeout(function() { 145 doReload(); 146 setTitle(); 147 }, 100); 148 } 149 150 //watch our facets for change, but wait a half second after a change 151 //NOTE: only reload if user has interacted OR if there were URL query params 152 $scope.timeout = $timeout(function(){}); 153 $scope.$watch('facets',function(newVal,oldVal){ 154 // Skip if init not complete or user hasn't interacted 155 if (firstLoad) { 156 firstLoad = false; 157 return; 158 } 159 console.log('$watch fired!', newVal); 160 //debounce so that it doesn't send a new search every keypress 161 $timeout.cancel($scope.timeout); //cancel the last timeout 162 $scope.timeout = $timeout(function(){ 163 console.log('Calling doReload after timeout'); 164 doReload(); 165 setTitle(); 166 }, 500); 167 },true) 168 169 $scope.hasFilters = function() { 170 var hasFilter = false; 171 if($scope.facets.authors.length > 0) { 172 hasFilter = true; 173 } 174 namespaces.forEach(function(type) { 175 if($scope.facets[type].length > 0) { 176 hasFilter = true; 177 } 178 }); 179 return hasFilter; 180 } 181 182 $("input[name='term']").on("keydown",function(event){ 183 if(event.keyCode == 13) { 184 doReload(); 185 return false;
186 } 187 }) 188 189 function setTitle() { 190 if($("body").attr("data-template") != "results") 191 return; 192 193 let tagsCombined = []; 194 namespaces.forEach(function (ns) { 195 tagsCombined = tagsCombined.concat($scope.facets[ns]); 196 }); 197 198 let newTitle = "Search Results " 199 200 let authors = $scope.facets.authors; 201 if(authors.length == 1 && tagsCombined.length == 0) { 202 newTitle = "Stories by " + authors[0].display + " -"; 203 } 204 if(authors.length == 0 && tagsCombined.length == 1) { 205 newTitle = "Stories tagged " + tagsCombined[0].display + " -" 206 } 207 208 document.title = document.title.replace(document.title.substr(0, document.title.indexOf("-")),newTitle) 209 210 } 211 setTitle() 212 213 function doReload() { 214 //get the id of the target 215 var targetID = $("#search-target").attr("data-search-target"); 216 //map the facets down to something we can use 217 218 //manualArticles= 219 // &term= 220 // &authors= 221 // &contacts= 222 // &displayDateRangeType= 223 // &displayDateStart= 224 // &displayDateEnd= 225 // &tags= 226 // &anyAll= 227 // &showEvents= 228 // &articlesExclude= 229 // &authorsExclude= 230 // &tagsExclude= 231 // &orderBy= 232 // &orderBySort= 233 // &limit=10 234 // &offset=30 235 let sendToSearch = new URLSearchParams(); 236 sendToSearch.set("submitted","submitted"); // Required for server to use URL params as filters 237 sendToSearch.set("force-search","true"); 238 sendToSearch.set("showEvents",$scope.facets.showEvents != null ? $scope.facets.showEvents : ""); 239 sendToSearch.set("term",$scope.facets.term != null ? $scope.facets.term : ""); 240 sendToSearch.set("displayDateStart",$scope.facets.displayDateStart != "" && $scope.facets.displayDateStart != null ? Date.parse($scope.facets.displayDateStart) : ""); 241 sendToSearch.set("displayDateEnd",$scope.facets.displayDateEnd != "" && $scope.facets.displayDateEnd != null ? Date.parse($scope.facets.displayDateEnd) : ""); 242 243 namespaces.forEach(function(ns) { 244 $scope.facets[ns].forEach(function(tag){ 245 // Send as "tags" param - ArticleListFromRequest.java reads "tags", not namespace-specific params 246 sendToSearch.append("tags", tag.id); 247 }); 248 }); 249 $scope.facets.authors.forEach(function(author){ 250 sendToSearch.append("authors",author.id); 251 }); 252 253 if(typeof $("#" + targetID)[0] != "undefined" && typeof $("#" + targetID)[0].reloadWithUrlSearchParams != "undefined") { 254 $("#" + targetID)[0].reloadWithUrlSearchParams(sendToSearch); 255 } 256 } 257 258 $scope.removeFacet = function(facet,category) { 259 $scope.facets[facet.category] = $scope.facets[facet.category].filter(function(el) { 260 return (el.id != facet.id); 261 }) 262 doReload(); 263 return false; 264 } 265 266 $scope.removeAllFacets = function() { 267 $scope.facets.authors=[]; 268 269 namespaces.forEach(function(ns){ 270 $scope.facets[ns] = []; 271 }) 272 273 doReload(); 274 return false; 275 } 276 } 277 }]) 278}( jQuery ));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.