PageSourceSearch

https://www.rheine.de/_/scripts/rgElements/src/Elements/rgSearch/rg-search.js

js rheine.de collected 2026-10-01 12:33:05 UTC 12,304 bytes, 301 lines download raw bytes

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            '<': '&lt;',
28            '>': '&gt;',
29            '"': '&quot;',
30            "'": '&#x27;',
31            "/": '&#x2F;',
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.