type HistoryEntry = { id: number; url: string; title: string };
type SearchShortcut = { description: string; url: string };
export class DOMRenderer {
private historyListElement: HTMLElement | null = null;
private shortcutsElement: HTMLElement | null = null;
constructor() {
this.historyListElement = document.getElementById('historyList');
this.shortcutsElement = document.getElementById('shortcuts');
}
displayHistory(historyEntries: HistoryEntry[]): void {
if (!this.historyListElement) return;
if (!historyEntries || historyEntries.length === 0) {
this.historyListElement.innerHTML = `
No history yet
Use CLI commands to browse websites
`;
return;
}
this.historyListElement.innerHTML = '';
historyEntries.forEach(item => {
const historyItem = this.createHistoryItem(item);
this.historyListElement!.appendChild(historyItem);
});
}
displayShortcuts(shortcutsData: Record): void {
if (!this.shortcutsElement) return;
if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
this.shortcutsElement.innerHTML = `
No shortcuts configured
Add shortcuts to your config file
`;
return;
}
this.shortcutsElement.innerHTML = '';
Object.entries(shortcutsData).forEach(([key, shortcut]) => {
const shortcutEl = this.createShortcutElement(key, shortcut);
this.shortcutsElement!.appendChild(shortcutEl);
});
}
showLoading(container: 'history' | 'shortcuts'): void {
const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
if (element) {
element.innerHTML = 'Loading...
';
}
}
showError(container: 'history' | 'shortcuts', message: string): void {
const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
if (element) {
element.innerHTML = `
`;
}
}
private createHistoryItem(item: HistoryEntry): HTMLElement {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
const parsed = this.safeParseURL(item.url);
const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
const line = document.createElement('div');
line.className = 'history-line';
// Favicon chip (wrapper ensures perfect circle sizing)
const iconWrap = document.createElement('div');
iconWrap.className = 'history-favicon-chip';
const icon = document.createElement('img');
icon.className = 'history-favicon-img';
icon.width = 16; icon.height = 16;
icon.loading = 'lazy';
icon.referrerPolicy = 'no-referrer';
icon.src = this.buildFaviconURL(item.url);
icon.addEventListener('error', () => { iconWrap.style.display = 'none'; });
iconWrap.appendChild(icon);
const titleEl = document.createElement('span');
titleEl.className = 'history-title';
titleEl.textContent = title;
const sep1 = document.createElement('span');
sep1.className = 'history-sep';
sep1.textContent = ' - ';
const domainEl = document.createElement('span');
domainEl.className = 'history-domain';
domainEl.textContent = parsed.host || '';
const sep2 = document.createElement('span');
sep2.className = 'history-sep';
sep2.textContent = ' - ';
const urlEl = document.createElement('span');
urlEl.className = 'history-url';
urlEl.textContent = item.url;
line.appendChild(iconWrap);
line.appendChild(titleEl);
line.appendChild(sep1);
line.appendChild(domainEl);
line.appendChild(sep2);
line.appendChild(urlEl);
historyItem.appendChild(line);
historyItem.addEventListener('click', () => {
this.navigateToUrl(item.url);
});
return historyItem;
}
private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement {
const shortcutEl = document.createElement('div');
shortcutEl.className = 'shortcut';
shortcutEl.innerHTML = `
${this.escapeHtml(key)}:
${this.escapeHtml(shortcut.description)}
`;
shortcutEl.addEventListener('click', () => {
// Navigate to the base URL of the shortcut (remove %s template parameter)
const baseUrl = this.extractBaseUrl(shortcut.url);
this.navigateToUrl(baseUrl);
});
return shortcutEl;
}
private async navigateToUrl(url: string): Promise {
console.log('Navigating to:', url);
window.location.href = url;
}
private extractBaseUrl(templateUrl: string): string {
try {
// Support both %s and {query} placeholders
const candidate = templateUrl.replace(/%s|\{query\}/g, "");
const url = new URL(candidate);
// Remove empty query params left after placeholder removal
const params = new URLSearchParams(url.search);
for (const key of Array.from(params.keys())) {
const v = params.get(key);
if (v === null || v === "") {
params.delete(key);
}
}
url.search = params.toString();
return url.toString();
} catch (error) {
// Fallbacks: try to return origin or stripped placeholder
console.warn('Failed to parse shortcut URL:', templateUrl);
try {
const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl;
const u = new URL(pre);
return `${u.protocol}//${u.host}`;
} catch {
return templateUrl.replace(/%s|\{query\}/g, "");
}
}
}
private async copyToClipboard(text: string): Promise {
try {
await navigator.clipboard.writeText(text);
console.log('Copied to clipboard:', text);
} catch (err) {
console.error('Failed to copy:', err);
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private safeParseURL(raw: string): { host: string } {
try {
const u = new URL(raw);
return { host: u.host };
} catch {
return { host: '' };
}
}
private buildFaviconURL(raw: string): string {
try {
const u = new URL(raw);
const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
return `${scheme}//${u.host}/favicon.ico`;
} catch {
return '';
}
}
}