1import { rgElement } from '../rgElement/rg-element.js'; 2 3const attachToggleHandler = function() { 4 let toggleIcons = document.querySelectorAll('*[data-toggle=' + this.tagName.toLowerCase() + ']'); 5 toggleIcons.forEach(function(toggleIcon) { 6 toggleIcon.addEventListener('click', function(ev) { 7 ev.preventDefault(); 8 this.toggleSearch(); 9 }.bind(this)); 10 toggleIcon.addEventListener('keydown', function(ev) { 11 if(ev.keyCode === 32) { 12 ev.preventDefault(); 13 this.toggleSearch(); 14 } 15 }.bind(this)); 16 }.bind(this)); 17 this.dialog?.addEventListener('click', function(ev) { 18 if(ev.target === this.dialog) { 19 this.hideSearch(); 20 } 21 }.bind(this) 22 ) 23 }, 24 sanitize = function(string) { 25 const map = { 26 '&': '&', 27 '<': '<', 28 '>': '>', 29 '"': '"', 30 "'": ''', 31 "/": '/', 32 }; 33 const reg = /[&<>"'/]/ig; 34 return string.replace(reg, (match) => (map[match])); 35 }, 36 prepareHTML = function() { 37 /** 38 * variables for templateHTML 39 */ 40 const adoptedCSS = this.getStyles('.rg-search', '.rg-search__wrapper'), 41 urlSearchParams = new URLSearchParams(window.location.search), 42 params = Object.fromEntries(urlSearchParams.entries()), 43 action = this.redirectTarget || '/suche', 44 variables = { 45 cssPath: this.rootpath + '/css/rg-search.css', 46 action: action, 47 placeholder: this.dataset.placeholder || 'Suchbegriff eingeben!', 48 searchTerm: sanitize(decodeURIComponent(params.q || '')), 49 customCss: this.dataset.customcss ? `<link rel="stylesheet" href="${this.dataset.customcss}">` : '', 50 adoptedCSS: adoptedCSS !== '' ? '<style>' + adoptedCSS + '</style>' : '', 51 buttonText: this.dataset.buttonText || 'SUCHE', 52 useDialog: this.parseAttribute(this.dataset.usedialog) === true, 53 toggleButton: this.parseAttribute(this.dataset.showtoggle) === false ? '' : `<a href="${action}" class="rg-search__trigger" part="trigger"><li class="fas fa-search" aria-hidden="true"></li></a>` 54 }; 55 return ` 56 <link rel="stylesheet" href="${variables.cssPath}"> 57 ${variables.customCss} 58 ${variables.adoptedCSS} 59 ${variables.toggleButton} 60 ${variables.useDialog === true ? `<dialog class="rg-search__dialog"><div class="rg-search__overlayclose" part="overlayclose"></div>` : 61 `<div class="rg-search__overlay" part="overlay"> 62 <div class="rg-search__overlayclose" part="overlayclose"></div> 63 </div>`} 64 <div class="rg-search__wrapper" part="wrapper"> 65 <div class="rg-search__description" part="description"><slot name="description"></slot></div> 66 <form class="rg-search__form" method="GET" action="${variables.action}" part="form"> 67 <input class="rg-search__input" name="q" placeholder="${variables.placeholder}" required="required" value="${variables.searchTerm}" part="input" role="searchbox" aria-label="Suchfeld"> 68 <button class="rg-search__button" type="submit" part="button" aria-label="Suche starten"><slot name="buttonContent">${variables.buttonText}</slot></button> 69 </form> 70 </div> 71 ${variables.useDialog === true ? `</dialog>` : ``} 72 ` 73 }, 74 prepareTemplates = function(rgSeachEl) { 75 const templates = { 76 header: null, 77 footer: null, 78 suggestion: null, 79 group: null, 80 loader: null, 81 notFound: null, 82 empty: null, 83 }; 84 rgSeachEl.querySelectorAll('template').forEach(tpl => { 85 if(tpl.dataset.for && templates.hasOwnProperty(tpl.dataset.for)) { 86 let html = tpl.innerHTML.replace(/{%/g, `\${`), 87 tplFn = 'return `' + html + '`'; 88 switch(tpl.dataset.for) { 89 case 'header': 90 case 'footer': 91 templates[tpl.dataset.for] = new Function('resultSet', tplFn); 92 break; 93 case 'suggestion': 94 templates[tpl.dataset.for] = new Function('item', 'resultSet', tplFn); 95 break; 96 case 'group': 97 templates[tpl.dataset.for] = new Function('groupName', 'resultSet', tplFn); 98 break; 99 case 'loader': 100 templates[tpl.dataset.for] = new Function(tplFn); 101 break; 102 case 'notFound': 103 templates[tpl.dataset.for] = new Function('resultSet', tplFn); 104 break; 105 case 'empty': 106 templates[tpl.dataset.for] = new Function('resultSet', tplFn); 107 break; 108 } 109 } 110 }); 111 for(const [ key, value ] of Object.entries(Object.assign({}, templates))) { 112 if(!value) { 113 delete templates[key]; 114 } 115 } 116 return templates; 117 }, 118 classNames = { 119 wrapper: 'rg-search__typeahead', /* main container element */ 120 input: 'tt-input', 121 hint: 'tt-hint', 122 highlight: 'tt-highlight', 123 list: 'tt-list', /* container element for suggestions */ 124 hide: 'tt-hide', 125 show: 'tt-show', 126 selected: 'tt-selected', 127 /* classes used within templates */ 128 header: 'tt-header', 129 footer: 'tt-footer', 130 loader: 'tt-loader', 131 suggestion: ["tt-suggestion"], 132 group: 'tt-group', 133 empty: 'tt-empty', 134 notFound: 'tt-notFound', 135 }; 136//evtl. https://github.com/digitalfortress-tech/typeahead-standalone 137let apiReady = false;
138 139/** 140 * the main class that gets exportet 141 */ 142export class rgSearch extends rgElement { 143 searchPanel = null; 144 searchInput = null; 145 toggleButton = null; 146 internalToggleButton = true; 147 alwaysOpen = false; 148 typeAhead = false; 149 overlayClose = null; 150 overlay = null; 151 dialog = null; 152 isVisible = false; 153 redirectTarget = '/suche' 154 155 constructor() { 156 super(); 157 } 158 159 connectedCallback() { 160 this.setUpElement(); 161 !this.shadowRoot && this.attachShadow({ mode: 'open' }); 162 this.shadowRoot.innerHTML = prepareHTML.call(this); 163 this.classList.remove('.rg-search'); 164 this.overlay = this.shadowRoot.querySelector('.rg-search__overlay'); 165 this.dialog = this.shadowRoot.querySelector('dialog.rg-search__dialog'); 166 this.overlayClose = this.shadowRoot.querySelector('.rg-search__overlayclose'); 167 this.overlayClose?.addEventListener('click', this.hideSearch.bind(this)); 168 if(this.parseAttribute(this.dataset.showoverlay) === true) { 169 if(this.overlay) { 170 this.overlay.addEventListener('click', this.hideSearch.bind(this)); 171 !this.dialog && (this.overlay.style.display = 'block'); 172 } 173 } 174 this.searchPanel = this.shadowRoot.querySelector('.rg-search__wrapper'); 175 this.classList.forEach(classname => { 176 if(classname !== 'rg-nav') { 177 this.searchPanel.classList.add(classname) 178 } 179 }) 180 this.searchInput = this.shadowRoot.querySelector('.rg-search__input'); 181 this.internalToggleButton = this.parseAttribute(this.dataset.showtoggle) === true; 182 this.alwaysOpen = this.parseAttribute(this.dataset.alwaysopen) === true; 183 this.redirectTarget = this.dataset.action || '/suche'; 184 this.typeAhead = this.parseAttribute(this.dataset.typeahead) === true; 185 if(this.typeAhead) { 186 this.setTypeAheadEnabled(true); 187 } 188 if(this.internalToggleButton) { 189 this.toggleButton = this.shadowRoot.querySelector('.rg-search__trigger'); 190 this.toggleButton.addEventListener('click', function(ev) { 191 ev.preventDefault(); 192 this.toggleSearch(); 193 }.bind(this)); 194 !this.dialog && this.overlay && (this.overlay.style.display = 'none'); 195 this.style.display = 'block'; 196 } else { 197 if(!this.alwaysOpen) { 198 this.style.display = 'none'; 199 } 200 } 201 if(this.parseAttribute(this.dataset.notoggle) !== true) { 202 if(document.readyState === 'complete') { 203 attachToggleHandler.call(this); 204 } else { 205 window.addEventListener('load', attachToggleHandler.bind(this)) 206 } 207 } 208 this.setupComplete(); 209 if(!this.alwaysOpen) { 210 document.addEventListener('rgnav:open', this.hideSearch.bind(this)); 211 } 212 if(this.dialog) { 213 this.dialog.addEventListener('close', this.hideSearch.bind(this)); 214 } 215 } 216 217 showSearch() { 218 this.isVisible = true; 219 if(!this.dialog) { 220 this.searchPanel.style.display = ''; 221 } else { 222 this.dialog.showModal(); 223 document.body.style.overflow = 'hidden'; 224 document.body.style.paddingRight = '17px'; 225 } 226 document.dispatchEvent(new CustomEvent('rgsearch:show', { 227 detail: { el: this } 228 })); 229 if(!this.internalToggleButton) { 230 this.style.display = 'block'; 231 } else { 232 if(!this.dialog && this.overlay && this.parseAttribute(this.dataset.showoverlay) === true) { 233 this.overlay.style.display = 'block'; 234 } 235 } 236 this.searchInput.focus(); 237 document.querySelectorAll('[data-inert]').forEach(el => el.setAttribute('inert', 'inert')); 238 } 239 240 hideSearch() { 241 this.isVisible = false; 242 if(!this.dialog) { 243 this.searchPanel.style.display = 'none'; 244 } else { 245 this.dialog.close(); 246 document.body.style.overflow = ''; 247 document.body.style.paddingRight = ''; 248 } 249 document.dispatchEvent(new CustomEvent('rgsearch:hide', { 250 detail: { el: this } 251 })); 252 if(!this.internalToggleButton) { 253 this.style.display = 'none'; 254 } else { 255 if(!this.dialog && this.overlay && this.parseAttribute(this.dataset.showoverlay) === true) { 256 this.overlay.style.display = 'none'; 257 } 258 } 259 document.querySelectorAll('[data-inert]').forEach(el => el.removeAttribute('inert')); 260 } 261 262 toggleSearch() { 263 if(!this.isVisible) { 264 this.showSearch(); 265 } else { 266 this.hideSearch(); 267 } 268 } 269 270 getInputField() { 271 return this.searchInput; 272 } 273 274 setTypeAheadEnabled(enabled) { 275 this.addEventListener('rg-search:apiloaded', function(ev) { 276 const typeAhead = ev.detail.defaultExport.default; 277 apiReady = true; 278 const templates = prepareTemplates(this); 279 typeAhead({ 280 input: this.searchInput, 281 templates: templates, 282 limit: 50, 283 diacritics: true, 284 source: { 285 remote: { 286 url: '/searchapi?mode=suggest&q=%QUERY', // OR "url: (inputQuery: string) => `https://remoteapi.com/${inputQuery}`" 287 wildcard: '%QUERY', 288 debounce: 500, // optional, default => 200ms 289 requestOptions: {} // optional, default => undefined 290 }, 291 //keys: [ "suggest" ], //if objectmode 292 }, 293 classNames: classNames, 294 hooks: { 295 sortByStartingLetter: (suggestions) => {/*do not sort*/}, 296 }, 297 }); 298 }.bind(this)); 299 this.extendApi('/api/rg-search-api.js'); 300 } 301}
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.