|
13 | 13 | 'en': { |
14 | 14 | 'title': 'Tiny RequestBin', |
15 | 15 | 'subtitle_prefix': 'Send requests to', |
| 16 | + 'example_path_placeholder': 'your-path', |
| 17 | + 'copy_example_url': 'Copy example URL', |
| 18 | + 'request_path_input': 'Request path', |
16 | 19 | 'auto_refresh': 'Auto Refresh', |
17 | 20 | 'auto_refresh_active': 'Live', |
18 | 21 | 'auto_refresh_inactive': 'Auto Off', |
|
21 | 24 | 'refresh': 'Refresh', |
22 | 25 | 'clear_requests': 'Clear', |
23 | 26 | 'search_path': 'Search path', |
| 27 | + 'search_toggle': 'Toggle path search', |
24 | 28 | 'search_path_placeholder': 'Filter by path, e.g. /api', |
25 | 29 | 'confirm': 'Confirm', |
26 | 30 | 'clear_confirm': 'Clear all captured requests?', |
|
47 | 51 | 'zh-CN': { |
48 | 52 | 'title': 'Tiny RequestBin', |
49 | 53 | 'subtitle_prefix': '发送请求到', |
| 54 | + 'example_path_placeholder': 'your-path', |
| 55 | + 'copy_example_url': '复制示例地址', |
| 56 | + 'request_path_input': '请求路径', |
50 | 57 | 'auto_refresh': '自动刷新', |
51 | 58 | 'auto_refresh_active': '自动刷新中', |
52 | 59 | 'auto_refresh_inactive': '自动刷新关闭', |
|
55 | 62 | 'refresh': '刷新', |
56 | 63 | 'clear_requests': '清空', |
57 | 64 | 'search_path': '搜索路径', |
| 65 | + 'search_toggle': '切换路径搜索', |
58 | 66 | 'search_path_placeholder': '按路径过滤,例如 /api', |
59 | 67 | 'confirm': '确定', |
60 | 68 | 'clear_confirm': '确定清空所有已捕获的请求?', |
|
123 | 131 | // 特殊处理一些动态元素 |
124 | 132 | document.title = translations[currentLang]['title']; |
125 | 133 | renderSubtitle(); |
| 134 | + |
| 135 | + const togglePathSearchButton = document.getElementById('togglePathSearchButton'); |
| 136 | + if (togglePathSearchButton) { |
| 137 | + const searchToggleLabel = translations[currentLang]['search_toggle'] || 'Toggle path search'; |
| 138 | + togglePathSearchButton.title = searchToggleLabel; |
| 139 | + togglePathSearchButton.setAttribute('aria-label', searchToggleLabel); |
| 140 | + } |
126 | 141 | } |
127 | 142 |
|
128 | 143 | function renderSubtitle() { |
129 | 144 | const subtitle = document.getElementById('subtitle'); |
130 | 145 | if (!subtitle) return; |
131 | 146 |
|
132 | 147 | const origin = window.location.origin || `${window.location.protocol}//${window.location.host}`; |
133 | | - const exampleUrl = `${origin}/your-path`; |
134 | 148 | const prefix = translations[currentLang]['subtitle_prefix'] || ''; |
| 149 | + const pathPlaceholder = translations[currentLang]['example_path_placeholder'] || 'your-path'; |
135 | 150 |
|
136 | 151 | const subtitleUrl = document.getElementById('subtitle-url'); |
137 | 152 | if (subtitleUrl) { |
138 | | - subtitleUrl.textContent = exampleUrl; |
| 153 | + subtitleUrl.textContent = `${origin}/`; |
139 | 154 | } |
140 | 155 |
|
141 | 156 | const subtitlePrefix = document.getElementById('subtitle-prefix'); |
142 | 157 | if (subtitlePrefix) { |
143 | 158 | subtitlePrefix.textContent = `${prefix} `; |
144 | 159 | } |
| 160 | + |
| 161 | + const subtitlePathInput = document.getElementById('subtitle-path-input'); |
| 162 | + if (subtitlePathInput) { |
| 163 | + subtitlePathInput.placeholder = pathPlaceholder; |
| 164 | + const pathInputLabel = translations[currentLang]['request_path_input'] || 'Request path'; |
| 165 | + subtitlePathInput.setAttribute('aria-label', pathInputLabel); |
| 166 | + updateSubtitlePathInputWidth(); |
| 167 | + } |
| 168 | + |
| 169 | + const copySubtitleBtn = document.getElementById('copy-subtitle-btn'); |
| 170 | + if (copySubtitleBtn) { |
| 171 | + const copyLabel = translations[currentLang]['copy_example_url'] || 'Copy example URL'; |
| 172 | + copySubtitleBtn.title = copyLabel; |
| 173 | + copySubtitleBtn.setAttribute('aria-label', copyLabel); |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + function getSubtitlePathValue() { |
| 178 | + const subtitlePathInput = document.getElementById('subtitle-path-input'); |
| 179 | + const fallbackPath = translations[currentLang]['example_path_placeholder'] || 'your-path'; |
| 180 | + const rawValue = subtitlePathInput ? subtitlePathInput.value.trim() : ''; |
| 181 | + |
| 182 | + if (!rawValue) { |
| 183 | + return fallbackPath; |
| 184 | + } |
| 185 | + |
| 186 | + return rawValue.replace(/^\/+/, ''); |
| 187 | + } |
| 188 | + |
| 189 | + function getSubtitleCopyUrl() { |
| 190 | + const subtitleUrl = document.getElementById('subtitle-url'); |
| 191 | + if (!subtitleUrl) return ''; |
| 192 | + |
| 193 | + const baseUrl = subtitleUrl.textContent || subtitleUrl.innerText || ''; |
| 194 | + return `${baseUrl}${getSubtitlePathValue()}`; |
| 195 | + } |
| 196 | + |
| 197 | + function updateSubtitlePathInputWidth() { |
| 198 | + const subtitlePathInput = document.getElementById('subtitle-path-input'); |
| 199 | + const subtitlePathMeasure = document.getElementById('subtitle-path-measure'); |
| 200 | + if (!subtitlePathInput || !subtitlePathMeasure) return; |
| 201 | + |
| 202 | + const fallbackPath = translations[currentLang]['example_path_placeholder'] || 'your-path'; |
| 203 | + subtitlePathMeasure.textContent = subtitlePathInput.value || subtitlePathInput.placeholder || fallbackPath; |
| 204 | + const measuredWidth = Math.ceil(subtitlePathMeasure.getBoundingClientRect().width) + 6; |
| 205 | + subtitlePathInput.style.width = `${Math.max(measuredWidth, 72)}px`; |
145 | 206 | } |
146 | 207 |
|
147 | 208 | async function copyText(text) { |
|
175 | 236 | <h1 class="text-2xl font-bold text-gray-900" data-i18n="title">Tiny RequestBin</h1> |
176 | 237 | <div id="subtitle" class="mt-1 flex items-center justify-center gap-2 text-sm text-gray-600"> |
177 | 238 | <p class="min-w-0"> |
178 | | - <span id="subtitle-prefix">Send requests to </span><span id="subtitle-url" class="font-mono">{{.BaseURL}}/your-path</span> |
| 239 | + <span id="subtitle-prefix">Send requests to </span><span id="subtitle-url" class="font-mono">{{.BaseURL}}/</span><input id="subtitle-path-input" type="text" placeholder="your-path" class="border-0 border-b border-dashed border-gray-300 bg-transparent px-0.5 font-mono text-inherit focus:border-indigo-400 focus:outline-none focus:ring-0" aria-label="Request path"><span id="subtitle-path-measure" class="invisible absolute left-[-9999px] top-[-9999px] whitespace-pre px-0.5 font-mono text-sm"></span> |
179 | 240 | </p> |
180 | 241 | <button id="copy-subtitle-btn" class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-gray-500 transition-colors hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-200" title="Copy example URL" aria-label="Copy example URL"> |
181 | 242 | <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> |
|
191 | 252 | <!-- Left Column: Request List --> |
192 | 253 | <aside class="w-1/3 bg-white border-r flex flex-col"> |
193 | 254 | <div class="p-4 border-b flex items-center justify-between gap-3"> |
194 | | - <h2 class="text-lg font-semibold" data-i18n="captured_requests">已捕获的请求</h2> |
| 255 | + <div class="flex items-center gap-2"> |
| 256 | + <h2 class="text-lg font-semibold" data-i18n="captured_requests">已捕获的请求</h2> |
| 257 | + <button id="togglePathSearchButton" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-200" title="Toggle path search" aria-label="Toggle path search" aria-expanded="false" aria-controls="pathSearchPanel"> |
| 258 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"> |
| 259 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.35-4.35m1.85-5.15a7 7 0 11-14 0 7 7 0 0114 0z"></path> |
| 260 | + </svg> |
| 261 | + </button> |
| 262 | + </div> |
195 | 263 | <div class="flex items-center gap-2"> |
196 | 264 | <div id="refreshControlGroup" class="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 text-xs font-semibold text-slate-700 overflow-hidden"> |
197 | 265 | <button id="autoRefreshToggleButton" class="inline-flex items-center gap-2 px-2.5 py-1.5 transition-colors hover:bg-slate-100"> |
|
219 | 287 | </button> |
220 | 288 | </div> |
221 | 289 | </div> |
222 | | - <div class="p-4 border-b bg-gray-50"> |
| 290 | + <div id="pathSearchPanel" class="hidden p-4 border-b bg-gray-50"> |
223 | 291 | <label for="pathSearchInput" class="mb-2 block text-sm font-medium text-gray-700" data-i18n="search_path">搜索路径</label> |
224 | 292 | <input |
225 | 293 | id="pathSearchInput" |
|
396 | 464 | const clearRequestsButton = document.getElementById('clearRequestsButton'); |
397 | 465 | const listRefreshButton = document.getElementById('listRefreshButton'); |
398 | 466 | const pathSearchInput = document.getElementById('pathSearchInput'); |
| 467 | + const pathSearchPanel = document.getElementById('pathSearchPanel'); |
| 468 | + const togglePathSearchButton = document.getElementById('togglePathSearchButton'); |
| 469 | + const subtitlePathInput = document.getElementById('subtitle-path-input'); |
399 | 470 | const requestList = document.querySelector('aside .overflow-y-auto'); |
400 | 471 | let refreshInterval; |
401 | 472 | let searchDebounceTimer; |
402 | 473 | let currentPathFilter = new URLSearchParams(window.location.search).get('path') || ''; |
403 | 474 | let isAutoRefreshEnabled = true; |
| 475 | + let isPathSearchOpen = currentPathFilter !== ''; |
404 | 476 |
|
405 | 477 | // --- Pagination controls --- |
406 | 478 | const prevPageBtn = document.getElementById('prevPageBtn'); |
|
412 | 484 | pathSearchInput.value = currentPathFilter; |
413 | 485 | } |
414 | 486 |
|
| 487 | + function updatePathSearchToggleButton() { |
| 488 | + if (!togglePathSearchButton) { |
| 489 | + return; |
| 490 | + } |
| 491 | + |
| 492 | + const activeClasses = ['bg-indigo-50', 'text-indigo-700']; |
| 493 | + const inactiveClasses = ['text-slate-500', 'hover:bg-slate-100', 'hover:text-slate-700']; |
| 494 | + |
| 495 | + togglePathSearchButton.setAttribute('aria-expanded', String(isPathSearchOpen)); |
| 496 | + if (currentPathFilter) { |
| 497 | + togglePathSearchButton.classList.remove(...inactiveClasses); |
| 498 | + togglePathSearchButton.classList.add(...activeClasses); |
| 499 | + } else { |
| 500 | + togglePathSearchButton.classList.remove(...activeClasses); |
| 501 | + togglePathSearchButton.classList.add(...inactiveClasses); |
| 502 | + } |
| 503 | + } |
| 504 | + |
| 505 | + function setPathSearchOpen(nextOpen, { focusInput = false } = {}) { |
| 506 | + if (!pathSearchPanel) { |
| 507 | + return; |
| 508 | + } |
| 509 | + |
| 510 | + isPathSearchOpen = nextOpen; |
| 511 | + pathSearchPanel.classList.toggle('hidden', !isPathSearchOpen); |
| 512 | + updatePathSearchToggleButton(); |
| 513 | + |
| 514 | + if (focusInput && isPathSearchOpen && pathSearchInput) { |
| 515 | + requestAnimationFrame(() => { |
| 516 | + pathSearchInput.focus(); |
| 517 | + pathSearchInput.select(); |
| 518 | + }); |
| 519 | + } |
| 520 | + } |
| 521 | + |
415 | 522 | function startRefresh() { |
416 | 523 | if (refreshInterval) clearInterval(refreshInterval); |
417 | 524 | let interval = parseInt(intervalInput.value, 10); |
|
667 | 774 | pathSearchInput.addEventListener('input', () => { |
668 | 775 | currentPathFilter = pathSearchInput.value.trim(); |
669 | 776 | currentPage = 1; |
| 777 | + updatePathSearchToggleButton(); |
670 | 778 |
|
671 | 779 | if (searchDebounceTimer) { |
672 | 780 | clearTimeout(searchDebounceTimer); |
|
679 | 787 | }); |
680 | 788 | } |
681 | 789 |
|
| 790 | + if (subtitlePathInput) { |
| 791 | + subtitlePathInput.addEventListener('input', updateSubtitlePathInputWidth); |
| 792 | + subtitlePathInput.addEventListener('focus', updateSubtitlePathInputWidth); |
| 793 | + subtitlePathInput.addEventListener('blur', updateSubtitlePathInputWidth); |
| 794 | + } |
| 795 | + |
| 796 | + if (togglePathSearchButton) { |
| 797 | + togglePathSearchButton.addEventListener('click', () => { |
| 798 | + const nextOpen = !isPathSearchOpen; |
| 799 | + setPathSearchOpen(nextOpen, { focusInput: nextOpen }); |
| 800 | + }); |
| 801 | + } |
| 802 | + |
682 | 803 | if (listRefreshButton) { |
683 | 804 | listRefreshButton.addEventListener('click', () => { |
684 | 805 | if (isUpdating) return; |
|
719 | 840 | if (!Number.isNaN(initialPage) && initialPage > 0) { |
720 | 841 | currentPage = initialPage; |
721 | 842 | } |
| 843 | + setPathSearchOpen(isPathSearchOpen); |
| 844 | + updateSubtitlePathInputWidth(); |
722 | 845 | if (isAutoRefreshEnabled) { |
723 | 846 | startRefresh(); |
724 | 847 | } |
|
810 | 933 | const copySubtitleBtn = document.getElementById('copy-subtitle-btn'); |
811 | 934 | if (copySubtitleBtn) { |
812 | 935 | copySubtitleBtn.addEventListener('click', async () => { |
813 | | - const subtitleUrl = document.getElementById('subtitle-url'); |
814 | | - if (!subtitleUrl) return; |
815 | | - |
816 | 936 | try { |
817 | | - await copyText(subtitleUrl.textContent || subtitleUrl.innerText || ''); |
| 937 | + await copyText(getSubtitleCopyUrl()); |
818 | 938 |
|
819 | 939 | const originalContent = copySubtitleBtn.innerHTML; |
820 | 940 | copySubtitleBtn.innerHTML = ` |
|
831 | 951 | copySubtitleBtn.innerHTML = originalContent; |
832 | 952 | copySubtitleBtn.classList.remove('text-green-600'); |
833 | 953 | copySubtitleBtn.classList.add('text-gray-500', 'hover:text-gray-800'); |
834 | | - copySubtitleBtn.title = 'Copy example URL'; |
835 | | - copySubtitleBtn.setAttribute('aria-label', 'Copy example URL'); |
| 954 | + const copyLabel = translations[currentLang]['copy_example_url'] || 'Copy example URL'; |
| 955 | + copySubtitleBtn.title = copyLabel; |
| 956 | + copySubtitleBtn.setAttribute('aria-label', copyLabel); |
836 | 957 | }, 2000); |
837 | 958 | } catch (err) { |
838 | 959 | console.error('Failed to copy subtitle URL: ', err); |
|
852 | 973 | copySubtitleBtn.innerHTML = originalContent; |
853 | 974 | copySubtitleBtn.classList.remove('text-red-600'); |
854 | 975 | copySubtitleBtn.classList.add('text-gray-500', 'hover:text-gray-800'); |
855 | | - copySubtitleBtn.title = 'Copy example URL'; |
856 | | - copySubtitleBtn.setAttribute('aria-label', 'Copy example URL'); |
| 976 | + const copyLabel = translations[currentLang]['copy_example_url'] || 'Copy example URL'; |
| 977 | + copySubtitleBtn.title = copyLabel; |
| 978 | + copySubtitleBtn.setAttribute('aria-label', copyLabel); |
857 | 979 | }, 2000); |
858 | 980 | } |
859 | 981 | }); |
|
0 commit comments