dom.ts RawHistoryFolder
1type HistoryEntry = { id: number; url: string; title: string };
2type SearchShortcut = { description: string; url: string };
3
4export class DOMRenderer {
5 private historyListElement: HTMLElement | null = null;
6 private shortcutsElement: HTMLElement | null = null;
7
8 constructor() {
9 this.historyListElement = document.getElementById('historyList');
10 this.shortcutsElement = document.getElementById('shortcuts');
11 }
12
13 displayHistory(historyEntries: HistoryEntry[]): void {
14 if (!this.historyListElement) return;
15
16 if (!historyEntries || historyEntries.length === 0) {
17 this.historyListElement.innerHTML = `
18 <div class="empty-state">
19 <h3>No history yet</h3>
20 <p>Use CLI commands to browse websites</p>
21 </div>
22 `;
23 return;
24 }
25
26 this.historyListElement.innerHTML = '';
27 historyEntries.forEach(item => {
28 const historyItem = this.createHistoryItem(item);
29 this.historyListElement!.appendChild(historyItem);
30 });
31 }
32
33 displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void {
34 if (!this.shortcutsElement) return;
35
36 if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
37 this.shortcutsElement.innerHTML = `
38 <div class="empty-state">
39 <h3>No shortcuts configured</h3>
40 <p>Add shortcuts to your config file</p>
41 </div>
42 `;
43 return;
44 }
45
46 this.shortcutsElement.innerHTML = '';
47 Object.entries(shortcutsData).forEach(([key, shortcut]) => {
48 const shortcutEl = this.createShortcutElement(key, shortcut);
49 this.shortcutsElement!.appendChild(shortcutEl);
50 });
51 }
52
53 showLoading(container: 'history' | 'shortcuts'): void {
54 const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
55 if (element) {
56 element.innerHTML = '<div class="loading">Loading...</div>';
57 }
58 }
59
60 showError(container: 'history' | 'shortcuts', message: string): void {
61 const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
62 if (element) {
63 element.innerHTML = `
64 <div class="empty-state">
65 <h3>Error</h3>
66 <p>${message}</p>
67 </div>
68 `;
69 }
70 }
71
72 private createHistoryItem(item: HistoryEntry): HTMLElement {
73 const historyItem = document.createElement('div');
74 historyItem.className = 'history-item';
75 const parsed = this.safeParseURL(item.url);
76 const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
77
78 const line = document.createElement('div');
79 line.className = 'history-line';
80
81 // Favicon chip (wrapper ensures perfect circle sizing)
82 const iconWrap = document.createElement('div');
83 iconWrap.className = 'history-favicon-chip';
84 const icon = document.createElement('img');
85 icon.className = 'history-favicon-img';
86 icon.width = 16; icon.height = 16;
87 icon.loading = 'lazy';
88 icon.referrerPolicy = 'no-referrer';
89 icon.src = this.buildFaviconURL(item.url);
90 icon.addEventListener('error', () => { iconWrap.style.display = 'none'; });
91 iconWrap.appendChild(icon);
92
93 const titleEl = document.createElement('span');
94 titleEl.className = 'history-title';
95 titleEl.textContent = title;
96
97 const sep1 = document.createElement('span');
98 sep1.className = 'history-sep';
99 sep1.textContent = ' - ';
100
101 const domainEl = document.createElement('span');
102 domainEl.className = 'history-domain';
103 domainEl.textContent = parsed.host || '';
104
105 const sep2 = document.createElement('span');
106 sep2.className = 'history-sep';
107 sep2.textContent = ' - ';
108
109 const urlEl = document.createElement('span');
110 urlEl.className = 'history-url';
111 urlEl.textContent = item.url;
112
113 line.appendChild(iconWrap);
114 line.appendChild(titleEl);
115 line.appendChild(sep1);
116 line.appendChild(domainEl);
117 line.appendChild(sep2);
118 line.appendChild(urlEl);
119
120 historyItem.appendChild(line);
121
122 historyItem.addEventListener('click', () => {
123 this.navigateToUrl(item.url);
124 });
125
126 return historyItem;
127 }
128
129 private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement {
130 const shortcutEl = document.createElement('div');
131 shortcutEl.className = 'shortcut';
132 shortcutEl.innerHTML = `
133 <div class="shortcut-key">${this.escapeHtml(key)}:</div>
134 <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div>
135 `;
136
137 shortcutEl.addEventListener('click', () => {
138 // Navigate to the base URL of the shortcut (remove %s template parameter)
139 const baseUrl = this.extractBaseUrl(shortcut.url);
140 this.navigateToUrl(baseUrl);
141 });
142
143 return shortcutEl;
144 }
145
146 private async navigateToUrl(url: string): Promise<void> {
147 console.log('Navigating to:', url);
148 window.location.href = url;
149 }
150
151 private extractBaseUrl(templateUrl: string): string {
152 try {
153 // Support both %s and {query} placeholders
154 const candidate = templateUrl.replace(/%s|\{query\}/g, "");
155 const url = new URL(candidate);
156 // Remove empty query params left after placeholder removal
157 const params = new URLSearchParams(url.search);
158 for (const key of Array.from(params.keys())) {
159 const v = params.get(key);
160 if (v === null || v === "") {
161 params.delete(key);
162 }
163 }
164 url.search = params.toString();
165 return url.toString();
166 } catch (error) {
167 // Fallbacks: try to return origin or stripped placeholder
168 console.warn('Failed to parse shortcut URL:', templateUrl);
169 try {
170 const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl;
171 const u = new URL(pre);
172 return `${u.protocol}//${u.host}`;
173 } catch {
174 return templateUrl.replace(/%s|\{query\}/g, "");
175 }
176 }
177 }
178
179 private async copyToClipboard(text: string): Promise<void> {
180 try {
181 await navigator.clipboard.writeText(text);
182 console.log('Copied to clipboard:', text);
183 } catch (err) {
184 console.error('Failed to copy:', err);
185 }
186 }
187
188 private escapeHtml(text: string): string {
189 const div = document.createElement('div');
190 div.textContent = text;
191 return div.innerHTML;
192 }
193
194 private safeParseURL(raw: string): { host: string } {
195 try {
196 const u = new URL(raw);
197 return { host: u.host };
198 } catch {
199 return { host: '' };
200 }
201 }
202
203 private buildFaviconURL(raw: string): string {
204 try {
205 const u = new URL(raw);
206 const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
207 return `${scheme}//${u.host}/favicon.ico`;
208 } catch {
209 return '';
210 }
211 }
212}