mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-12 20:29:12 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
/***********************************************************************
|
||||
* DestinationInput - Keyboard entry for destinations that are not one *
|
||||
* of the built-in capitals. *
|
||||
* *
|
||||
* Opened from the "Input Destination" entry at the top of the *
|
||||
* destination menu. The dialog runs in two stages: *
|
||||
* *
|
||||
* 1. Form - state/region and city are typed, country is picked *
|
||||
* from the menu. Any one of the three is enough. *
|
||||
* 2. Results - the matches the geocoder returned, one per row. *
|
||||
* Picking one and pressing Select flies there. *
|
||||
* *
|
||||
* The dialog owns the keyboard while it is open: flight input already *
|
||||
* ignores events from form fields, and the shortcuts below stop at *
|
||||
* the dialog so a typed letter never reaches the simulator. *
|
||||
***********************************************************************/
|
||||
var DestinationInput = (function () {
|
||||
|
||||
var dialog, formStage, resultsStage;
|
||||
var stateInput, cityInput, countrySelect;
|
||||
var formMessage, resultsMessage, resultList;
|
||||
var searchBtn, selectBtn;
|
||||
|
||||
var onSelect = null;
|
||||
var onOpen = null;
|
||||
var onClose = null;
|
||||
|
||||
var results = [];
|
||||
var activeIndex = -1;
|
||||
var searching = false;
|
||||
var isOpen = false;
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Country menu
|
||||
------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Fills the country menu from the capitals dataset, which is the
|
||||
* only list of country names the page ships with. Leaving the menu
|
||||
* on its first entry searches without a country filter.
|
||||
*/
|
||||
function populateCountries() {
|
||||
if (typeof CAPITALS_BY_REGION === 'undefined') return;
|
||||
|
||||
var seen = {};
|
||||
var names = [];
|
||||
|
||||
for (var i = 0; i < CAPITALS_BY_REGION.length; i++) {
|
||||
var capitals = CAPITALS_BY_REGION[i].capitals;
|
||||
for (var j = 0; j < capitals.length; j++) {
|
||||
var country = capitals[j].country;
|
||||
if (seen[country]) continue;
|
||||
seen[country] = true;
|
||||
names.push(country);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
for (var k = 0; k < names.length; k++) {
|
||||
var option = document.createElement('option');
|
||||
option.value = names[k];
|
||||
option.textContent = names[k];
|
||||
countrySelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Stage switching
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function showForm() {
|
||||
formStage.classList.remove('hidden');
|
||||
resultsStage.classList.add('hidden');
|
||||
resultsMessage.textContent = '';
|
||||
}
|
||||
|
||||
function showResults() {
|
||||
formStage.classList.add('hidden');
|
||||
resultsStage.classList.remove('hidden');
|
||||
formMessage.textContent = '';
|
||||
}
|
||||
|
||||
function setSearching(active) {
|
||||
searching = active;
|
||||
searchBtn.disabled = active;
|
||||
searchBtn.textContent = active ? 'Searching...' : 'Search';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Results list
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function renderResults() {
|
||||
resultList.innerHTML = '';
|
||||
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var row = document.createElement('li');
|
||||
row.className = 'dest-result';
|
||||
// Focus stays on the listbox, so each option needs an id for
|
||||
// the list's aria-activedescendant to point a reader at it
|
||||
row.id = 'dest-result-' + i;
|
||||
row.setAttribute('role', 'option');
|
||||
row.setAttribute('aria-selected', 'false');
|
||||
row.dataset.index = String(i);
|
||||
row.textContent = results[i].label;
|
||||
resultList.appendChild(row);
|
||||
}
|
||||
|
||||
setActiveIndex(results.length ? 0 : -1);
|
||||
}
|
||||
|
||||
function setActiveIndex(index) {
|
||||
activeIndex = index;
|
||||
var rows = resultList.children;
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var active = i === index;
|
||||
rows[i].classList.toggle('active', active);
|
||||
rows[i].setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
}
|
||||
|
||||
selectBtn.disabled = index < 0;
|
||||
|
||||
// Point the listbox at the option the arrow keys landed on, and
|
||||
// drop the reference entirely when nothing is active
|
||||
if (index >= 0 && rows[index]) {
|
||||
resultList.setAttribute('aria-activedescendant', rows[index].id);
|
||||
if (rows[index].scrollIntoView) {
|
||||
rows[index].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else {
|
||||
resultList.removeAttribute('aria-activedescendant');
|
||||
}
|
||||
}
|
||||
|
||||
function moveActive(step) {
|
||||
if (!results.length) return;
|
||||
var next = activeIndex + step;
|
||||
if (next < 0) next = 0;
|
||||
if (next > results.length - 1) next = results.length - 1;
|
||||
setActiveIndex(next);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Actions
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function runSearch() {
|
||||
if (searching) return;
|
||||
|
||||
formMessage.textContent = '';
|
||||
setSearching(true);
|
||||
|
||||
Geocoder.search({
|
||||
state: stateInput.value,
|
||||
city: cityInput.value,
|
||||
country: countrySelect.value
|
||||
}, function (error, found) {
|
||||
setSearching(false);
|
||||
|
||||
if (error) {
|
||||
formMessage.textContent = error;
|
||||
return;
|
||||
}
|
||||
|
||||
results = found;
|
||||
renderResults();
|
||||
showResults();
|
||||
|
||||
resultsMessage.textContent = found.length
|
||||
? ''
|
||||
: 'No matching destination was found. Cancel to adjust the search.';
|
||||
|
||||
if (found.length) resultList.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function commitSelection() {
|
||||
if (activeIndex < 0 || !results[activeIndex]) return;
|
||||
var chosen = results[activeIndex];
|
||||
close();
|
||||
if (onSelect) onSelect(chosen);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Open / close
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function open() {
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
|
||||
results = [];
|
||||
activeIndex = -1;
|
||||
// Rows from the previous search are stale the moment the form
|
||||
// reopens, so nothing should still be pointing at one of them
|
||||
resultList.removeAttribute('aria-activedescendant');
|
||||
formMessage.textContent = '';
|
||||
setSearching(false);
|
||||
showForm();
|
||||
|
||||
dialog.classList.remove('hidden');
|
||||
if (onOpen) onOpen();
|
||||
stateInput.focus();
|
||||
stateInput.select();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!isOpen) return;
|
||||
isOpen = false;
|
||||
|
||||
dialog.classList.add('hidden');
|
||||
if (onClose) onClose();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Wiring
|
||||
------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Binds the dialog. options:
|
||||
* onSelect(destination) - a result the user picked
|
||||
* onOpen() - the dialog became visible
|
||||
* onClose() - the dialog was dismissed or committed
|
||||
*/
|
||||
function init(options) {
|
||||
options = options || {};
|
||||
onSelect = options.onSelect || null;
|
||||
onOpen = options.onOpen || null;
|
||||
onClose = options.onClose || null;
|
||||
|
||||
dialog = document.getElementById('destination-dialog');
|
||||
if (!dialog) return;
|
||||
|
||||
formStage = document.getElementById('destination-form');
|
||||
resultsStage = document.getElementById('destination-results');
|
||||
stateInput = document.getElementById('dest-state');
|
||||
cityInput = document.getElementById('dest-city');
|
||||
countrySelect = document.getElementById('dest-country');
|
||||
formMessage = document.getElementById('dest-form-message');
|
||||
resultsMessage = document.getElementById('dest-results-message');
|
||||
resultList = document.getElementById('dest-result-list');
|
||||
searchBtn = document.getElementById('dest-search');
|
||||
selectBtn = document.getElementById('dest-select');
|
||||
|
||||
populateCountries();
|
||||
|
||||
searchBtn.addEventListener('click', runSearch);
|
||||
selectBtn.addEventListener('click', commitSelection);
|
||||
document.getElementById('dest-cancel').addEventListener('click', close);
|
||||
document.getElementById('dest-results-cancel').addEventListener('click', close);
|
||||
|
||||
// Enter submits from any field in the form stage
|
||||
formStage.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
runSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Click a row to highlight it, double click to fly straight there
|
||||
resultList.addEventListener('click', function (e) {
|
||||
var row = e.target.closest ? e.target.closest('.dest-result') : null;
|
||||
if (!row) return;
|
||||
setActiveIndex(parseInt(row.dataset.index, 10));
|
||||
});
|
||||
|
||||
resultList.addEventListener('dblclick', function (e) {
|
||||
var row = e.target.closest ? e.target.closest('.dest-result') : null;
|
||||
if (!row) return;
|
||||
setActiveIndex(parseInt(row.dataset.index, 10));
|
||||
commitSelection();
|
||||
});
|
||||
|
||||
resultList.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
moveActive(1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
moveActive(-1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commitSelection();
|
||||
}
|
||||
});
|
||||
|
||||
// Escape closes from anywhere inside the dialog, and clicking the
|
||||
// backdrop outside the card does the same
|
||||
dialog.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.addEventListener('mousedown', function (e) {
|
||||
if (e.target === dialog) close();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
open: open,
|
||||
close: close,
|
||||
isOpen: function () { return isOpen; }
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user