mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Ran prettier
This commit is contained in:
parent
2b879e462f
commit
5b0baf7cd5
1 changed files with 216 additions and 205 deletions
|
|
@ -1,17 +1,17 @@
|
||||||
class NavigationSearch extends HTMLElement {
|
class NavigationSearch extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.selectedIndex = -1;
|
this.selectedIndex = -1;
|
||||||
this.matches = [];
|
this.matches = [];
|
||||||
this.debounceTimer = null;
|
this.debounceTimer = null;
|
||||||
|
|
||||||
this.render();
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
this.render();
|
||||||
this.shadowRoot.innerHTML = `
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
@ -183,143 +183,150 @@ class NavigationSearch extends HTMLElement {
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
const dialog = this.shadowRoot.querySelector("dialog");
|
||||||
|
const input = this.shadowRoot.querySelector(".search-input");
|
||||||
|
const resultsContainer =
|
||||||
|
this.shadowRoot.querySelector(".results-container");
|
||||||
|
|
||||||
|
// Global keyboard listener for "/"
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "/" && !this.isInputFocused() && !dialog.open) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.openMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input event
|
||||||
|
input.addEventListener("input", (e) => {
|
||||||
|
this.handleSearch(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.moveSelection(1);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.moveSelection(-1);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.selectCurrentItem();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
this.closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on result item
|
||||||
|
resultsContainer.addEventListener("click", (e) => {
|
||||||
|
const item = e.target.closest(".result-item");
|
||||||
|
if (item) {
|
||||||
|
const index = parseInt(item.dataset.index);
|
||||||
|
this.selectItem(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
dialog.addEventListener("click", (e) => {
|
||||||
|
if (e.target === dialog) {
|
||||||
|
this.closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
this.loadInitialData();
|
||||||
|
}
|
||||||
|
|
||||||
|
isInputFocused() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
return (
|
||||||
|
activeElement &&
|
||||||
|
(activeElement.tagName === "INPUT" ||
|
||||||
|
activeElement.tagName === "TEXTAREA" ||
|
||||||
|
activeElement.isContentEditable)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadInitialData() {
|
||||||
|
const itemsAttr = this.getAttribute("items");
|
||||||
|
if (itemsAttr) {
|
||||||
|
try {
|
||||||
|
this.allItems = JSON.parse(itemsAttr);
|
||||||
|
this.matches = this.allItems;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse items attribute:", e);
|
||||||
|
this.allItems = [];
|
||||||
|
this.matches = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch(query) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
const url = this.getAttribute("url");
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
// Fetch from API
|
||||||
|
this.fetchResults(url, query);
|
||||||
|
} else {
|
||||||
|
// Filter local items
|
||||||
|
this.filterLocalItems(query);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchResults(url, query) {
|
||||||
|
try {
|
||||||
|
const searchUrl = `${url}?q=${encodeURIComponent(query)}`;
|
||||||
|
const response = await fetch(searchUrl);
|
||||||
|
const data = await response.json();
|
||||||
|
this.matches = data.matches || [];
|
||||||
|
this.selectedIndex = this.matches.length > 0 ? 0 : -1;
|
||||||
|
this.renderResults();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch search results:", e);
|
||||||
|
this.matches = [];
|
||||||
|
this.renderResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterLocalItems(query) {
|
||||||
|
if (!query.trim()) {
|
||||||
|
this.matches = [];
|
||||||
|
} else {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
this.matches = (this.allItems || []).filter(
|
||||||
|
(item) =>
|
||||||
|
item.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
item.url.toLowerCase().includes(lowerQuery),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.selectedIndex = this.matches.length > 0 ? 0 : -1;
|
||||||
|
this.renderResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResults() {
|
||||||
|
const container = this.shadowRoot.querySelector(".results-container");
|
||||||
|
const input = this.shadowRoot.querySelector(".search-input");
|
||||||
|
|
||||||
|
if (this.matches.length === 0) {
|
||||||
|
const message = input.value.trim()
|
||||||
|
? "No results found"
|
||||||
|
: "Start typing to search...";
|
||||||
|
container.innerHTML = `<div class="no-results">${message}</div>`;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
container.innerHTML = this.matches
|
||||||
const dialog = this.shadowRoot.querySelector('dialog');
|
.map(
|
||||||
const input = this.shadowRoot.querySelector('.search-input');
|
(match, index) => `
|
||||||
const resultsContainer = this.shadowRoot.querySelector('.results-container');
|
|
||||||
|
|
||||||
// Global keyboard listener for "/"
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === '/' && !this.isInputFocused() && !dialog.open) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.openMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Input event
|
|
||||||
input.addEventListener('input', (e) => {
|
|
||||||
this.handleSearch(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keyboard navigation
|
|
||||||
input.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.moveSelection(1);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.moveSelection(-1);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.selectCurrentItem();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
this.closeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click on result item
|
|
||||||
resultsContainer.addEventListener('click', (e) => {
|
|
||||||
const item = e.target.closest('.result-item');
|
|
||||||
if (item) {
|
|
||||||
const index = parseInt(item.dataset.index);
|
|
||||||
this.selectItem(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close on backdrop click
|
|
||||||
dialog.addEventListener('click', (e) => {
|
|
||||||
if (e.target === dialog) {
|
|
||||||
this.closeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
this.loadInitialData();
|
|
||||||
}
|
|
||||||
|
|
||||||
isInputFocused() {
|
|
||||||
const activeElement = document.activeElement;
|
|
||||||
return activeElement && (
|
|
||||||
activeElement.tagName === 'INPUT' ||
|
|
||||||
activeElement.tagName === 'TEXTAREA' ||
|
|
||||||
activeElement.isContentEditable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadInitialData() {
|
|
||||||
const itemsAttr = this.getAttribute('items');
|
|
||||||
if (itemsAttr) {
|
|
||||||
try {
|
|
||||||
this.allItems = JSON.parse(itemsAttr);
|
|
||||||
this.matches = this.allItems;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse items attribute:', e);
|
|
||||||
this.allItems = [];
|
|
||||||
this.matches = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSearch(query) {
|
|
||||||
clearTimeout(this.debounceTimer);
|
|
||||||
|
|
||||||
this.debounceTimer = setTimeout(() => {
|
|
||||||
const url = this.getAttribute('url');
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
// Fetch from API
|
|
||||||
this.fetchResults(url, query);
|
|
||||||
} else {
|
|
||||||
// Filter local items
|
|
||||||
this.filterLocalItems(query);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchResults(url, query) {
|
|
||||||
try {
|
|
||||||
const searchUrl = `${url}?q=${encodeURIComponent(query)}`;
|
|
||||||
const response = await fetch(searchUrl);
|
|
||||||
const data = await response.json();
|
|
||||||
this.matches = data.matches || [];
|
|
||||||
this.selectedIndex = this.matches.length > 0 ? 0 : -1;
|
|
||||||
this.renderResults();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch search results:', e);
|
|
||||||
this.matches = [];
|
|
||||||
this.renderResults();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterLocalItems(query) {
|
|
||||||
if (!query.trim()) {
|
|
||||||
this.matches = [];
|
|
||||||
} else {
|
|
||||||
const lowerQuery = query.toLowerCase();
|
|
||||||
this.matches = (this.allItems || []).filter(item =>
|
|
||||||
item.name.toLowerCase().includes(lowerQuery) ||
|
|
||||||
item.url.toLowerCase().includes(lowerQuery)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.selectedIndex = this.matches.length > 0 ? 0 : -1;
|
|
||||||
this.renderResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderResults() {
|
|
||||||
const container = this.shadowRoot.querySelector('.results-container');
|
|
||||||
const input = this.shadowRoot.querySelector('.search-input');
|
|
||||||
|
|
||||||
if (this.matches.length === 0) {
|
|
||||||
const message = input.value.trim() ? 'No results found' : 'Start typing to search...';
|
|
||||||
container.innerHTML = `<div class="no-results">${message}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = this.matches.map((match, index) => `
|
|
||||||
<div
|
<div
|
||||||
class="result-item ${index === this.selectedIndex ? 'selected' : ''}"
|
class="result-item ${index === this.selectedIndex ? "selected" : ""}"
|
||||||
data-index="${index}"
|
data-index="${index}"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="${index === this.selectedIndex}"
|
aria-selected="${index === this.selectedIndex}"
|
||||||
|
|
@ -329,73 +336,77 @@ class NavigationSearch extends HTMLElement {
|
||||||
<div class="result-url">${this.escapeHtml(match.url)}</div>
|
<div class="result-url">${this.escapeHtml(match.url)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
// Scroll selected item into view
|
// Scroll selected item into view
|
||||||
if (this.selectedIndex >= 0) {
|
if (this.selectedIndex >= 0) {
|
||||||
const selectedItem = container.children[this.selectedIndex];
|
const selectedItem = container.children[this.selectedIndex];
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
selectedItem.scrollIntoView({ block: 'nearest' });
|
selectedItem.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
moveSelection(direction) {
|
moveSelection(direction) {
|
||||||
const newIndex = this.selectedIndex + direction;
|
const newIndex = this.selectedIndex + direction;
|
||||||
if (newIndex >= 0 && newIndex < this.matches.length) {
|
if (newIndex >= 0 && newIndex < this.matches.length) {
|
||||||
this.selectedIndex = newIndex;
|
this.selectedIndex = newIndex;
|
||||||
this.renderResults();
|
this.renderResults();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectCurrentItem() {
|
selectCurrentItem() {
|
||||||
if (this.selectedIndex >= 0 && this.selectedIndex < this.matches.length) {
|
if (this.selectedIndex >= 0 && this.selectedIndex < this.matches.length) {
|
||||||
this.selectItem(this.selectedIndex);
|
this.selectItem(this.selectedIndex);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectItem(index) {
|
selectItem(index) {
|
||||||
const match = this.matches[index];
|
const match = this.matches[index];
|
||||||
if (match) {
|
if (match) {
|
||||||
// Dispatch custom event
|
// Dispatch custom event
|
||||||
this.dispatchEvent(new CustomEvent('select', {
|
this.dispatchEvent(
|
||||||
detail: match,
|
new CustomEvent("select", {
|
||||||
bubbles: true,
|
detail: match,
|
||||||
composed: true
|
bubbles: true,
|
||||||
}));
|
composed: true,
|
||||||
|
}),
|
||||||
// Navigate to URL
|
);
|
||||||
window.location.href = match.url;
|
|
||||||
|
|
||||||
this.closeMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openMenu() {
|
// Navigate to URL
|
||||||
const dialog = this.shadowRoot.querySelector('dialog');
|
window.location.href = match.url;
|
||||||
const input = this.shadowRoot.querySelector('.search-input');
|
|
||||||
|
|
||||||
dialog.showModal();
|
|
||||||
input.value = '';
|
|
||||||
input.focus();
|
|
||||||
|
|
||||||
// Reset state - start with no items shown
|
|
||||||
this.matches = [];
|
|
||||||
this.selectedIndex = -1;
|
|
||||||
this.renderResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeMenu() {
|
this.closeMenu();
|
||||||
const dialog = this.shadowRoot.querySelector('dialog');
|
|
||||||
dialog.close();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
escapeHtml(text) {
|
openMenu() {
|
||||||
const div = document.createElement('div');
|
const dialog = this.shadowRoot.querySelector("dialog");
|
||||||
div.textContent = text;
|
const input = this.shadowRoot.querySelector(".search-input");
|
||||||
return div.innerHTML;
|
|
||||||
}
|
dialog.showModal();
|
||||||
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
// Reset state - start with no items shown
|
||||||
|
this.matches = [];
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.renderResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeMenu() {
|
||||||
|
const dialog = this.shadowRoot.querySelector("dialog");
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the custom element
|
// Register the custom element
|
||||||
customElements.define('navigation-search', NavigationSearch);
|
customElements.define("navigation-search", NavigationSearch);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue