1/** 2 * @file 3 * Drupal behaviors attached to cola_search module. 4 * 5 */ 6 7/** 8 * Cola Search scripts including Google Custom Search Engine Overrides. 9 * 10 * @type {Drupal~behavior} 11 * 12 * @prop {Drupal~behaviorAttach} attach 13 * Adjusts markup generated by the Google Custom Search for accessibility. 14 */ 15(function ($, Drupal, window, document, undefined) { 16 'use strict'; 17 18 Drupal.behaviors.colaSearch = { 19 attach: function (context, settings) { 20 if (typeof settings.colaSearch === 'undefined') { 21 // Settings are required. 22 return; 23 } 24 if (typeof settings.colaSearch.searchEngineId === 'undefined') { 25 // Nothing to do if we don't have a search engine id. 26 return; 27 } 28 if (typeof settings.colaSearch.searchElements === 'undefined') { 29 // Nothing to do if we don't have any search elements to render. 30 return; 31 } 32 // Make sure refinements is defined, default to false (hidden); 33 settings.colaSearch.refinements = settings.colaSearch.refinements || false; 34 35 /** 36 * Theme GCSE search results list. 37 */ 38 Drupal.theme.prototype.colaSearchGcseResults = function (items, baseClass, stateSuffix) { 39 if (items.length > 0) {
40 var $itemsContainer = $('<div></div>') 41 .addClass(baseClass) 42 .addClass(baseClass + '--' + stateSuffix); 43 var $itemsList = $('<ul></ul>', { class: baseClass + '__list' }); 44 for (var i = 0; i < items.length; i++) { 45 var $item = $('<li></li>'); 46 var $itemAnchor = $('<a/>', { 47 href: items[i].actualUrl 48 }); 49 $itemAnchor.append('<h2>' + items[i].title + '</h2>'); 50 $item.append($itemAnchor); 51 $itemsList.append($item); 52 } 53 $itemsContainer.append($itemsList); 54 } 55 return $itemsContainer; 56 } 57 58 /** 59 * Configuration for our mutation observers. 60 * 61 * The options childList and subtree configure the observer to watch for 62 * children and descendants of children of the observed node respectively. 63 * 64 * @see https://davidwalsh.name/mutationobserver-api 65 */ 66 var observerConfig = { childList: true, subtree: true }; 67 68 /** 69 * Function to make changes to markup when search elements change. 70 */ 71 var reactToMutation = function (mutations, observer, container) { 72 // Remove observer while we make changes to the markup, so we don't 73 // trigger reactions to our own changes. 74 observer.disconnect(); 75 for (var i = 0; i < mutations.length; i++) { 76 var $mutation = $(mutations[i].target); 77 var nodeAdded = mutations[i].addedNodes.length; 78 var addedNode = mutations[i].addedNodes[0]; 79 var searchBoxOnly = $(container).hasClass('cb-gcse-container--searchbox-only'); 80 81 // Add search role to search form. 82 // @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Search_role 83 if ($mutation.find('form.gsc-search-box').length) { 84 $mutation.find('form.gsc-search-box').attr('role', 'search'); 85 } 86 87 // Add aria-labels to fix accessibility. 88 // Title attribute alone is not sufficient for a11y 89 // @see https://developer.paciellogroup.com/blog/2012/01/html5-accessibility-chops-title-attribute-use-and-abuse/ 90 if ($mutation.find('.gsc-input').length) { 91 // Add aria label to form search input element. 92 $mutation.find('input.gsc-input') 93 .attr('aria-label', 'Search') 94 .attr('placeholder', 'Enter search terms'); 95 // Add aria label to the clear search link. 96 $mutation.find('a.gsst_a').attr('aria-label', 'Reset search'); 97 } 98 99 // Adjustments to search results markup. 100 if (!searchBoxOnly) { 101 // Hide refinements as soon as they are added, if site is not 102 // configured to show them. 103 if (!settings.colaSearch.refinements) { 104 if ($mutation.hasClass('gsc-tabsAreaInvisible')) { 105 $('.gsc-tabsAreaInvisible').hide(); 106 } 107 if ($mutation.hasClass('gsc-tabsArea')) { 108 $('.gsc-tabsArea').hide(); 109 } 110 } 111 112 // Announce result info if present and has just been added. 113 if ($mutation.hasClass('gsc-result-info') && mutations[i].addedNodes.length) { 114 var infoText = $('.gsc-result-info').text(); 115 var announceText =''; 116 if ($('.gsc-cursor-current-page').length) { 117 announceText = 'You are on page '; 118 announceText += $('.gsc-cursor-current-page').text() + ' of '; 119 announceText += infoText; 120 } 121 else { 122 announceText = 'Listing ' + infoText; 123 } 124 Drupal.announce(Drupal.t(announceText)); 125 } 126 // If the mutation record includes changes to the 'no result' div, 127 // check to see if nodes were added and if the added node was a 128 // no result message. Finally, announce it via drupal announce. 129 if (nodeAdded && addedNode.childNodes.length) { 130 if ($(addedNode.childNodes).hasClass('gs-no-results-result')) { 131 Drupal.announce(Drupal.t($('.gs-no-results-result').find('.gs-snippet').text())); 132 } 133 } 134 135 // Change pagination markup to match accessible pattern. 136 // @see https://www.a11ymatters.com/pattern/pagination/ 137 if ($mutation.find('.gsc-cursor-box').length) { 138 var $pagination = $mutation.find('.gsc-cursor-box'); 139 140 // Modify the pagination to fit the pattern if we haven't done so 141 // already, otherwise, just make sure thet the current page is 142 // marked properly. 143 if (!$pagination.find('nav').length) { 144 var $paginationWrappers = $('<nav/>', { 145 'class': 'cb-gsce-pagination', 146 'role': 'navigation', 147 'aria-label': 'Pagination Navigation', 148 'html': '<ul></ul>' 149 }); 150 $pagination.find('.gsc-cursor > .gsc-cursor-page') 151 .attr('role', 'button') 152 .attr('aria-label', function() {return 'Go to page ' + $(this).text();}) 153 .wrap('<li class="cb-gsce-pagination__item">'); 154 $pagination.find('.gsc-cursor .gsc-cursor-current-page') 155 .attr('aria-label', function() {return 'Current Page, Page ' + $(this).text();}) 156 .attr('aria-current', 'true'); 157 $pagination.find('.gsc-cursor > .cb-gsce-pagination__item') 158 .wrapAll($paginationWrappers); 159 } 160 else { 161 $pagination.find('.gsc-cursor .gsc-cursor-page') 162 .attr('aria-label', function() {return 'Go to page ' + $(this).text();}) 163 .removeAttr('aria-current', ''); 164 $pagination.find('.gsc-cursor .gsc-cursor-current-page') 165 .attr('aria-label', function() {return 'Current Page, Page ' + $(this).text();}) 166 .attr('aria-current', 'true'); 167 } 168 } 169 170 } 171 } 172 173 // Re-enable observer after updating markup. 174 observer.observe(container, observerConfig); 175 }; 176 177 /** 178 * Loop through the search elements and initialize them once. 179 */ 180 $.each(settings.colaSearch.searchElements, function(elementId, gcseOptions) { 181 $('#' + elementId, context).once('cola-search-init', function() { 182 var searchElement = document.getElementById(elementId); 183 var observer = new MutationObserver(function(mutations, observer) { 184 reactToMutation(mutations, observer, searchElement); 185 }); 186 observer.observe(searchElement, observerConfig); 187 }); 188 }); 189 190 /** 191 * GCSE initialization callback to render search elements. 192 */ 193 var renderSearchElements = function() { 194 if (document.readyState == 'complete') { 195 // Document is ready when CSE element is initialized. 196 $.each(settings.colaSearch.searchElements, function(elementId, gcseOptions) { 197 google.search.cse.element.render(gcseOptions); 198 }); 199 } 200 else { 201 // Document is not ready yet, when CSE element is initialized. 202 google.setOnLoadCallback(function() { 203 $.each(settings.colaSearch.searchElements, function(elementId, gcseOptions) { 204 google.search.cse.element.render(gcseOptions); 205 }); 206 }, true); 207 } 208 }; 209 210 /** 211 * Web search results have been rendered callback for GCSE. 212 * 213 * Invoked immediately before the Search Element JavaScript renders the 214 * page footer. 215 */ 216 var webSearchResultsRendered = function(gname, query, promos, results) { 217 // @see https://developers.google.com/custom-search/docs/element#callbacks 218 var $promoImages = $(promos).find('.gs-promotion-table-snippet-with-image img'); 219 var $resultImages = $(results).find('.gsc-thumbnail img'); 220 var $allImages= $promoImages.add($resultImages); 221 var $resultTitleLinks = $(results).find('a.gs-title'); 222 223 // Promoted results are limited to 60x60px. 224 // https://support.google.com/customsearch/answer/2631074?hl=en 225 // Let's retrieve a size equal to the size of the other search results. 226 $promoImages.each(function() { 227 var src = this.src; 228 if (typeof src === 'string') { 229 src = src.replace(/w\d+\-h\d+$/gi, 'w150-h150'); 230 this.src = src; 231 } 232 }); 233 // Remove anchors around images from tabindex and hide them from 234 // screen readers via attributes to avoid link duplication and empty 235 // anchors (the images don't have alt tags). 236 $allImages.each(function() { 237 if ($(this).parent().is('a')) { 238 $(this).parent().attr({ 239 'aria-hidden': 'true', 240 'tabindex': '-1', 241 'role': 'presentation' 242 }); 243 } 244 // Add a class to the result container for theming. 245 $(this).parents('.gsc-result').addClass('cb-gsc-has-thumbnail'); 246 }); 247 248 // Wrap result links in an h2. Idealy we would want the results to 249 // be in a list, but those changes seem too disruptive. 250 $resultTitleLinks.each(function() { 251 if (!$(this).parent().is('h2')) { 252 $(this).wrap('<h2></h2>'); 253 } 254 }); 255 256 }; 257 258 /** 259 * Initialize GCSE on initial document load. 260 * 261 * Note this prevents search box rendering after editing with IPE. Since 262 * only developers should be modifying search panes we'll not worry 263 * about that right now. 264 */ 265 $('body', context).once('cola-search-init', function() { 266 267 // Insert it before the CSE code snippet so that cse.js can take the 268 // script parameters, like parsetags, callbacks. 269 window.__gcse = { 270 parsetags: 'explicit',
271 initializationCallback: renderSearchElements, 272 searchCallbacks: { 273 web: { 274 rendered: webSearchResultsRendered 275 } 276 } 277 }; 278 // CSE code snippet initializes google custom search. 279 (function() { 280 var cx = settings.colaSearch.searchEngineId; 281 var gcse = document.createElement('script'); 282 gcse.type = 'text/javascript'; 283 gcse.async = true; 284 gcse.src = 'https://cse.google.com/cse.js?cx=' + cx; 285 gcse.setAttribute('nonce', settings.colaSearch.nonce); 286 var s = document.getElementsByTagName('script')[0]; 287 s.parentNode.insertBefore(gcse, s); 288 })(); 289 290 }); 291 292 } 293 }; 294})(jQuery, Drupal, this, this.document); 295;
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.