PageSourceSearch

https://bikeindex.org/assets/hotwire_combobox.esm-4266156bf16a37f7…067b58b62282d8f3f540bc384369ebaa3e4bb9b5.js

js bikeindex.org collected 2026-10-01 10:23:37 UTC 51,960 bytes, 1,841 lines download raw bytes

1/*!
2HotwireCombobox 0.4.1
3*/
4import { Controller } from '@hotwired/stimulus';
5
6const Combobox = {};
7
8Combobox.Actors = Base => class extends Base {
9  _initializeActors() {
10    this._actingListbox = this.listboxTarget;
11    this._actingCombobox = this.comboboxTarget;
12  }
13
14  _forAllComboboxes(callback) {
15    this._allComboboxes.forEach(callback);
16  }
17
18  get _actingListbox() {
19    return this.actingListbox
20  }
21
22  set _actingListbox(listbox) {
23    this.actingListbox = listbox;
24  }
25
26  get _actingCombobox() {
27    return this.actingCombobox
28  }
29
30  set _actingCombobox(combobox) {
31    this.actingCombobox = combobox;
32  }
33
34  get _allComboboxes() {
35    return [ this.comboboxTarget, this.dialogComboboxTarget ]
36  }
37};
38
39Combobox.Announcements = Base => class extends Base {
40  _announceToScreenReader(display, action) {
41    this.announcerTarget.innerText = `${display} ${action}`;
42  }
43};
44
45Combobox.AsyncLoading = Base => class extends Base {
46  get _isAsync() {
47    return this.hasAsyncSrcValue
48  }
49
50  get _isSync() {
51    return !this._isAsync
52  }
53};
54
55function Concerns(Base, ...mixins) {
56  return mixins.reduce((accumulator, current) => current(accumulator), Base)
57}
58
59function visible(target) {
60  return !(target.hidden || target.closest("[hidden]"))
61}
62
63function wrapAroundAccess(array, index) {
64  const first = 0;
65  const last = array.length - 1;
66
67  if (index < first) return array[last]
68  if (index > last) return array[first]
69  return array[index]
70}
71
72function applyFilter(query, { matching }) {
73  return (target) => {
74    if (query) {
75      const value = target.getAttribute(matching) || "";
76      const match = value.toLowerCase().includes(query.toLowerCase());
77
78      target.hidden = !match;
79    } else {
80      target.hidden = false;
81    }
82  }
83}
84
85function cancel(event) {
86  event.stopPropagation();
87  event.preventDefault();
88}
89
90function startsWith(string, substring) {
91  return string.toLowerCase().startsWith(substring.toLowerCase())
92}
93
94function debounce(fn, delay) {
95  let timeoutId = null;
96
97  return (...args) => {
98    const callback = () => fn.apply(this, args);
99    clearTimeout(timeoutId);
100    timeoutId = setTimeout(callback, delay);
101  }
102}
103
104function isDeleteEvent(event) {
105  return event.inputType === "deleteContentBackward" || event.inputType === "deleteWordBackward"
106}
107
108function sleep(ms) {
109  return new Promise(resolve => setTimeout(resolve, ms))
110}
111
112function unselectedPortion(element) {
113  if (element.selectionStart === element.selectionEnd) {
114    return element.value
115  } else {
116    return element.value.substring(0, element.selectionStart)
117  }
118}
119
120function dispatch(eventName, { target, cancelable, detail } = {}) {
121  const event = new CustomEvent(eventName, {
122    cancelable,
123    bubbles: true,
124    composed: true,
125    detail
126  });
127
128  if (target && target.isConnected) {
129    target.dispatchEvent(event);
130  } else {
131    document.documentElement.dispatchEvent(event);
132  }
133
134  return event
135}
136
137function nextRepaint() {
138  if (document.visibilityState === "hidden") {
139    return nextEventLoopTick()
140  } else {
141    return nextAnimationFrame()
142  }
143}
144
145function nextAnimationFrame() {
146  return new Promise((resolve) => requestAnimationFrame(() => resolve()))
147}
148
149function nextEventLoopTick() {
150  return new Promise((resolve) => setTimeout(() => resolve(), 0))
151}
152
153function randomUUID() {
154  const uuidPattern = "10000000-1000-4000-8000-100000000000";
155
156  return uuidPattern.replace(/[018]/g, (match) => {
157    const randomByte = crypto.getRandomValues(new Uint8Array(1))[0];
158    return (match ^ (randomByte & 15) >> (match / 4)).toString(16)
159  })
160}
161
162Combobox.Autocomplete = Base => class extends Base {
163  _connectListAutocomplete() {
164    if (!this._autocompletesList) {
165      this._visuallyHideListbox();
166    }
167  }
168
169  _hardAutocomplete(option) {
170    const typedValue = this._typedQuery;
171    const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
172
173    this._fullQuery = autocompletedValue;
174
175    if (this._isAutocompletableWith(typedValue, autocompletedValue)) {
176      this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
177    } else {
178      this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
179    }
180  }
181
182  _softAutocomplete(option) {
183    const typedValue = this._typedQuery;
184    const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
185
186    if (this._isAutocompletableWith(typedValue, autocompletedValue)) {
187      this._fullQuery = autocompletedValue;
188      this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
189    }
190  }
191
192  _isAutocompletableWith(typedValue, autocompletedValue) {
193    return this._autocompletesInline && startsWith(autocompletedValue, typedValue)
194  }
195
196  // +visuallyHideListbox+ hides the listbox from the user,
197  // but makes it still searchable by JS.
198  _visuallyHideListbox() {
199    this.listboxTarget.style.display = "none";
200  }
201
202  get _isExactAutocompleteMatch() {
203    return this._immediatelyAutocompletableValue === this._fullQuery
204  }
205
206  // All `_isExactAutocompleteMatch` matches are `_isPartialAutocompleteMatch` matches
207  // but not all `_isPartialAutocompleteMatch` matches are `_isExactAutocompleteMatch` matches.
208  get _isPartialAutocompleteMatch() {
209    return !!this._immediatelyAutocompletableValue &&
210      startsWith(this._immediatelyAutocompletableValue, this._fullQuery)
211  }
212
213  get _autocompletesList() {
214    return this.autocompleteValue === "both" || this.autocompleteValue === "list"
215  }
216
217  get _autocompletesInline() {
218    return this.autocompleteValue === "both" || this.autocompleteValue === "inline"
219  }
220
221  get _immediatelyAutocompletableValue() {
222    return this._ensurableOption?.getAttribute(this.autocompletableAttributeValue)
223  }
224};
225
226const MAX_CALLBACK_ATTEMPTS = 3;
227
228Combobox.Callbacks = Base => class extends Base {
229  _initializeCallbacks() {
230    this.callbackQueue = [];
231    this.callbackExecutionAttempts = {};
232  }
233
234  _enqueueCallback() {
235    const callbackId = randomUUID();
236    this.callbackQueue.push(callbackId);
237    return callbackId
238  }
239
240  _isNextCallback(callbackId) {
241    return this._nextCallback === callbackId
242  }
243
244  _callbackAttemptsExceeded(callbackId) {
245    return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
246  }
247
248  _callbackAttempts(callbackId) {
249    return this.callbackExecutionAttempts[callbackId] || 0
250  }
251
252  _recordCallbackAttempt(callbackId) {
253    this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1;
254  }
255
256  _dequeueCallback(callbackId) {
257    this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId);
258    this._forgetCallbackExecutionAttempts(callbackId);
259  }
260
261  _forgetCallbackExecutionAttempts(callbackId) {
262    delete this.callbackExecutionAttempts[callbackId];
263  }
264
265  get _nextCallback() {
266    return this.callbackQueue[0]
267  }
268};
269
270Combobox.Dialog = Base => class extends Base {
271  rerouteListboxStreamToDialog({ detail: { newStream } }) {
272    if (newStream.target == this.listboxTarget.id && this._dialogIsOpen) {
273      newStream.setAttribute("target", this.dialogListboxTarget.id);
274    }
275  }
276
277  _connectDialog() {
278    if (window.visualViewport) {
279      window.visualViewport.addEventListener("resize", this._resizeDialog);
280    }
281  }
282
283  _disconnectDialog() {
284    if (window.visualViewport) {
285      window.visualViewport.removeEventListener("resize", this._resizeDialog);
286    }
287  }
288
289  _moveArtifactsToDialog() {
290    this.dialogComboboxTarget.value = this._fullQuery;
291
292    this._actingCombobox = this.dialogComboboxTarget;
293    this._actingListbox = this.dialogListboxTarget;
294
295    this.dialogListboxTarget.append(...this.listboxTarget.children);
296  }
297
298  _moveArtifactsInline() {
299    this.comboboxTarget.value = this._fullQuery;
300
301    this._actingCombobox = this.comboboxTarget;
302    this._actingListbox = this.listboxTarget;
303
304    this.listboxTarget.append(...this.dialogListboxTarget.children);
305  }
306
307  _resizeDialog = () => {
308    if (window.visualViewport) {
309      this.dialogTarget.style.setProperty("--hw-visual-viewport-height", `${window.visualViewport.height}px`);
310    }
311  }
312
313  // After closing a dialog, focus returns to the last focused element.
314  // +preventFocusingComboboxAfterClosingDialog+ focuses a placeholder element before opening
315  // the dialog, so that the combobox is not focused again after closing, which would reopen.
316  _preventFocusingComboboxAfterClosingDialog() {
317    this.dialogFocusTrapTarget.focus();
318  }
319
320  get _isSmallViewport() {
321    return window.matchMedia(`(max-width: ${this.smallViewportMaxWidthValue})`).matches
322  }
323
324  get _dialogIsOpen() {
325    return this.dialogTarget.open
326  }
327};
328
329Combobox.Events = Base => class extends Base {
330  _dispatchPreselectionEvent({ isNewAndAllowed, previousValue }) {
331    if (previousValue === this._incomingFieldValueString) return
332
333    dispatch("hw-combobox:preselection", {
334      target: this.element,
335      detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
336    });
337  }
338
339  _dispatchSelectionEvent() {
340    dispatch("hw-combobox:selection", {
341      target: this.element,
342      detail: this._eventableDetails
343    });
344  }
345
346  _dispatchRemovalEvent({ removedDisplay, removedValue }) {
347    dispatch("hw-combobox:removal", {
348      target: this.element,
349      detail: { ...this._eventableDetails, removedDisplay, removedValue }
350    });
351  }
352
353  get _eventableDetails() {
354    return {
355      value: this._incomingFieldValueString,
356      display: this._fullQuery,
357      query: this._typedQuery,
358      fieldName: this._fieldName,
359      isValid: this._valueIsValid
360    }
361  }
362};
363
364class FetchResponse {
365  constructor(response) {
366    this.response = response;
367  }
368  get statusCode() {
369    return this.response.status;
370  }
371  get redirected() {
372    return this.response.redirected;
373  }
374  get ok() {
375    return this.response.ok;
376  }
377  get unauthenticated() {
378    return this.statusCode === 401;
379  }
380  get unprocessableEntity() {
381    return this.statusCode === 422;
382  }
383  get authenticationURL() {
384    return this.response.headers.get("WWW-Authenticate");
385  }
386  get contentType() {
387    const contentType = this.response.headers.get("Content-Type") || "";
388    return contentType.replace(/;.*$/, "");
389  }
390  get headers() {
391    return this.response.headers;
392  }
393  get html() {
394    if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) {
395      return this.text;
396    }
397    return Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
398  }
399  get json() {
400    if (this.contentType.match(/^application\/.*json$/)) {
401      return this.responseJson || (this.responseJson = this.response.json());
402    }
403    return Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
404  }
405  get text() {
406    return this.responseText || (this.responseText = this.response.text());
407  }
408  get isTurboStream() {
409    return this.contentType.match(/^text\/vnd\.turbo-stream\.html/);
410  }
411  async renderTurboStream() {
412    if (this.isTurboStream) {
413      if (window.Turbo) {
414        await window.Turbo.renderStreamMessage(await this.text);
415      } else {
416        console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
417      }
418    } else {
419      return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
420    }
421  }
422}
423
424class RequestInterceptor {
425  static register(interceptor) {
426    this.interceptor = interceptor;
427  }
428  static get() {
429    return this.interceptor;
430  }
431  static reset() {
432    this.interceptor = undefined;
433  }
434}
435
436function getCookie(name) {
437  const cookies = document.cookie ? document.cookie.split("; ") : [];
438  const prefix = `${encodeURIComponent(name)}=`;
439  const cookie = cookies.find((cookie => cookie.startsWith(prefix)));
440  if (cookie) {
441    const value = cookie.split("=").slice(1).join("=");
442    if (value) {
443      return decodeURIComponent(value);
444    }
445  }
446}
447
448function compact(object) {
449  const result = {};
450  for (const key in object) {
451    const value = object[key];
452    if (value !== undefined) {
453      result[key] = value;
454    }
455  }
456  return result;
457}
458
459function metaContent(name) {
460  const element = document.head.querySelector(`meta[name="${name}"]`);
461  return element && element.content;
462}
463
464function stringEntriesFromFormData(formData) {
465  return [ ...formData ].reduce(((entries, [name, value]) => entries.concat(typeof value === "string" ? [ [ name, value ] ] : [])), []);
466}
467
468function mergeEntries(searchParams, entries) {
vendor: 5,334 bytes, lines 469-640
469  for (const [name, value] of entries) {
470    if (value instanceof window.File) continue;
471    if (searchParams.has(name) && !name.includes("[]")) {
472      searchParams.delete(name);
473      searchParams.set(name, value);
474    } else {
475      searchParams.append(name, value);
476    }
477  }
478}
479
480class FetchRequest {
481  constructor(method, url, options = {}) {
482    this.method = method;
483    this.options = options;
484    this.originalUrl = url.toString();
485  }
486  async perform() {
487    try {
488      const requestInterceptor = RequestInterceptor.get();
489      if (requestInterceptor) {
490        await requestInterceptor(this);
491      }
492    } catch (error) {
493      console.error(error);
494    }
495    const response = new FetchResponse(await window.fetch(this.url, this.fetchOptions));
496    if (response.unauthenticated && response.authenticationURL) {
497      return Promise.reject(window.location.href = response.authenticationURL);
498    }
499    const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
500    if (responseStatusIsTurboStreamable && response.isTurboStream) {
501      await response.renderTurboStream();
502    }
503    return response;
504  }
505  addHeader(key, value) {
506    const headers = this.additionalHeaders;
507    headers[key] = value;
508    this.options.headers = headers;
509  }
510  sameHostname() {
511    if (!this.originalUrl.startsWith("http:")) {
512      return true;
513    }
514    try {
515      return new URL(this.originalUrl).hostname === window.location.hostname;
516    } catch (_) {
517      return true;
518    }
519  }
520  get fetchOptions() {
521    return {
522      method: this.method.toUpperCase(),
523      headers: this.headers,
524      body: this.formattedBody,
525      signal: this.signal,
526      credentials: this.credentials,
527      redirect: this.redirect
528    };
529  }
530  get headers() {
531    const baseHeaders = {
532      "X-Requested-With": "XMLHttpRequest",
533      "Content-Type": this.contentType,
534      Accept: this.accept
535    };
536    if (this.sameHostname()) {
537      baseHeaders["X-CSRF-Token"] = this.csrfToken;
538    }
539    return compact(Object.assign(baseHeaders, this.additionalHeaders));
540  }
541  get csrfToken() {
542    return getCookie(metaContent("csrf-param")) || metaContent("csrf-token");
543  }
544  get contentType() {
545    if (this.options.contentType) {
546      return this.options.contentType;
547    } else if (this.body == null || this.body instanceof window.FormData) {
548      return undefined;
549    } else if (this.body instanceof window.File) {
550      return this.body.type;
551    }
552    return "application/json";
553  }
554  get accept() {
555    switch (this.responseKind) {
556     case "html":
557      return "text/html, application/xhtml+xml";
558
559     case "turbo-stream":
560      return "text/vnd.turbo-stream.html, text/html, application/xhtml+xml";
561
562     case "json":
563      return "application/json, application/vnd.api+json";
564
565     default:
566      return "*/*";
567    }
568  }
569  get body() {
570    return this.options.body;
571  }
572  get query() {
573    const originalQuery = (this.originalUrl.split("?")[1] || "").split("#")[0];
574    const params = new URLSearchParams(originalQuery);
575    let requestQuery = this.options.query;
576    if (requestQuery instanceof window.FormData) {
577      requestQuery = stringEntriesFromFormData(requestQuery);
578    } else if (requestQuery instanceof window.URLSearchParams) {
579      requestQuery = requestQuery.entries();
580    } else {
581      requestQuery = Object.entries(requestQuery || {});
582    }
583    mergeEntries(params, requestQuery);
584    const query = params.toString();
585    return query.length > 0 ? `?${query}` : "";
586  }
587  get url() {
588    return this.originalUrl.split("?")[0].split("#")[0] + this.query;
589  }
590  get responseKind() {
591    return this.options.responseKind || "html";
592  }
593  get signal() {
594    return this.options.signal;
595  }
596  get redirect() {
597    return this.options.redirect || "follow";
598  }
599  get credentials() {
600    return this.options.credentials || "same-origin";
601  }
602  get additionalHeaders() {
603    return this.options.headers || {};
604  }
605  get formattedBody() {
606    const bodyIsAString = Object.prototype.toString.call(this.body) === "[object String]";
607    const contentTypeIsJson = this.headers["Content-Type"] === "application/json";
608    if (contentTypeIsJson && !bodyIsAString) {
609      return JSON.stringify(this.body);
610    }
611    return this.body;
612  }
613}
614
615async function get(url, options) {
616  const request = new FetchRequest("get", url, options);
617  return request.perform();
618}
619
620async function post(url, options) {
621  const request = new FetchRequest("post", url, options);
622  return request.perform();
623}
624
625// Copyright (c) 2021 Marcelo Lauxen
626
627// Permission is hereby granted, free of charge, to any person obtaining
628// a copy of this software and associated documentation files (the
629// "Software"), to deal in the Software without restriction, including
630// without limitation the rights to use, copy, modify, merge, publish,
631// distribute, sublicense, and/or sell copies of the Software, and to
632// permit persons to whom the Software is furnished to do so, subject to
633// the following conditions:
634
635// The above copyright notice and this permission notice shall be
636// included in all copies or substantial portions of the Software.
637
638// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
639// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
640// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
641// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
642// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
643// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
644// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
645
646Combobox.Filtering = Base => class extends Base {
647  prepareToFilter({ key }) {
648    const intendsToFilter = key.match(/^[a-zA-Z0-9]$|^ArrowDown$/);
649
650    if (this._isClosed && intendsToFilter) {
651      this.open(); // `.open()` sets the appropriate state so the combobox knows it’s open.
652      this._expand(); // `.open()` will call `._expand()` via stimulus callbacks, but we’re calling it inline so it happens immediately.
653    }
654  }
655
656  filterAndSelect({ inputType }) {
657    this._filter(inputType);
658
659    if (this._isSync) {
660      this._selectOnQuery(inputType);
661    }
662  }
663
664  clear(event) {
665    this._clearQuery();
666    this.chipDismisserTargets.forEach(el => el.click());
667    if (event && !event.defaultPrevented) event.target.focus();
668  }
669
670  _initializeFiltering() {
671    this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this), this.debounceIntervalValue);
672  }
673
674  _filter(inputType) {
675    if (this._isAsync) {
676      this._debouncedFilterAsync(inputType);
677    } else {
678      this._filterSync();
679    }
680
681    this._markQueried();
682  }
683
684  _debouncedFilterAsync(inputType) {
685    this._filterAsync(inputType);
686  }
687
688  async _filterAsync(inputType) {
689    const query = {
690      q: this._fullQuery,
691      input_type: inputType,
692      for_id: this.element.dataset.asyncId,
693      callback_id: this._enqueueCallback()
694    };
695
696    await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
697  }
698
699  _filterSync() {
700    this._allFilterableOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
701  }
702
703  _clearQuery() {
704    this._fullQuery = "";
705    this.filterAndSelect({ inputType: "deleteContentBackward" });
706  }
707
708  _markQueried() {
709    this._forAllComboboxes(el => el.toggleAttribute("data-queried", this._isQueried));
710  }
711
712  get _isQueried() {
713    return this._fullQuery.length > 0
714  }
715
716  get _fullQuery() {
717    return this._actingCombobox.value
718  }
719
720  set _fullQuery(value) {
721    this._actingCombobox.value = value;
722  }
723
724  get _typedQuery() {
725    return unselectedPortion(this._actingCombobox)
726  }
727};
728
729Combobox.FormField = Base => class extends Base {
730  get _fieldValue() {
731    if (this._isMultiselect) {
732      const currentValue = this.hiddenFieldTarget.value;
733      const arrayFromValue = currentValue ? currentValue.split(",") : [];
734
735      return new Set(arrayFromValue)
736    } else {
737      return this.hiddenFieldTarget.value
738    }
739  }
740
741  get _fieldValueString() {
742    if (this._isMultiselect) {
743      return this._fieldValueArray.join(",")
744    } else {
745      return this.hiddenFieldTarget.value
746    }
747  }
748
749  get _incomingFieldValueString() {
750    if (this._isMultiselect) {
751      const array = this._fieldValueArray;
752
753      if (this.hiddenFieldTarget.dataset.valueForMultiselect) {
754        array.push(this.hiddenFieldTarget.dataset.valueForMultiselect);
755      }
756
757      return array.join(",")
758    } else {
759      return this.hiddenFieldTarget.value
760    }
761  }
762
763  get _fieldValueArray() {
764    if (this._isMultiselect) {
765      return Array.from(this._fieldValue)
766    } else {
767      return [ this.hiddenFieldTarget.value ]
768    }
769  }
770
771  set _fieldValue(value) {
772    if (this._isMultiselect) {
773      this.hiddenFieldTarget.dataset.valueForMultiselect = value?.replace(/,/g, "");
774      this.hiddenFieldTarget.dataset.displayForMultiselect = this._fullQuery;
775    } else {
776      this.hiddenFieldTarget.value = value;
777    }
778  }
779
780  get _hasEmptyFieldValue() {
781    if (this._isMultiselect) {
782      return this.hiddenFieldTarget.dataset.valueForMultiselect == "" || this.hiddenFieldTarget.dataset.valueForMultiselect == "undefined"
783    } else {
784      return this.hiddenFieldTarget.value === ""
785    }
786  }
787
788  get _hasFieldValue() {
789    return !this._hasEmptyFieldValue
790  }
791
792  get _fieldName() {
793    return this.hiddenFieldTarget.name
794  }
795
796  set _fieldName(value) {
797    this.hiddenFieldTarget.name = value;
798  }
799};
800
801Combobox.Multiselect = Base => class extends Base {
802  navigateChip(event) {
803    this._chipKeyHandlers[event.key]?.call(this, event);
804  }
805
806  removeChip({ currentTarget, params }) {
807    let display;
808    const option = this._optionElementWithValue(params.value);
809
810    if (option) {
811      display = option.getAttribute(this.autocompletableAttributeValue);
812      this._markNotSelected(option);
813      this._markNotMultiselected(option);
814    } else {
815      display = params.value; // for new options
816    }
817
818    this._removeFromFieldValue(params.value);
819    this._filter("hw:multiselectSync");
820
821    currentTarget.closest("[data-hw-combobox-chip]").remove();
822
823    if (!this._isSmallViewport) {
824      this.open();
825    }
826
827    this._announceToScreenReader(display, "removed");
828    this._dispatchRemovalEvent({ removedDisplay: display, removedValue: params.value });
829  }
830
831  hideChipsForCache() {
832    this.element.querySelectorAll("[data-hw-combobox-chip]").forEach(chip => chip.hidden = true);
833  }
834
835  _chipKeyHandlers = {
836    Backspace: (event) => {
837      this.removeChip(event);
838      cancel(event);
839    },
840    Enter: (event) => {
841      this.removeChip(event);
842      cancel(event);
843    },
844    Space: (event) => {
845      this.removeChip(event);
846      cancel(event);
847    },
848    Escape: (event) => {
849      this.open();
850      cancel(event);
851    }
852  }
853
854  _connectMultiselect() {
855    if (!this._isMultiPreselected) {
856      this._preselectMultiple();
857      this._markMultiPreselected();
858    }
859  }
860
861  async _createChip(shouldReopen) {
862    if (!this._isMultiselect) return
863
864    this._beforeClearingMultiselectQuery(async (display, value) => {
865      this._fullQuery = "";
866
867      this._filter("hw:multiselectSync");
868      this._requestChips(value);
869      this._addToFieldValue(value);
870
871      if (shouldReopen) {
872        await nextRepaint();
873        this.open();
874      }
875
876      this._announceToScreenReader(display, "multi-selected. Press Shift + Tab, then Enter to remove.");
877    });
878  }
879
880  async _requestChips(values) {
881    await post(this.selectionChipSrcValue, {
882      responseKind: "turbo-stream",
883      query: {
884        for_id: this.element.dataset.asyncId,
885        combobox_values: values
886      }
887    });
888  }
889
890  _beforeClearingMultiselectQuery(callback) {
891    const display = this.hiddenFieldTarget.dataset.displayForMultiselect;
892    const value = this.hiddenFieldTarget.dataset.valueForMultiselect;
893
894    if (value && !this._fieldValue.has(value)) {
895      callback(display, value);
896    }
897
898    this.hiddenFieldTarget.dataset.displayForMultiselect = "";
899    this.hiddenFieldTarget.dataset.valueForMultiselect = "";
900  }
901
902  _resetMultiselectionMarks() {
903    if (!this._isMultiselect) return
904
905    this._fieldValueArray.forEach(value => {
906      const option = this._optionElementWithValue(value);
907
908      if (option) {
909        option.setAttribute("data-multiselected", "");
910        option.hidden = true;
911      }
912    });
913  }
914
915  _markNotMultiselected(option) {
916    if (!this._isMultiselect) return
917
918    option.removeAttribute("data-multiselected");
919    option.hidden = false;
920  }
921
922  _addToFieldValue(value) {
923    const newValue = this._fieldValue;
924
925    newValue.add(String(value));
926    this.hiddenFieldTarget.value = Array.from(newValue).join(",");
927
928    if (this._isSync) this._resetMultiselectionMarks();
929  }
930
931  _removeFromFieldValue(value) {
932    const newValue = this._fieldValue;
933
934    newValue.delete(String(value));
935    this.hiddenFieldTarget.value = Array.from(newValue).join(",");
936
937    if (this._isSync) this._resetMultiselectionMarks();
938  }
939
940  _focusLastChipDismisser() {
941    this.chipDismisserTargets[this.chipDismisserTargets.length - 1]?.focus();
942  }
943
944  _markMultiPreselected() {
945    this.element.dataset.multiPreselected = "";
946  }
947
948  get _isMultiselect() {
949    return this.hasSelectionChipSrcValue
950  }
951
952  get _isSingleSelect() {
953    return !this._isMultiselect
954  }
955
956  get _isMultiPreselected() {
957    return this.element.hasAttribute("data-multi-preselected")
958  }
959};
960
961Combobox.Navigation = Base => class extends Base {
962  navigate(event) {
963    if (this._autocompletesList) {
964      this._navigationKeyHandlers[event.key]?.call(this, event);
965    }
966  }
967
968  _navigationKeyHandlers = {
969    ArrowUp: (event) => {
970      this._selectIndex(this._selectedOptionIndex - 1);
971      cancel(event);
972    },
973    ArrowDown: (event) => {
974      this._selectIndex(this._selectedOptionIndex + 1);
975
976      if (this._selectedOptionIndex === 0) {
977        this._actingListbox.scrollTop = 0;
978      }
979
980      cancel(event);
981    },
982    Home: (event) => {
983      this._selectIndex(0);
984      cancel(event);
985    },
986    End: (event) => {
987      this._selectIndex(this._visibleOptionElements.length - 1);
988      cancel(event);
989    },
990    Enter: (event) => {
991      this.close("hw:keyHandler:enter");
992      cancel(event);
993    },
994    Escape: (event) => {
995      this._isOpen ? this.close("hw:keyHandler:escape") : this._clearQuery();
996      cancel(event);
997    },
998    Backspace: (event) => {
999      if (this._isMultiselect && !this._fullQuery) {
1000        this._focusLastChipDismisser();
1001        cancel(event);
1002      }
1003    }
1004  }
1005};
1006
1007Combobox.NewOptions = Base => class extends Base {
1008  _shouldTreatAsNewOptionForFiltering(queryIsBeingRefined) {
1009    if (queryIsBeingRefined) {
1010      return this._isNewOptionWithNoPotentialMatches
1011    } else {
1012      return this._isNewOptionWithPotentialMatches
1013    }
1014  }
1015
1016  // If the user is going to keep refining the query, we can't be sure whether
1017  // the option will end up being new or not unless there are no potential matches.
1018  // +_isNewOptionWithNoPotentialMatches+ allows us to make our best guess
1019  // while the state of the combobox is still in flux.
1020  //
1021  // It's okay for the combobox to say it's not new even if it will be eventually,
1022  // as only the final state matters for submission purposes. This method exists
1023  // as a best effort to keep the state accurate as often as we can.
1024  //
1025  // Note that the first visible option is automatically selected as you type.
1026  // So if there's a partial match, it's not a new option at this point.
1027  //
1028  // The final state is locked-in upon closing the combobox via `_isNewOptionWithPotentialMatches`.
1029  get _isNewOptionWithNoPotentialMatches() {
1030    return this._isNewOptionWithPotentialMatches && !this._isPartialAutocompleteMatch
1031  }
1032
1033  // If the query is finalized, we don't care that there are potential matches
1034  // because new options can be substrings of existing options.
1035  //
1036  // We can't use `_isNewOptionWithNoPotentialMatches` because that would
1037  // rule out new options that are partial matches.
1038  get _isNewOptionWithPotentialMatches() {
1039    return this._isQueried && this._allowNew && !this._isExactAutocompleteMatch
1040  }
1041};
1042
1043Combobox.Options = Base => class extends Base {
1044  _resetOptionsSilently() {
1045    this._resetOptions(this._deselect.bind(this));
1046  }
1047
1048  _resetOptionsAndNotify() {
1049    this._resetOptions(this._deselectAndNotify.bind(this));
1050  }
1051
1052  _resetOptions(deselectionStrategy) {
1053    this._fieldName = this.originalNameValue;
1054    deselectionStrategy();
1055  }
1056
1057  _optionElementWithValue(value) {
1058    return this._actingListbox.querySelector(`[${this.filterableAttributeValue}][data-value='${value}']`)
1059  }
1060
1061  _displayForOptionElement(element) {
1062    return element.getAttribute(this.autocompletableAttributeValue)
1063  }
1064
1065  get _allowNew() {
1066    return !!this.nameWhenNewValue
1067  }
1068
1069  get _allOptions() {
1070    return Array.from(this._allFilterableOptionElements)
1071  }
1072
1073  get _allFilterableOptionElements() {
1074    return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]:not([data-multiselected])`)
1075  }
1076
1077  get _visibleOptionElements() {
1078    return [ ...this._allFilterableOptionElements ].filter(visible)
1079  }
1080
1081  get _selectedOptionElement() {
1082    return this._actingListbox.querySelector("[role=option][aria-selected=true]:not([data-multiselected])")
1083  }
1084
1085  get _multiselectedOptionElements() {
1086    return this._actingListbox.querySelectorAll("[role=option][data-multiselected]")
1087  }
1088
1089  get _selectedOptionIndex() {
1090    return [ ...this._visibleOptionElements ].indexOf(this._selectedOptionElement)
1091  }
1092
1093  get _isUnjustifiablyBlank() {
1094    const valueIsMissing = this._hasEmptyFieldValue;
1095    const noBlankOptionSelected = !this._selectedOptionElement;
1096
1097    return valueIsMissing && noBlankOptionSelected
1098  }
1099};
1100
1101Combobox.Selection = Base => class extends Base {
1102  selectOnClick({ currentTarget, inputType }) {
1103    this._forceSelectionAndFilter(currentTarget, inputType);
1104    this.close("hw:optionRoleClick");
1105  }
1106
1107  _connectSelection() {
1108    if (this.hasPrefilledDisplayValue) {
1109      this._fullQuery = this.prefilledDisplayValue;
1110      this._markQueried();
1111    }
1112  }
1113
1114  _selectOnQuery(inputType) {
1115    if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent({ inputType: inputType }))) {
1116      this._selectNew();
1117    } else if (isDeleteEvent({ inputType: inputType })) {
1118      this._deselect();
1119    } else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
1120      this._select(this._ensurableOption, this._softAutocomplete.bind(this));
1121    } else if (this._isOpen && this._visibleOptionElements[0]) {
1122      this._select(this._visibleOptionElements[0], this._softAutocomplete.bind(this));
1123    } else if (this._isOpen) {
1124      this._resetOptionsAndNotify();
1125      this._markInvalid();
1126    } else ;
1127  }
1128
1129  _select(option, autocompleteStrategy) {
1130    const previousValue = this._fieldValueString;
1131
1132    this._resetOptionsSilently();
1133
1134    autocompleteStrategy(option);
1135
1136    this._fieldValue = option.dataset.value;
1137    this._markSelected(option);
1138    this._markValid();
1139    this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
1140
1141    option.scrollIntoView({ block: "nearest" });
1142  }
1143
1144  _selectNew() {
1145    const previousValue = this._fieldValueString;
1146
1147    this._resetOptionsSilently();
1148    this._fieldValue = this._fullQuery;
1149    this._fieldName = this.nameWhenNewValue;
1150    this._markValid();
1151    this._dispatchPreselectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
1152  }
1153
1154  _deselect() {
1155    const previousValue = this._fieldValueString;
1156
1157    if (this._selectedOptionElement) {
1158      this._markNotSelected(this._selectedOptionElement);
1159    }
1160
1161    this._fieldValue = "";
1162    this._setActiveDescendant("");
1163
1164    return previousValue
1165  }
1166
1167  _deselectAndNotify() {
1168    const previousValue = this._deselect();
1169    this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
1170  }
1171
1172  _selectIndex(index) {
1173    const option = wrapAroundAccess(this._visibleOptionElements, index);
1174    this._forceSelectionWithoutFiltering(option);
1175  }
1176
1177  _preselectSingle() {
1178    if (this._isSingleSelect && this._hasValueButNoSelection) {
1179      const option = this._optionElementWithValue(this._fieldValue);
1180      if (option) this._markSelected(option);
1181    }
1182  }
1183
1184  _preselectMultiple() {
1185    if (this._isMultiselect && this._hasValueButNoSelection) {
1186      this._requestChips(this._fieldValueString);
1187      this._resetMultiselectionMarks();
1188    }
1189  }
1190
1191  _forceSelectionAndFilter(option, inputType) {
1192    this._forceSelectionWithoutFiltering(option);
1193    this._filter(inputType);
1194  }
1195
1196  _forceSelectionWithoutFiltering(option) {
1197    this._select(option, this._hardAutocomplete.bind(this));
1198  }
1199
1200  _lockInSelection() {
1201    if (this._shouldLockInSelection) {
1202      this._forceSelectionAndFilter(this._ensurableOption, "hw:lockInSelection");
1203    }
1204  }
1205
1206  _markSelected(option) {
1207    if (this.hasSelectedClass) option.classList.add(this.selectedClass);
1208    option.setAttribute("aria-selected", true);
1209    this._setActiveDescendant(option.id);
1210  }
1211
1212  _markNotSelected(option) {
1213    if (this.hasSelectedClass) option.classList.remove(this.selectedClass);
1214    option.removeAttribute("aria-selected");
1215    this._removeActiveDescendant();
1216  }
1217
1218  _setActiveDescendant(id) {
1219    this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id));
1220  }
1221
1222  _removeActiveDescendant() {
1223    this._setActiveDescendant("");
1224  }
1225
1226  get _hasValueButNoSelection() {
1227    return this._hasFieldValue && !this._hasSelection
1228  }
1229
1230  get _hasSelection() {
1231    if (this._isSingleSelect) {
1232      this._selectedOptionElement;
1233    } else {
1234      this._multiselectedOptionElements.length > 0;
1235    }
1236  }
1237
1238  get _shouldLockInSelection() {
1239    return this._isQueried && !!this._ensurableOption && !this._isNewOptionWithPotentialMatches
1240  }
1241
1242  get _ensurableOption() {
1243    return this._selectedOptionElement || this._visibleOptionElements[0]
1244  }
1245};
1246
1247function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
1248
1249// Older browsers don't support event options, feature detect it.
1250
1251// Adopted and modified solution from Bohdan Didukh (2017)
1252// https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi
1253
1254var hasPassiveEvents = false;
1255if (typeof window !== 'undefined') {
1256  var passiveTestOptions = {
1257    get passive() {
1258      hasPassiveEvents = true;
1259      return undefined;
1260    }
1261  };
1262  window.addEventListener('testPassive', null, passiveTestOptions);
1263  window.removeEventListener('testPassive', null, passiveTestOptions);
1264}
1265
1266var isIosDevice = typeof window !== 'undefined' && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1);
1267
1268
1269var locks = [];
1270var documentListenerAdded = false;
1271var initialClientY = -1;
1272var previousBodyOverflowSetting = void 0;
1273var previousBodyPosition = void 0;
1274var previousBodyPaddingRight = void 0;
1275
1276// returns true if `el` should be allowed to receive touchmove events.
1277var allowTouchMove = function allowTouchMove(el) {
1278  return locks.some(function (lock) {
1279    if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
1280      return true;
1281    }
1282
1283    return false;
1284  });
1285};
1286
1287var preventDefault = function preventDefault(rawEvent) {
1288  var e = rawEvent || window.event;
1289
1290  // For the case whereby consumers adds a touchmove event listener to document.
1291  // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
1292  // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
1293  // the touchmove event on document will break.
1294  if (allowTouchMove(e.target)) {
1295    return true;
1296  }
1297
1298  // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
1299  if (e.touches.length > 1) return true;
1300
1301  if (e.preventDefault) e.preventDefault();
1302
1303  return false;
1304};
1305
1306var setOverflowHidden = function setOverflowHidden(options) {
1307  // If previousBodyPaddingRight is already set, don't set it again.
1308  if (previousBodyPaddingRight === undefined) {
1309    var _reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
1310    var scrollBarGap = window.innerWidth - document.documentElement.clientWidth;
1311
1312    if (_reserveScrollBarGap && scrollBarGap > 0) {
1313      var computedBodyPaddingRight = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'), 10);
1314      previousBodyPaddingRight = document.body.style.paddingRight;
1315      document.body.style.paddingRight = computedBodyPaddingRight + scrollBarGap + 'px';
1316    }
1317  }
1318
1319  // If previousBodyOverflowSetting is already set, don't set it again.
1320  if (previousBodyOverflowSetting === undefined) {
1321    previousBodyOverflowSetting = document.body.style.overflow;
1322    document.body.style.overflow = 'hidden';
1323  }
1324};
1325
1326var restoreOverflowSetting = function restoreOverflowSetting() {
1327  if (previousBodyPaddingRight !== undefined) {
1328    document.body.style.paddingRight = previousBodyPaddingRight;
1329
1330    // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
1331    // can be set again.
1332    previousBodyPaddingRight = undefined;
1333  }
1334
1335  if (previousBodyOverflowSetting !== undefined) {
1336    document.body.style.overflow = previousBodyOverflowSetting;
1337
1338    // Restore previousBodyOverflowSetting to undefined
1339    // so setOverflowHidden knows it can be set again.
1340    previousBodyOverflowSetting = undefined;
1341  }
1342};
1343
1344var setPositionFixed = function setPositionFixed() {
1345  return window.requestAnimationFrame(function () {
1346    // If previousBodyPosition is already set, don't set it again.
1347    if (previousBodyPosition === undefined) {
1348      previousBodyPosition = {
1349        position: document.body.style.position,
1350        top: document.body.style.top,
1351        left: document.body.style.left
1352      };
1353
1354      // Update the dom inside an animation frame
1355      var _window = window,
1356          scrollY = _window.scrollY,
1357          scrollX = _window.scrollX,
1358          innerHeight = _window.innerHeight;
1359
1360      document.body.style.position = 'fixed';
1361      document.body.style.top = -scrollY + 'px';
1362      document.body.style.left = -scrollX + 'px';
1363
1364      setTimeout(function () {
1365        return window.requestAnimationFrame(function () {
1366          // Attempt to check if the bottom bar appeared due to the position change
1367          var bottomBarHeight = innerHeight - window.innerHeight;
1368          if (bottomBarHeight && scrollY >= innerHeight) {
1369            // Move the content further up so that the bottom bar doesn't hide it
1370            document.body.style.top = -(scrollY + bottomBarHeight);
1371          }
1372        });
1373      }, 300);
1374    }
1375  });
1376};
1377
1378var restorePositionSetting = function restorePositionSetting() {
1379  if (previousBodyPosition !== undefined) {
1380    // Convert the position from "px" to Int
1381    var y = -parseInt(document.body.style.top, 10);
1382    var x = -parseInt(document.body.style.left, 10);
1383
1384    // Restore styles
1385    document.body.style.position = previousBodyPosition.position;
1386    document.body.style.top = previousBodyPosition.top;
1387    document.body.style.left = previousBodyPosition.left;
1388
1389    // Restore scroll
1390    window.scrollTo(x, y);
1391
1392    previousBodyPosition = undefined;
1393  }
1394};
1395
1396// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
1397var isTargetElementTotallyScrolled = function isTargetElementTotallyScrolled(targetElement) {
1398  return targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false;
1399};
1400
1401var handleScroll = function handleScroll(event, targetElement) {
1402  var clientY = event.targetTouches[0].clientY - initialClientY;
1403
1404  if (allowTouchMove(event.target)) {
1405    return false;
1406  }
1407
1408  if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
1409    // element is at the top of its scroll.
1410    return preventDefault(event);
1411  }
1412
1413  if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
1414    // element is at the bottom of its scroll.
1415    return preventDefault(event);
1416  }
1417
1418  event.stopPropagation();
1419  return true;
1420};
1421
1422var disableBodyScroll = function disableBodyScroll(targetElement, options) {
1423  // targetElement must be provided
1424  if (!targetElement) {
1425    // eslint-disable-next-line no-console
1426    console.error('disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.');
1427    return;
1428  }
1429
1430  // disableBodyScroll must not have been called on this targetElement before
1431  if (locks.some(function (lock) {
1432    return lock.targetElement === targetElement;
1433  })) {
1434    return;
1435  }
1436
1437  var lock = {
1438    targetElement: targetElement,
1439    options: options || {}
1440  };
1441
1442  locks = [].concat(_toConsumableArray(locks), [lock]);
1443
1444  if (isIosDevice) {
1445    setPositionFixed();
1446  } else {
1447    setOverflowHidden(options);
1448  }
1449
1450  if (isIosDevice) {
1451    targetElement.ontouchstart = function (event) {
1452      if (event.targetTouches.length === 1) {
1453        // detect single touch.
1454        initialClientY = event.targetTouches[0].clientY;
1455      }
1456    };
1457    targetElement.ontouchmove = function (event) {
1458      if (event.targetTouches.length === 1) {
1459        // detect single touch.
1460        handleScroll(event, targetElement);
1461      }
1462    };
1463
1464    if (!documentListenerAdded) {
1465      document.addEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
1466      documentListenerAdded = true;
1467    }
1468  }
1469};
1470
1471var enableBodyScroll = function enableBodyScroll(targetElement) {
1472  if (!targetElement) {
1473    // eslint-disable-next-line no-console
1474    console.error('enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.');
1475    return;
1476  }
1477
1478  locks = locks.filter(function (lock) {
1479    return lock.targetElement !== targetElement;
1480  });
1481
1482  if (isIosDevice) {
1483    targetElement.ontouchstart = null;
1484    targetElement.ontouchmove = null;
1485
1486    if (documentListenerAdded && locks.length === 0) {
1487      document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
1488      documentListenerAdded = false;
1489    }
1490  }
1491
1492  if (isIosDevice) {
1493    restorePositionSetting();
1494  } else {
1495    restoreOverflowSetting();
1496  }
1497};
1498
1499// MIT License
1500
1501// Copyright (c) 2018 Will Po
1502
1503// Permission is hereby granted, free of charge, to any person obtaining a copy
1504// of this software and associated documentation files (the "Software"), to deal
1505// in the Software without restriction, including without limitation the rights
1506// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1507// copies of the Software, and to permit persons to whom the Software is
1508// furnished to do so, subject to the following conditions:
1509
1510// The above copyright notice and this permission notice shall be included in all
1511// copies or substantial portions of the Software.
1512
1513// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1514// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1515// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1516// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1517// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1518// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1519// SOFTWARE.
1520
1521Combobox.Toggle = Base => class extends Base {
1522  open() {
1523    this.expandedValue = true;
1524  }
1525
1526  close(inputType) {
1527    if (this._isOpen) {
1528      const shouldReopen = this._isMultiselect &&
1529        this._isSync &&
1530        !this._isSmallViewport &&
1531        inputType != "hw:clickOutside" &&
1532        inputType != "hw:focusOutside" &&
1533        inputType != "hw:asyncCloser";
1534
1535      this._lockInSelection();
1536      this._clearInvalidQuery();
1537
1538      this.expandedValue = false;
1539
1540      if (inputType != "hw:keyHandler:escape") {
1541        this._dispatchSelectionEvent();
1542        this._createChip(shouldReopen);
1543      }
1544
1545      if (this._isSingleSelect && this._selectedOptionElement) {
1546        this._announceToScreenReader(this._displayForOptionElement(this._selectedOptionElement), "selected");
1547      }
1548    }
1549  }
1550
1551  toggle() {
1552    if (this.expandedValue) {
1553      this.close("hw:toggle");
1554    } else {
1555      this.open();
1556    }
1557  }
1558
1559  closeOnClickOutside(event) {
1560    const target = event.target;
1561
1562    if (this._isClosed) return
1563    if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
1564    if (this._withinElementBounds(event)) return
1565
1566    this.close("hw:clickOutside");
1567  }
1568
1569  closeOnFocusOutside({ target }) {
1570    if (this._isClosed) return
1571    if (this.element.contains(target)) return
1572
1573    this.close("hw:focusOutside");
1574  }
1575
1576  clearOrToggleOnHandleClick() {
1577    if (this.comboboxTarget.disabled) return
1578
1579    if (this._isQueried) {
1580      this._clearQuery();
1581      this.open();
1582    } else {
1583      this.toggle();
1584    }
1585  }
1586
1587  // Some browser extensions like 1Password overlay elements on top of the combobox.
1588  // Hovering over these elements emits a click event for some reason.
1589  // These events don't contain any telling information, so we use `_withinElementBounds`
1590  // as an alternative to check whether the click is legitimate.
1591  _withinElementBounds(event) {
1592    const { left, right, top, bottom } = this.mainWrapperTarget.getBoundingClientRect();
1593    const { clientX, clientY } = event;
1594
1595    return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
1596  }
1597
1598  _isDialogDismisser(target) {
1599    return target.closest("dialog") && target.role != "combobox"
1600  }
1601
1602  _expand() {
1603    if (this._isSync) {
1604      this._preselectSingle();
1605    }
1606
1607    if (this._autocompletesList && this._isSmallViewport) {
1608      this._openInDialog();
1609    } else {
1610      this._openInline();
1611    }
1612
1613    this._actingCombobox.setAttribute("aria-expanded", true); // needs to happen after setting acting combobox
1614  }
1615
1616  // +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
1617  // it interprets a change in `expandedValue` — whereas `.close()` is only called 
1617internally by us.
1618  _collapse() {
1619    this._actingCombobox.setAttribute("aria-expanded", false); // needs to happen before resetting acting combobox
1620
1621    if (this._dialogIsOpen) {
1622      this._closeInDialog();
1623    } else {
1624      this._closeInline();
1625    }
1626  }
1627
1628  _openInDialog() {
1629    this._moveArtifactsToDialog();
1630    this._preventFocusingComboboxAfterClosingDialog();
1631    this._preventBodyScroll();
1632    this.dialogTarget.showModal();
1633    this._resizeDialog();
1634  }
1635
1636  _openInline() {
1637    this.listboxTarget.hidden = false;
1638  }
1639
1640  _closeInDialog() {
1641    this._moveArtifactsInline();
1642    this.dialogTarget.close();
1643    this._restoreBodyScroll();
1644    this._actingCombobox.scrollIntoView({ block: "center" });
1645  }
1646
1647  _closeInline() {
1648    this.listboxTarget.hidden = true;
1649  }
1650
1651  _preventBodyScroll() {
1652    disableBodyScroll(this.dialogListboxTarget);
1653  }
1654
1655  _restoreBodyScroll() {
1656    enableBodyScroll(this.dialogListboxTarget);
1657  }
1658
1659  _clearInvalidQuery() {
1660    if (this._isUnjustifiablyBlank) {
1661      this._deselect();
1662      this._clearQuery();
1663    }
1664  }
1665
1666  get _isOpen() {
1667    return this.expandedValue
1668  }
1669
1670  get _isClosed() {
1671    return !this._isOpen
1672  }
1673};
1674
1675Combobox.Validity = Base => class extends Base {
1676  _markValid() {
1677    if (this._valueIsInvalid) return
1678
1679    this._forAllComboboxes(combobox => {
1680      if (this.hasInvalidClass) {
1681        combobox.classList.remove(this.invalidClass);
1682      }
1683
1684      combobox.removeAttribute("aria-invalid");
1685      combobox.removeAttribute("aria-errormessage");
1686    });
1687  }
1688
1689  _markInvalid() {
1690    if (this._valueIsValid) return
1691
1692    this._forAllComboboxes(combobox => {
1693      if (this.hasInvalidClass) {
1694        combobox.classList.add(this.invalidClass);
1695      }
1696
1697      combobox.setAttribute("aria-invalid", true);
1698      combobox.setAttribute("aria-errormessage", `Please select a valid option for ${combobox.name}`);
1699    });
1700  }
1701
1702  get _valueIsValid() {
1703    return !this._valueIsInvalid
1704  }
1705
1706  // +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
1707  // because the `required` attribute is only forwarded to the `comboboxTarget` element
1708  get _valueIsInvalid() {
1709    const isRequiredAndEmpty = this.comboboxTarget.required && this._hasEmptyFieldValue;
1710    return isRequiredAndEmpty
1711  }
1712};
1713
1714window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0; // ms, for testing purposes
1715
1716const concerns = [
1717  Controller,
1718  Combobox.Actors,
1719  Combobox.Announcements,
1720  Combobox.AsyncLoading,
1721  Combobox.Autocomplete,
1722  Combobox.Callbacks,
1723  Combobox.Dialog,
1724  Combobox.Events,
1725  Combobox.Filtering,
1726  Combobox.FormField,
1727  Combobox.Multiselect,
1728  Combobox.Navigation,
1729  Combobox.NewOptions,
1730  Combobox.Options,
1731  Combobox.Selection,
1732  Combobox.Toggle,
1733  Combobox.Validity
1734];
1735
1736class HwComboboxController extends Concerns(...concerns) {
1737  static classes = [ "invalid", "selected" ]
1738  static targets = [
1739    "announcer",
1740    "combobox",
1741    "chipDismisser",
1742    "closer",
1743    "dialog", "dialogCombobox", "dialogFocusTrap", "dialogListbox",
1744    "endOfOptionsStream",
1745    "handle",
1746    "hiddenField",
1747    "listbox",
1748    "mainWrapper"
1749  ]
1750
1751  static values = {
1752    asyncSrc: String,
1753    autocompletableAttribute: String,
1754    autocomplete: String,
1755    debounceInterval: Number,
1756    expanded: Boolean,
1757    filterableAttribute: String,
1758    nameWhenNew: String,
1759    originalName: String,
1760    prefilledDisplay: String,
1761    selectionChipSrc: String,
1762    smallViewportMaxWidth: String
1763  }
1764
1765  initialize() {
1766    this._initializeActors();
1767    this._initializeFiltering();
1768    this._initializeCallbacks();
1769  }
1770
1771  connect() {
1772    this.idempotentConnect();
1773  }
1774
1775  idempotentConnect() {
1776    this._connectSelection();
1777    this._connectMultiselect();
1778    this._connectListAutocomplete();
1779    this._connectDialog();
1780  }
1781
1782  disconnect() {
1783    this._disconnectDialog();
1784  }
1785
1786  expandedValueChanged() {
1787    if (this.expandedValue) {
1788      this._expand();
1789    } else {
1790      this._collapse();
1791    }
1792  }
1793
1794  async endOfOptionsStreamTargetConnected(element) {
1795    if (element.dataset.callbackId) {
1796      this._runCallback(element);
1797    } else {
1798      this._preselectSingle();
1799    }
1800  }
1801
1802  async _runCallback(element) {
1803    const callbackId = element.dataset.callbackId;
1804
1805    if (this._callbackAttemptsExceeded(callbackId)) {
1806      return this._dequeueCallback(callbackId)
1807    } else {
1808      this._recordCallbackAttempt(callbackId);
1809    }
1810
1811    if (this._isNextCallback(callbackId)) {
1812      const inputType = element.dataset.inputType;
1813      const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
1814
1815      if (delay) await sleep(delay);
1816      this._dequeueCallback(callbackId);
1817      this._resetMultiselectionMarks();
1818
1819      if (inputType === "hw:multiselectSync") {
1820        this.open();
1821      } else if (inputType !== "hw:lockInSelection") {
1822        this._selectOnQuery(inputType);
1823      }
1824    } else {
1825      await nextRepaint();
1826      this._runCallback(element);
1827    }
1828  }
1829
1830  closerTargetConnected() {
1831    this.close("hw:asyncCloser");
1832  }
1833
1834  // Use +_printStack+ for debugging purposes
1835  _printStack() {
1836    const err = new Error();
1837    console.log(err.stack || err.stacktrace);
1838  }
1839}
1840
1841export { HwComboboxController as default };

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.