@github/auto-complete-element
Version:
Auto-complete input values from server results
616 lines (609 loc) • 23.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// node_modules/@github/combobox-nav/dist/index.js
var Combobox = class {
static {
__name(this, "Combobox");
}
constructor(input, list, { tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions } = {}) {
this.input = input;
this.list = list;
this.tabInsertsSuggestions = tabInsertsSuggestions !== null && tabInsertsSuggestions !== void 0 ? tabInsertsSuggestions : true;
this.defaultFirstOption = defaultFirstOption !== null && defaultFirstOption !== void 0 ? defaultFirstOption : false;
this.scrollIntoViewOptions = scrollIntoViewOptions;
this.isComposing = false;
if (!list.id) {
list.id = `combobox-${Math.random().toString().slice(2, 6)}`;
}
this.ctrlBindings = !!navigator.userAgent.match(/Macintosh/);
this.keyboardEventHandler = (event) => keyboardBindings(event, this);
this.compositionEventHandler = (event) => trackComposition(event, this);
this.inputHandler = this.clearSelection.bind(this);
input.setAttribute("role", "combobox");
input.setAttribute("aria-controls", list.id);
input.setAttribute("aria-expanded", "false");
input.setAttribute("aria-autocomplete", "list");
input.setAttribute("aria-haspopup", "listbox");
}
destroy() {
this.clearSelection();
this.stop();
this.input.removeAttribute("role");
this.input.removeAttribute("aria-controls");
this.input.removeAttribute("aria-expanded");
this.input.removeAttribute("aria-autocomplete");
this.input.removeAttribute("aria-haspopup");
}
start() {
this.input.setAttribute("aria-expanded", "true");
this.input.addEventListener("compositionstart", this.compositionEventHandler);
this.input.addEventListener("compositionend", this.compositionEventHandler);
this.input.addEventListener("input", this.inputHandler);
this.input.addEventListener("keydown", this.keyboardEventHandler);
this.list.addEventListener("click", commitWithElement);
this.indicateDefaultOption();
}
stop() {
this.clearSelection();
this.input.setAttribute("aria-expanded", "false");
this.input.removeEventListener("compositionstart", this.compositionEventHandler);
this.input.removeEventListener("compositionend", this.compositionEventHandler);
this.input.removeEventListener("input", this.inputHandler);
this.input.removeEventListener("keydown", this.keyboardEventHandler);
this.list.removeEventListener("click", commitWithElement);
}
indicateDefaultOption() {
var _a;
if (this.defaultFirstOption) {
(_a = Array.from(this.list.querySelectorAll('[role="option"]:not([aria-disabled="true"])')).filter(visible)[0]) === null || _a === void 0 ? void 0 : _a.setAttribute("data-combobox-option-default", "true");
}
}
navigate(indexDiff = 1) {
const focusEl = Array.from(this.list.querySelectorAll('[aria-selected="true"]')).filter(visible)[0];
const els = Array.from(this.list.querySelectorAll('[role="option"]')).filter(visible);
const focusIndex = els.indexOf(focusEl);
if (focusIndex === els.length - 1 && indexDiff === 1 || focusIndex === 0 && indexDiff === -1) {
this.clearSelection();
this.input.focus();
return;
}
let indexOfItem = indexDiff === 1 ? 0 : els.length - 1;
if (focusEl && focusIndex >= 0) {
const newIndex = focusIndex + indexDiff;
if (newIndex >= 0 && newIndex < els.length)
indexOfItem = newIndex;
}
const target = els[indexOfItem];
if (!target)
return;
for (const el of els) {
el.removeAttribute("data-combobox-option-default");
if (target === el) {
this.input.setAttribute("aria-activedescendant", target.id);
target.setAttribute("aria-selected", "true");
fireSelectEvent(target);
target.scrollIntoView(this.scrollIntoViewOptions);
} else {
el.removeAttribute("aria-selected");
}
}
}
clearSelection() {
this.input.removeAttribute("aria-activedescendant");
for (const el of this.list.querySelectorAll('[aria-selected="true"]')) {
el.removeAttribute("aria-selected");
}
this.indicateDefaultOption();
}
};
function keyboardBindings(event, combobox) {
if (event.shiftKey || event.metaKey || event.altKey)
return;
if (!combobox.ctrlBindings && event.ctrlKey)
return;
if (combobox.isComposing)
return;
switch (event.key) {
case "Enter":
if (commit(combobox.input, combobox.list)) {
event.preventDefault();
}
break;
case "Tab":
if (combobox.tabInsertsSuggestions && commit(combobox.input, combobox.list)) {
event.preventDefault();
}
break;
case "Escape":
combobox.clearSelection();
break;
case "ArrowDown":
combobox.navigate(1);
event.preventDefault();
break;
case "ArrowUp":
combobox.navigate(-1);
event.preventDefault();
break;
case "n":
if (combobox.ctrlBindings && event.ctrlKey) {
combobox.navigate(1);
event.preventDefault();
}
break;
case "p":
if (combobox.ctrlBindings && event.ctrlKey) {
combobox.navigate(-1);
event.preventDefault();
}
break;
default:
if (event.ctrlKey)
break;
combobox.clearSelection();
}
}
__name(keyboardBindings, "keyboardBindings");
function commitWithElement(event) {
if (!(event.target instanceof Element))
return;
const target = event.target.closest('[role="option"]');
if (!target)
return;
if (target.getAttribute("aria-disabled") === "true")
return;
fireCommitEvent(target, { event });
}
__name(commitWithElement, "commitWithElement");
function commit(input, list) {
const target = list.querySelector('[aria-selected="true"], [data-combobox-option-default="true"]');
if (!target)
return false;
if (target.getAttribute("aria-disabled") === "true")
return true;
target.click();
return true;
}
__name(commit, "commit");
function fireCommitEvent(target, detail) {
target.dispatchEvent(new CustomEvent("combobox-commit", { bubbles: true, detail }));
}
__name(fireCommitEvent, "fireCommitEvent");
function fireSelectEvent(target) {
target.dispatchEvent(new Event("combobox-select", { bubbles: true }));
}
__name(fireSelectEvent, "fireSelectEvent");
function visible(el) {
return !el.hidden && !(el instanceof HTMLInputElement && el.type === "hidden") && (el.offsetWidth > 0 || el.offsetHeight > 0);
}
__name(visible, "visible");
function trackComposition(event, combobox) {
combobox.isComposing = event.type === "compositionstart";
const list = document.getElementById(combobox.input.getAttribute("aria-controls") || "");
if (!list)
return;
combobox.clearSelection();
}
__name(trackComposition, "trackComposition");
// dist/debounce.js
function debounce(callback, wait = 0) {
let timeout;
return function(...Rest) {
clearTimeout(timeout);
timeout = window.setTimeout(() => {
clearTimeout(timeout);
callback(...Rest);
}, wait);
};
}
__name(debounce, "debounce");
// dist/autocomplete.js
var SCREEN_READER_DELAY = window.testScreenReaderDelay || 100;
var Autocomplete = class {
static {
__name(this, "Autocomplete");
}
constructor(container, input, results, autoselectEnabled = false) {
var _a;
this.container = container;
this.input = input;
this.results = results;
this.combobox = new Combobox(input, results, {
defaultFirstOption: autoselectEnabled
});
this.feedback = container.getRootNode().getElementById(`${this.results.id}-feedback`);
this.autoselectEnabled = autoselectEnabled;
this.clearButton = container.getRootNode().getElementById(`${this.input.id || this.input.name}-clear`);
this.clientOptions = results.querySelectorAll("[role=option]");
if (this.feedback) {
this.feedback.setAttribute("aria-live", "polite");
this.feedback.setAttribute("aria-atomic", "true");
}
if (this.clearButton && !this.clearButton.getAttribute("aria-label")) {
const labelElem = document.querySelector(`label[for="${this.input.name}"]`);
this.clearButton.setAttribute("aria-label", `clear:`);
this.clearButton.setAttribute("aria-labelledby", `${this.clearButton.id} ${(labelElem === null || labelElem === void 0 ? void 0 : labelElem.id) || ""}`);
}
if (!this.input.getAttribute("aria-expanded")) {
this.input.setAttribute("aria-expanded", "false");
}
if (this.results.popover) {
if (this.results.matches(":popover-open")) {
this.results.hidePopover();
}
} else {
this.results.hidden = true;
}
if (!this.results.getAttribute("aria-label")) {
this.results.setAttribute("aria-label", "results");
}
this.input.setAttribute("autocomplete", "off");
this.input.setAttribute("spellcheck", "false");
this.interactingWithList = false;
this.onInputChange = debounce(this.onInputChange.bind(this), 300);
this.onResultsMouseDown = this.onResultsMouseDown.bind(this);
this.onInputBlur = this.onInputBlur.bind(this);
this.onInputFocus = this.onInputFocus.bind(this);
this.onKeydown = this.onKeydown.bind(this);
this.onCommit = this.onCommit.bind(this);
this.handleClear = this.handleClear.bind(this);
this.input.addEventListener("keydown", this.onKeydown);
this.input.addEventListener("focus", this.onInputFocus);
this.input.addEventListener("blur", this.onInputBlur);
this.input.addEventListener("input", this.onInputChange);
this.results.addEventListener("mousedown", this.onResultsMouseDown);
this.results.addEventListener("combobox-commit", this.onCommit);
(_a = this.clearButton) === null || _a === void 0 ? void 0 : _a.addEventListener("click", this.handleClear);
}
destroy() {
this.input.removeEventListener("keydown", this.onKeydown);
this.input.removeEventListener("focus", this.onInputFocus);
this.input.removeEventListener("blur", this.onInputBlur);
this.input.removeEventListener("input", this.onInputChange);
this.results.removeEventListener("mousedown", this.onResultsMouseDown);
this.results.removeEventListener("combobox-commit", this.onCommit);
}
handleClear(event) {
event.preventDefault();
if (this.input.getAttribute("aria-expanded") === "true") {
this.input.setAttribute("aria-expanded", "false");
this.updateFeedbackForScreenReaders("Results hidden.");
}
this.input.value = "";
this.container.value = "";
this.input.focus();
this.input.dispatchEvent(new Event("change"));
this.close();
}
onKeydown(event) {
if (event.key === "Escape" && this.container.open) {
this.close();
event.stopPropagation();
event.preventDefault();
} else if (event.altKey && event.key === "ArrowUp" && this.container.open) {
this.close();
event.stopPropagation();
event.preventDefault();
} else if (event.altKey && event.key === "ArrowDown" && !this.container.open) {
if (!this.input.value.trim())
return;
this.open();
event.stopPropagation();
event.preventDefault();
}
}
onInputFocus() {
if (this.interactingWithList)
return;
this.fetchResults();
}
onInputBlur() {
if (this.interactingWithList)
return;
this.close();
}
onCommit({ target }) {
const selected = target;
if (!(selected instanceof HTMLElement))
return;
this.close();
if (selected instanceof HTMLAnchorElement)
return;
const value = selected.getAttribute("data-autocomplete-value") || selected.textContent;
this.updateFeedbackForScreenReaders(`${selected.textContent || ""} selected.`);
this.container.value = value;
if (!value) {
this.updateFeedbackForScreenReaders(`Results hidden.`);
}
}
onResultsMouseDown() {
this.interactingWithList = true;
}
onInputChange() {
if (this.feedback && this.feedback.textContent) {
this.feedback.textContent = "";
}
this.container.removeAttribute("value");
this.fetchResults();
}
identifyOptions() {
let id = 0;
for (const el of this.results.querySelectorAll('[role="option"]:not([id])')) {
el.id = `${this.results.id}-option-${id++}`;
}
}
updateFeedbackForScreenReaders(inputString) {
setTimeout(() => {
if (this.feedback) {
this.feedback.textContent = inputString;
}
}, SCREEN_READER_DELAY);
}
fetchResults() {
const query = this.input.value.trim();
if (!query && !this.container.fetchOnEmpty) {
this.close();
return;
}
const src = this.container.src;
if (!src)
return;
const url = new URL(src, window.location.href);
const params = new URLSearchParams(url.search.slice(1));
params.append("q", query);
url.search = params.toString();
this.container.dispatchEvent(new CustomEvent("loadstart"));
this.container.fetchResult(url).then((html) => {
this.results.innerHTML = html;
this.identifyOptions();
this.combobox.indicateDefaultOption();
const allNewOptions = this.results.querySelectorAll('[role="option"]');
const hasResults = !!allNewOptions.length || !!this.results.querySelectorAll('[data-no-result-found="true"]').length;
const numOptions = allNewOptions.length;
const [firstOption] = allNewOptions;
const firstOptionValue = firstOption === null || firstOption === void 0 ? void 0 : firstOption.textContent;
if (this.autoselectEnabled && firstOptionValue) {
this.updateFeedbackForScreenReaders(`${numOptions} results. ${firstOptionValue} is the top result: Press Enter to activate.`);
} else {
this.updateFeedbackForScreenReaders(`${numOptions || "No"} results.`);
}
hasResults ? this.open() : this.close();
this.container.dispatchEvent(new CustomEvent("load"));
this.container.dispatchEvent(new CustomEvent("loadend"));
}).catch(() => {
this.container.dispatchEvent(new CustomEvent("error"));
this.container.dispatchEvent(new CustomEvent("loadend"));
});
}
open() {
const isHidden = this.results.popover ? !this.results.matches(":popover-open") : this.results.hidden;
if (isHidden) {
this.combobox.start();
if (this.results.popover) {
this.results.showPopover();
} else {
this.results.hidden = false;
}
}
this.container.open = true;
this.interactingWithList = true;
}
close() {
const isVisible = this.results.popover ? this.results.matches(":popover-open") : !this.results.hidden;
if (isVisible) {
this.combobox.stop();
if (this.results.popover) {
this.results.hidePopover();
} else {
this.results.hidden = true;
}
}
this.container.open = false;
this.interactingWithList = false;
}
};
// dist/auto-complete-element.js
var __classPrivateFieldGet = function(receiver, state2, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state2 === "function" ? receiver !== state2 || !f : !state2.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state2.get(receiver);
};
var __classPrivateFieldSet = function(receiver, state2, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state2 === "function" ? receiver !== state2 || !f : !state2.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state2.set(receiver, value), value;
};
var __rest = function(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var _AutoCompleteElement_instances;
var _AutoCompleteElement_forElement;
var _AutoCompleteElement_inputElement;
var _AutoCompleteElement_reattachState;
var _AutoCompleteElement_requestController;
var HTMLElement2 = globalThis.HTMLElement || null;
var AutoCompleteEvent = class extends Event {
static {
__name(this, "AutoCompleteEvent");
}
constructor(type, _a) {
var { relatedTarget } = _a, init = __rest(_a, ["relatedTarget"]);
super(type, init);
this.relatedTarget = relatedTarget;
}
};
var state = /* @__PURE__ */ new WeakMap();
var cspTrustedTypesPolicyPromise = null;
var AutoCompleteElement = class extends HTMLElement2 {
static {
__name(this, "AutoCompleteElement");
}
constructor() {
super(...arguments);
_AutoCompleteElement_instances.add(this);
_AutoCompleteElement_forElement.set(this, null);
_AutoCompleteElement_inputElement.set(this, null);
_AutoCompleteElement_requestController.set(this, void 0);
}
static define(tag = "auto-complete", registry = customElements) {
registry.define(tag, this);
return this;
}
static setCSPTrustedTypesPolicy(policy) {
cspTrustedTypesPolicyPromise = policy === null ? policy : Promise.resolve(policy);
}
get forElement() {
var _a;
if ((_a = __classPrivateFieldGet(this, _AutoCompleteElement_forElement, "f")) === null || _a === void 0 ? void 0 : _a.isConnected) {
return __classPrivateFieldGet(this, _AutoCompleteElement_forElement, "f");
}
const id = this.getAttribute("for");
const root2 = this.getRootNode();
if (id && (root2 instanceof Document || root2 instanceof ShadowRoot)) {
return root2.getElementById(id);
}
return null;
}
set forElement(element) {
__classPrivateFieldSet(this, _AutoCompleteElement_forElement, element, "f");
this.setAttribute("for", "");
}
get inputElement() {
var _a;
if ((_a = __classPrivateFieldGet(this, _AutoCompleteElement_inputElement, "f")) === null || _a === void 0 ? void 0 : _a.isConnected) {
return __classPrivateFieldGet(this, _AutoCompleteElement_inputElement, "f");
}
return this.querySelector("input");
}
set inputElement(input) {
__classPrivateFieldSet(this, _AutoCompleteElement_inputElement, input, "f");
__classPrivateFieldGet(this, _AutoCompleteElement_instances, "m", _AutoCompleteElement_reattachState).call(this);
}
connectedCallback() {
if (!this.isConnected)
return;
__classPrivateFieldGet(this, _AutoCompleteElement_instances, "m", _AutoCompleteElement_reattachState).call(this);
new MutationObserver(() => {
if (!state.get(this)) {
__classPrivateFieldGet(this, _AutoCompleteElement_instances, "m", _AutoCompleteElement_reattachState).call(this);
}
}).observe(this, { subtree: true, childList: true });
}
disconnectedCallback() {
const autocomplete = state.get(this);
if (autocomplete) {
autocomplete.destroy();
state.delete(this);
}
}
get src() {
return this.getAttribute("src") || "";
}
set src(url) {
this.setAttribute("src", url);
}
get value() {
return this.getAttribute("value") || "";
}
set value(value) {
this.setAttribute("value", value);
}
get open() {
return this.hasAttribute("open");
}
set open(value) {
if (value) {
this.setAttribute("open", "");
} else {
this.removeAttribute("open");
}
}
get fetchOnEmpty() {
return this.hasAttribute("fetch-on-empty");
}
set fetchOnEmpty(fetchOnEmpty) {
this.toggleAttribute("fetch-on-empty", fetchOnEmpty);
}
async fetchResult(url) {
var _a;
(_a = __classPrivateFieldGet(this, _AutoCompleteElement_requestController, "f")) === null || _a === void 0 ? void 0 : _a.abort();
const { signal } = __classPrivateFieldSet(this, _AutoCompleteElement_requestController, new AbortController(), "f");
const res = await fetch(url.toString(), {
signal,
headers: {
Accept: "text/fragment+html"
}
});
if (!res.ok) {
throw new Error(await res.text());
}
if (cspTrustedTypesPolicyPromise) {
const cspTrustedTypesPolicy = await cspTrustedTypesPolicyPromise;
return cspTrustedTypesPolicy.createHTML(await res.text(), res);
}
return await res.text();
}
static get observedAttributes() {
return ["open", "value", "for"];
}
attributeChangedCallback(name, oldValue, newValue) {
var _a, _b;
if (oldValue === newValue)
return;
const autocomplete = state.get(this);
if (!autocomplete)
return;
if (this.forElement !== ((_a = state.get(this)) === null || _a === void 0 ? void 0 : _a.results) || this.inputElement !== ((_b = state.get(this)) === null || _b === void 0 ? void 0 : _b.input)) {
__classPrivateFieldGet(this, _AutoCompleteElement_instances, "m", _AutoCompleteElement_reattachState).call(this);
}
switch (name) {
case "open":
newValue === null ? autocomplete.close() : autocomplete.open();
break;
case "value":
if (newValue !== null) {
autocomplete.input.value = newValue;
}
this.dispatchEvent(new AutoCompleteEvent("auto-complete-change", {
bubbles: true,
relatedTarget: autocomplete.input
}));
break;
}
}
};
_AutoCompleteElement_forElement = /* @__PURE__ */ new WeakMap(), _AutoCompleteElement_inputElement = /* @__PURE__ */ new WeakMap(), _AutoCompleteElement_requestController = /* @__PURE__ */ new WeakMap(), _AutoCompleteElement_instances = /* @__PURE__ */ new WeakSet(), _AutoCompleteElement_reattachState = /* @__PURE__ */ __name(function _AutoCompleteElement_reattachState2() {
var _a;
(_a = state.get(this)) === null || _a === void 0 ? void 0 : _a.destroy();
const { forElement, inputElement } = this;
if (!forElement || !inputElement)
return;
const autoselectEnabled = this.getAttribute("data-autoselect") === "true";
state.set(this, new Autocomplete(this, inputElement, forElement, autoselectEnabled));
forElement.setAttribute("role", "listbox");
}, "_AutoCompleteElement_reattachState");
// dist/auto-complete-element-define.js
var root = typeof globalThis !== "undefined" ? globalThis : window;
try {
root.AutocompleteElement = root.AutoCompleteElement = AutoCompleteElement.define();
} catch (e) {
if (!(root.DOMException && e instanceof DOMException && e.name === "NotSupportedError") && !(e instanceof ReferenceError)) {
throw e;
}
}
// dist/index.js
var index_default = AutoCompleteElement;
export {
AutoCompleteElement,
AutoCompleteEvent,
index_default as default
};