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