1/** 2 * @file 3 * Contains views_ajax_history.js. 4 */ 5 6(function ($, Drupal, drupalSettings) { 7 8 // Need to keep this to check if there are extra parameters in the original URL. 9 var original = { 10 path: window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '') + window.location.pathname, 11 // @TODO integrate #1359798 without breaking history.js 12 query: window.location.search || '' 13 }; 14 15 window.onpageshow = function (event) { 16 if (event.persisted) { 17 window.location.reload() 18 } 19 }; 20 21 Drupal.ViewsAjaxHistory = Drupal.ViewsAjaxHistory || {}; 22 23 /** 24 * Keep the original beforeSubmit method to use it later. 25 */ 26 Drupal.ViewsAjaxHistory.beforeSubmit = Drupal.Ajax.prototype.beforeSubmit; 27 28 /** 29 * Keep the original beforeSerialize method to use it later. 30 */ 31 Drupal.ViewsAjaxHistory.beforeSerialize = Drupal.Ajax.prototype.beforeSerialize; 32 33 /** 34 * Keep the original beforeSend method to use it later. 35 */ 36 Drupal.ViewsAjaxHistory.beforeSend = Drupal.Ajax.prototype.beforeSend; 37 38 Drupal.behaviors.viewsAjaxHistory = { 39 attach: function (context, drupalSettings) { 40 // Init the current page too, because the first loaded pager element do 41 // not have loadable history and will not work the back button. 42 if (once('views-ajax-history-first-page-load', 'body').length) { 43 drupalSettings.viewsAjaxHistory.onloadPageItem = drupalSettings.viewsAjaxHistory.renderPageItem; 44 } 45 } 46 }; 47 48 /** 49 * Modification of Drupal.Views.parseQueryString() to allow extracting multivalues fields. 50 * 51 * @param query 52 * String, either a full url or just the query string. 53 */ 54 var parseQueryString = function (query) { 55 var args = {}; 56 var pos = query.indexOf('?'); 57 if (pos != -1) { 58 query = query.substring(pos + 1); 59 } 60 var pairs = query.split('&'); 61 var pair, key, value; 62 for (var i in pairs) { 63 if (typeof(pairs[i]) == 'string') { 64 pair = pairs[i].split('='); 65 // Ignore the 'q' path argument, if present. 66 if (pair[0] != 'q' && pair[1]) { 67 key = decodeURIComponent(pair[0].replace(/\+/g, ' ')); 68 value = decodeURIComponent(pair[1].replace(/\+/g, ' ')); 69 // Field name ends with [], it's multivalues. 70 if (/\[\]$/.test(key)) { 71 if (!(key in args)) { 72 args[key] = [value]; 73 } 74 // Don't duplicate values. 75 else if (!$.inArray(value, args[key]) !== -1) { 76 args[key].push(value); 77 } 78 } 79 else { 80 args[key] = value; 81 } 82 } 83 } 84 } 85 return args; 86 }; 87 88 /** 89 * Strip views values and duplicates from URL. 90 * 91 * @param url 92 * String with the full URL to clean up. 93 * @param viewArgs 94 * Object containing field values from views. 95 * 96 * @return url 97 * String URL with views values and reduced duplicates. 98 */ 99 var cleanURL = function (url, viewArgs) { 100 var args = ('reset' in viewArgs) ? {} : parseQueryString(url); 101 var query = []; 102 103 // With clean urls off we need to add the 'q' parameter. 104 if (/\?/.test(drupalSettings.views.ajax_path)) { 105 query.push('q=' + Drupal.Views.getPath(url)); 106 } 107 108 $.each(args, function (name, value) { 109 // Use values from viewArgs if they exists. 110 if (name in viewArgs) { 111 value = viewArgs[name]; 112 } 113 if (Array.isArray(value)) { 114 $.merge(query, $.map($.unique(value), function (sub) { 115 return name + '=' + encodeURIComponent(sub); 116 })); 117 } 118 else { 119 query.push(name + '=' + encodeURIComponent(value)); 120 } 121 }); 122 123 url = window.location.href.split('?'); 124 return url[0] + (query.length ? '?' + query.join('&') : ''); 125 }; 126 127 /** 128 * Remove the functions from the state. They can't been pushed into the history. 129 * 130 * @param state 131 * Object containing the state to be cleaned. 132 * 133 * @return state 134 * Object that has been cleaned up. 135 */ 136 var cleanStateForHistory = function (state) { 137 var stateWithNoFunctions = {}; 138 for (var key in state) { 139 if (typeof state[key] !== "function") { 140 stateWithNoFunctions[key] = state[key]; 141 } 142 } 143 return stateWithNoFunctions; 144 }; 145 146 /** 147 * Parse a URL query string. 148 * 149 * @param queryString 150 * String containing the query to parse. 151 */ 152 var parseQuery = function (queryString) { 153 var query = {}; 154 $.map(queryString.split('&'), function (val) { 155 var s = val.split('='); 156 query[s[0]] = s[1]; 157 }); 158 return query; 159 }; 160 161 /** 162 * Unbind 'popstate' when adding a new state to avoid an infinite loop. 163 * 164 * We only use the 'popstate' event to trigger refresh on back of forward click. 165 * 166 * @param options 167 * Object containing the values from views' AJAX call. 168 * @param url 169 * String with the current URL to be cleaned up. 170 */ 171 var addState = function (options, url) { 172 // The data in the history state must be serializable. 173 var historyOptions = $.extend({}, options); 174 175 // Store the actual view's dom id. 176 drupalSettings.viewsAjaxHistory.lastViewDomID = options.data.view_dom_id; 177 $(window).unbind('popstate', loadView); 178 history.pushState(cleanStateForHistory(historyOptions), document.title, cleanURL(url, options.data)); 179 $(window).bind('popstate', loadView); 180 }; 181 182 /** 183 * Make an AJAX request to update the view when navigating back and forth. 184 */ 185 var loadView = function () { 186 var options; 187 188 // This should be the first loaded page, so init the options object. 189 if (history.state === null) { 190 var viewsAjaxSettingsKey = 'views_dom_id:' + drupalSettings.viewsAjaxHistory.lastViewDomID; 191 if (drupalSettings.views.ajaxViews.hasOwnProperty(viewsAjaxSettingsKey)) { 192 var viewsAjaxSettings = drupalSettings.views.ajaxViews[viewsAjaxSettingsKey]; 193 var initialExposedInput = drupalSettings.viewsAjaxHistory.initialExposedInput[viewsAjaxSettingsKey]; 194 $.extend(viewsAjaxSettings,initialExposedInput); 195 viewsAjaxSettings.page = drupalSettings.viewsAjaxHistory.onloadPageItem; 196 options = { 197 data: viewsAjaxSettings, 198 url: drupalSettings.views.ajax_path 199 }; 200 } 201 } 202 else { 203 options = history.state; 204 } 205 206 // Drupal's AJAX options. 207 var settings = $.extend({ 208 submit: options.data, 209 setClick: true, 210 event: 'click', 211 selector: '.view-dom-id-' + options.data.view_dom_id, 212 progress: { type: 'throbber' }, 213 httpMethod: 'GET', 214 }, options); 215 216 var viewsAjaxSubmit = Drupal.ajax(settings);
217 // Trigger ajax call. 218 viewsAjaxSubmit.execute(); 219 }; 220 221 /** 222 * Check to see if facets are enabled for the view. 223 * 224 * @param viewName string 225 * 226 * return bool 227 * Whether the view has facets. 228 */ 229 var hasFacets = function (viewName) { 230 var hasFacets = false; 231 if (drupalSettings.facets_views_ajax) { 232 // Loop through the facets. 233 $.each(drupalSettings.facets_views_ajax, function (facetId, facetSettings) { 234 if (facetSettings.view_id === viewName) { 235 // Yes, facets are enabled for this view. 236 hasFacets = true; 237 } 238 }); 239 } 240 241 return hasFacets; 242 } 243 244 /** 245 * Override beforeSerialize to handle click on pager links. 246 * 247 * @param $element 248 * jQuery DOM element 249 * @param options 250 */ 251 Drupal.Ajax.prototype.beforeSerialize = function (element, options) { 252 // Check that we handle a click on a link, not a form submission. 253 if (options.data.view_name && element && $(element).is('a')) { 254 let params = new URLSearchParams($(element).attr('href')); 255 if (!$.isEmptyObject(drupalSettings.viewsAjaxHistory.excludeArgs)) { 256 var keysToRemove = []; 257 $.each(drupalSettings.viewsAjaxHistory.excludeArgs, function (index, pathToExclude) { 258 params.forEach(function (value, key, parent) { 259 if (key.startsWith(pathToExclude)) { 260 keysToRemove.push(key); 261 } 262 }); 263 }); 264 keysToRemove.forEach(function (key) { 265 params.delete(key) 266 }); 267 } 268 addState(options, '?' + params.toString()); 269 } 270 271 // Call the original Drupal method with the right context. 272 Drupal.ViewsAjaxHistory.beforeSerialize.apply(this, arguments); 273 }; 274 275 /** 276 * Override beforeSubmit to handle exposed form submissions. 277 * 278 * @param form_values 279 * Object with all field values. 280 * @param element 281 * jQuery DOM form element. 282 * @param options 283 * Object containing AJAX options. 284 */ 285 Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) { 286 if (options && options.data && options.data.view_name) { 287 var url = original.path + '?' + new URLSearchParams(new FormData(element.get(0))).toString(); 288 var currentQuery = parseQueryString(window.location.href); 289 290 // Prepare ajax url 291 var ajaxUrl = options.url.split('?')[0]; 292 var ajaxQuery = parseQueryString(options.url); 293 294 // Remove the page number from the query string, as a new filter has been 295 // applied and should return new results. 296 if ($.inArray("page", Object.keys(currentQuery)) !== -1) { 297 delete currentQuery.page; 298 delete ajaxQuery.page; 299 } 300 301 // Copy selected values in history state. 302 $.each(form_values, function () { 303 // Field name ending with [] is a multi value field. 304 if (/\[\]$/.test(this.name)) { 305 if (!options.data[this.name]) { 306 options.data[this.name] = []; 307 } 308 options.data[this.name].push(this.value); 309 } 310 // Regular field. 311 else { 312 options.data[this.name] = this.value; 313 } 314 }); 315 // Remove exposed data from the current query to leave behind any 316 // non exposed form related query vars. 317 element.find('[name]').each(function () { 318 if (currentQuery[this.name]) { 319 delete currentQuery[this.name]; 320 delete ajaxQuery[this.name]; 321 } 322 }); 323 324 // If the exposed form has checkboxes, we need to check if these are 325 // unchecked and if so, remove them from the url 326 element.find('input[type="checkbox"]').each(function (key, value) { 327 if (!form_values[this.name]) { 328 if (currentQuery[this.name]) { 329 delete currentQuery[this.name]; 330 } 331 else if (options.data[this.name]) { 332 delete options.data[this.name]; 333 } 334 if (ajaxQuery[this.name]) { 335 delete ajaxQuery[this.name]; 336 } 337 } 338 }); 339 340 // Helper function to remove multi-value query keys that match a given 341 // name 342 let removeMultiValueQueryKeys = function (multiValueParamToRemove, queryParams) {
343 Object.getOwnPropertyNames(queryParams).forEach(function (queryKey) { 344 let queryKeyWithoutBracket = queryKey.replace(/\[\d+]$/, ''); 345 if (multiValueParamToRemove === queryKeyWithoutBracket) { 346 delete queryParams[queryKey]; 347 } 348 }); 349 return queryParams; 350 }; 351 352 // If the exposed form has a multiple select. 353 element.find('select[multiple]').each(function (key, value) { 354 if ($(value).val().length === 0) { 355 delete options.data[this.name]; 356 delete currentQuery[this.name]; 357 delete ajaxQuery[this.name]; 358 } 359 // Pagers creates query params that are indexed like this: 360 // ?someparam[0]=123,someparam[1]=456 361 // instead of this: 362 // ?someparam[]=123&someparam[]=456 363 // We need to clear them out. The submitted form values will use the 364 // non-indexed versions, and we can't have the indexed versions creating 365 // a conflict. 366 let nameWithoutBracket = this.name.replace(/\[]$/, ''); 367 currentQuery = removeMultiValueQueryKeys(nameWithoutBracket, currentQuery); 368 ajaxQuery = removeMultiValueQueryKeys(nameWithoutBracket, ajaxQuery); 369 }); 370 371 url += (/\?/.test(url) ? '&' : '?') + $.param(currentQuery); 372 // Update options with updated ajax url. 373 options.url = ajaxUrl + '?' + $.param(ajaxQuery); 374 addState(options, url); 375 } 376 // Call the original Drupal method with the right context. 377 Drupal.ViewsAjaxHistory.beforeSubmit.apply(this, arguments); 378 }; 379 380 /** 381 * Override beforeSend to clean up the Ajax submission URL. 382 * 383 * @param {XMLHttpRequest} xmlhttprequest 384 * Native Ajax object. 385 * @param {object} options 386 * jQuery.ajax options. 387 */ 388 Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) { 389 var data = (typeof options.data === 'string') ? parseQuery(options.data) : {}; 390 391 if (data.view_name && options.type !== 'GET') { 392 if (hasFacets(data.view_name) === true) { 393 var currentQuery = parseQueryString(window.location.href); 394 options.url = drupalSettings.views.ajax_path + '?' + $.param(currentQuery) + '&' + Drupal.ajax.WRAPPER_FORMAT + '=drupal_ajax'; 395 } 396 else { 397 // Override the URL to not contain any fields that were submitted. 398 var delimiter = drupalSettings.views.ajax_path.indexOf('?') === -1 ? '?' : '&'; 399 options.url = drupalSettings.views.ajax_path + delimiter + Drupal.ajax.WRAPPER_FORMAT + '=drupal_ajax'; 400 } 401 } 402 // Call the original Drupal method with the right context. 403 Drupal.ViewsAjaxHistory.beforeSend.apply(this, arguments); 404 } 405 406})(jQuery, Drupal, drupalSettings);
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.