PageSourceSearch

https://www.elkem.com/Script/coveo/search-box.js?v=3

js elkem.com collected 2026-10-02 03:16:25 UTC 11,708 bytes, 387 lines download raw bytes

1class SearchBox extends HTMLElement {
2  static get observedAttributes() {
3    return ['api-key', 'api-host', 'search-hub', 'max-results', 'redirect-url', 'class'];
4  }
5
6  constructor() {
7    super();
8    this.attachShadow({ mode: 'open' });
9    this.timer = null;
10    this.highlightedIndex = -1;
11    this.suggestions = [];
12
13    // Defaults
14    this.apiKey = '';
15    this.apiHost = 'https://platform.cloud.coveo.com';
16    this.searchHub = 'default';
17    this.maxResults = 5;
18    this.redirectUrl = '/';
19
20    this.shadowRoot.innerHTML = `
21      <style>
22        .search-container {
23          position: relative;
24          display: flex;
25          flex-direction: row;
26          flex-wrap: nowrap;
27          width: 100%;
28          max-width: 520px;
29          border-radius: 0.5rem;
30          border: 1px solid #e5e8e8;
31          background-color: #fff;
32
33          &:focus-within {
34            box-shadow: 0 0 0 3px rgba(220, 36, 29, 0.3);
35            border-color: #dc241d;
36          }
37        }
38        input {
39          outline: transparent;
40          width: 100%;
41          max-width: 842px;
42          height: 100%;
43          flex-grow: 1;
44          border: 0px;
45          border-radius: .5rem 0 0 .5rem;
46          padding: 0.875rem 1rem;
47          background-color: transparent;
48          font-size: 1rem;
49          color: #626971;
50
51          @media (max-width: 991px) {
52            padding-top: 9px;
53            padding-bottom: 9px;
54          }
55          &:focus + .suggestions {
56            transition: .15s background ease-in-out;
57            background-color: white;
58            border: 1px solid #ccc;
59            border-radius: 0.5rem;
60
61            li {
62              opacity: 1;
63              transition: .2s opacity ease-in-out;
64            }
65          }
66        }
67        button {
68          cursor: pointer;
69          display: flex;
70          justify-content: center;
71          margin: auto 0;
72          border: 0;
73          padding: 1rem;
74          background: transparent;
75
76          svg {
77            margin: auto;
78            width: 1rem;
79            height: 1rem;
80          }
81          &:focus-visible {
82            outline: #ec1211 auto 1px;
83          }
84          &.hidden {
85            display:none;
86          }
87        }
88        .btn-search {
89          margin: -1px;
90          border-radius: 0 .5rem .5rem 0;
91          background-color: #ec1211;
92          color:#fff;
93        }
94        .btn-reset {
95          margin-right: .375rem;
96          padding: 0.75rem;
97          color: #626971;
98
99          svg {
100            width:0.75rem;
101            height:0.75rem;
102          }
103          &:hover {
104            color: #ec1211;
105          }
106        }
107        .startpage-search {
108          &.search-container  {
109            max-width: unset;
110            border-color: transparent;
111          }
112          .btn-search {
113            color: #000;
114            background-color: #7aea97;
115          }
116        }
117        ul.suggestions {
118          position: absolute;
119          top: 100%;
120          left: 0;
121          right: 0;
122          max-height: 150px;
123          overflow-y: auto;
124          list-style: none;
125          margin: 0;
126          padding: 0;
127          z-index: 10000;
128          transition: .2s background ease-in-out;
129        }
130        ul.suggestions li {
131          cursor: pointer;
132          min-height: 40px;
133          display: flex;
134          flex-direction: row;
135          flex-wrap: nowrap;
136          gap: 0.5rem;
137          transition: .2s opacity ease-in-out;
138          opacity: 0;
139          padding: 0 1rem;
140          color: #626971;
141
142          &.highlighted {
143            background-color: #ddd;
144          }
145          &:hover {
146            background-color: #eee;
147          }
148          svg {
149            margin: auto 0;
150            aspect-ratio: 1/1;
151            width:1rem;
152            min-width: 1rem;
153            height:1rem;
154          }
155          span {
156            margin: auto 0;
157            overflow: hidden;
158            display: -webkit-box;
159            -webkit-box-orient: vertical;
160            -webkit-line-clamp: 2;
161            white-space: normal;
162          }
163        }
164      </style>
165      <div class="search-container">
166        <input name="global-search" type="text" placeholder="What are you looking for?" />
167        <ul class="suggestions"></ul>
168        <button class="btn-reset hidden" aria-label="Clear">
169            <svg fill="currentColor" viewBox="0 0 18 18">
170              <path d="m18 2-1.8-2-7.1 7.1-7.1-7.1-2 2 7.1 7.1-7.1 7.1 2 1.8 7.1-6.9 7.1 6.9 1.8-1.8-6.9-7.1z"></path>
171            </svg>
172        </button>
173        <button class="btn-search" aria-label="Search">
174          <svg fill="currentColor" viewBox="0 0 16 16">
175            <path d="m6.4 0c3.5 0 6.4 2.9 6.4 6.4 0 1.4-.4 2.7-1.2 3.7l4 4c.4.4.4 1 .1 1.5l-.1.1c-.2.2-.5.3-.8.3s-.6-.1-.8-.3l-4-4c-1 .7-2.3 1.2-3.7 1.2-3.4-.1-6.3-3-6.3-6.5s2.9-6.4 6.4-6.4zm
1750 2.1c-2.3 0-4.3 1.9-4.3 4.3s1.9 4.3 4.3 4.3 4.3-1.9 4.3-4.3-1.9-4.3-4.3-4.3z"></path>
176          </svg>
177        </button>
178      </div>
179    `;
180  }
181
182  connectedCallback() {
183    this.input = this.shadowRoot.querySelector('input');
184    this.suggestionList = this.shadowRoot.querySelector('.suggestions');
185    this.buttonReset = this.shadowRoot.querySelector('button.btn-reset');
186    this.buttonSearch = this.shadowRoot.querySelector('button.btn-search');
187
188    this.input.addEventListener('input', this.handleInput.bind(this));
189    this.input.addEventListener('keydown', this.handleKeyDown.bind(this));
190    this.input.addEventListener('focus', this.handleFocus.bind(this));
191    this.buttonReset.addEventListener('click', () => {
192      this.buttonReset.classList.add('hidden')
193      this.input.value = '';
194      this.input.focus();
195    });
196    this.buttonSearch.addEventListener('click', () => this.emitSearch(this.input.value));
197    this.input.addEventListener('blur', this.handleBlur.bind(this));
198    this.fetchSuggestions();
199  }
200
201  attributeChangedCallback(name, oldVal, newVal) {
202    switch (name) {
203      case 'api-key':
204        this.apiKey = newVal;
205        break;
206      case 'api-host':
207        this.apiHost = newVal;
208        break;
209      case 'search-hub':
210        this.searchHub = newVal;
211        break;
212      case 'max-results':
213        this.maxResults = parseInt(newVal, 10) || 5;
214        break;
215      case 'redirect-url':
216        this.redirectUrl = newVal;
217        break;
218      case 'class':
219        this.shadowRoot.querySelector('.search-container').classList.add(newVal)
220        break;
221    }
222  }
223
224  async handleFocus() {
225    this.highlightedIndex = -1;
226    this.prevQuery = '';
227    this.suggestions = await this.fetchSuggestions('');
228    this.renderSuggestions(this.suggestions);
229  }
230
231  async handleInput(event) {
232    const query = event.target.value.trim();
233    if (query.length > 0) this.buttonReset.classList.remove('hidden')
234    else this.buttonReset.classList.add('hidden')
235    this.prevQuery = query;
236    this.highlightedIndex = -1;
237
238    if (this.timer) clearTimeout(this.timer);
239
240    this.timer = setTimeout(async () => {
241      if (query.length === 0) {
242        this.renderSuggestions([]);
243        return;
244      }
245
246      this.suggestions = await this.fetchSuggestions(query);
247      this.renderSuggestions(this.suggestions);
248    }, 300);
249  }
250
251  handleBlur() {
252    setTimeout(() => {
253      this.renderSuggestions([]);
254    }, 100); // Enough time to allow clicks
255  }
256
257  handleKeyDown(event) {
258    const maxIndex = this.suggestions.length - 1;
259
260    switch (event.key) {
261      case 'ArrowDown':
262        event.preventDefault();
263        this.highlightedIndex = (this.highlightedIndex + 1) > maxIndex ? -1 : this.highlightedIndex + 1;
264        this.input.value = this.suggestions[this.highlightedIndex] || this.prevQuery;
265        this.renderSuggestions(this.suggestions);
266        break;
267
268      case 'ArrowUp':
269        event.preventDefault();
270        this.highlightedIndex = (this.highlightedIndex - 1) < -1 ? maxIndex : (this.highlightedIndex - 1) < 0 ? -1 : this.highlightedIndex - 1;
271        this.input.value = this.suggestions[this.highlightedIndex] || this.prevQuery;
272        this.renderSuggestions(this.suggestions);
273        break;
274
275      case 'Enter':
276        event.preventDefault();
277        if (this.highlightedIndex >= 0 && this.suggestions[this.highlightedIndex]) {
278          this.selectSuggestion(this.suggestions[this.highlightedIndex]);
279        } else {
280          this.emitSearch(this.input.value);
281        }
282        break;
283    }
284  }
285
286  async fetchSuggestions(query) {
287    if (!this.apiKey || !this.apiHost) {
288      console.warn('Missing api-key or api-host');
289      return [];
290    }
291
292    try {
293      const response = await fetch(`${this.apiHost}/rest/search/v2/querySuggest`, {
294        method: 'POST',
295        headers: {
296          'Content-Type': 'application/json',
297          'Authorization': `Bearer ${this.apiKey}`
298        },
299        body: JSON.stringify({
300          q: query,
301          searchHub: this.searchHub,
302          count: this.maxResults
303        })
304      });
305
306      const data = await response.json();
307      return data.completions?.map(item => item.expression) || [];
308
309    } catch (err) {
310      console.error('Coveo API error:', err);
311      return [];
312    }
313  }
314
315  async fetchRedirects(query) {
316    if (!this.apiKey || !this.apiHost) {
317      console.warn('Missing api-key or api-host');
318      return [];
319    }
320
321    try {
322      const response = await fetch(`${this.apiHost}/rest/search/v2/plan`, {
323        method: 'POST',
324        headers: {
325          'Content-Type': 'application/json',
326          'Authorization': `Bearer ${this.apiKey}`
327        },
328        body: JSON.stringify({
329          q: query,
330          searchHub: this.searchHub,
331          count: this.maxResults
332        })
333      });
334
335      const data = await response.json();
336      return data?.preprocessingOutput?.triggers || [];
337
338    } catch (err) {
339      console.error('Coveo API error:', err);
340      return [];
341    }
342
343  }
344
345  renderSuggestions(suggestions) {
346    this.suggestionList.innerHTML = '';
347    suggestions.forEach((suggestion, index) => {
348      const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
349      iconPath.setAttribute('fill', 'currentColor');
350      iconPath.setAttribute('d', 'm6.4 0c3.5 0 6.4 2.9 6.4 6.4 0 1.4-.4 2.7-1.2 3.7l4 4c.4.4.4 1 .1 1.5l-.1.1c-.2.2-.5.3-.8.3s-.6-.1-.8-.3l-4-4c-1 .7-2.3 1.2-3.7 1.2-3.4-.1-6.3-3-6.3-6.5s2.9-6.4 6.4-6.4zm
3500 2.1c-2.3 0-4.3 1.9-4.3 4.3s1.9 4.3 4.3 4.3 4.3-1.9 4.3-4.3-1.9-4.3-4.3-4.3z');
351      const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
352      icon.appendChild(iconPath)
353      const contentSpan = document.createElement('span');
354      contentSpan.textContent = suggestion;
355      const li = document.createElement('li');
356      li.appendChild(icon)
357      li.appendChild(contentSpan)
358      if (index === this.highlightedIndex) {
359        li.classList.add('highlighted');
360      }
361      li.addEventListener('mousedown', this.selectSuggestion.bind(this));
362      this.suggestionList.appendChild(li);
363    });
364  }
365
366  selectSuggestion(suggestion) {
367    this.input.value = typeof suggestion == "string" ? suggestion : suggestion.target.innerText;
368    this.renderSuggestions([]);
369    this.suggestions = [];
370    this.highlightedIndex = -1;
371    this.emitSearch(typeof suggestion == "string" ? suggestion : suggestion.target.innerText);
372  }
373
374  async emitSearch(query) {
375    this.input.value = '';
376    let redirects = await this.fetchRedirects(query);
377    if (redirects.length > 0) {
378      redirects = redirects.filter(r => r.type == 'redirect')
379      window.location.href = redirects[0].content;
380    }
381    else {
382      window.location.href = `${this.redirectUrl}#q=${encodeURIComponent(query)}`;
383    }
384  }
385}
386
387customElements.define('search-box', SearchBox);

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.