Skip to content

Commit a116949

Browse files
committed
feat: improve request URL input UX
1 parent 641a52a commit a116949

1 file changed

Lines changed: 135 additions & 13 deletions

File tree

templates/main.tmpl

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
'en': {
1414
'title': 'Tiny RequestBin',
1515
'subtitle_prefix': 'Send requests to',
16+
'example_path_placeholder': 'your-path',
17+
'copy_example_url': 'Copy example URL',
18+
'request_path_input': 'Request path',
1619
'auto_refresh': 'Auto Refresh',
1720
'auto_refresh_active': 'Live',
1821
'auto_refresh_inactive': 'Auto Off',
@@ -21,6 +24,7 @@
2124
'refresh': 'Refresh',
2225
'clear_requests': 'Clear',
2326
'search_path': 'Search path',
27+
'search_toggle': 'Toggle path search',
2428
'search_path_placeholder': 'Filter by path, e.g. /api',
2529
'confirm': 'Confirm',
2630
'clear_confirm': 'Clear all captured requests?',
@@ -47,6 +51,9 @@
4751
'zh-CN': {
4852
'title': 'Tiny RequestBin',
4953
'subtitle_prefix': '发送请求到',
54+
'example_path_placeholder': 'your-path',
55+
'copy_example_url': '复制示例地址',
56+
'request_path_input': '请求路径',
5057
'auto_refresh': '自动刷新',
5158
'auto_refresh_active': '自动刷新中',
5259
'auto_refresh_inactive': '自动刷新关闭',
@@ -55,6 +62,7 @@
5562
'refresh': '刷新',
5663
'clear_requests': '清空',
5764
'search_path': '搜索路径',
65+
'search_toggle': '切换路径搜索',
5866
'search_path_placeholder': '按路径过滤,例如 /api',
5967
'confirm': '确定',
6068
'clear_confirm': '确定清空所有已捕获的请求?',
@@ -123,25 +131,78 @@
123131
// 特殊处理一些动态元素
124132
document.title = translations[currentLang]['title'];
125133
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+
}
126141
}
127142

128143
function renderSubtitle() {
129144
const subtitle = document.getElementById('subtitle');
130145
if (!subtitle) return;
131146

132147
const origin = window.location.origin || `${window.location.protocol}//${window.location.host}`;
133-
const exampleUrl = `${origin}/your-path`;
134148
const prefix = translations[currentLang]['subtitle_prefix'] || '';
149+
const pathPlaceholder = translations[currentLang]['example_path_placeholder'] || 'your-path';
135150

136151
const subtitleUrl = document.getElementById('subtitle-url');
137152
if (subtitleUrl) {
138-
subtitleUrl.textContent = exampleUrl;
153+
subtitleUrl.textContent = `${origin}/`;
139154
}
140155

141156
const subtitlePrefix = document.getElementById('subtitle-prefix');
142157
if (subtitlePrefix) {
143158
subtitlePrefix.textContent = `${prefix} `;
144159
}
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`;
145206
}
146207

147208
async function copyText(text) {
@@ -175,7 +236,7 @@
175236
<h1 class="text-2xl font-bold text-gray-900" data-i18n="title">Tiny RequestBin</h1>
176237
<div id="subtitle" class="mt-1 flex items-center justify-center gap-2 text-sm text-gray-600">
177238
<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>
179240
</p>
180241
<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">
181242
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
@@ -191,7 +252,14 @@
191252
<!-- Left Column: Request List -->
192253
<aside class="w-1/3 bg-white border-r flex flex-col">
193254
<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>
195263
<div class="flex items-center gap-2">
196264
<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">
197265
<button id="autoRefreshToggleButton" class="inline-flex items-center gap-2 px-2.5 py-1.5 transition-colors hover:bg-slate-100">
@@ -219,7 +287,7 @@
219287
</button>
220288
</div>
221289
</div>
222-
<div class="p-4 border-b bg-gray-50">
290+
<div id="pathSearchPanel" class="hidden p-4 border-b bg-gray-50">
223291
<label for="pathSearchInput" class="mb-2 block text-sm font-medium text-gray-700" data-i18n="search_path">搜索路径</label>
224292
<input
225293
id="pathSearchInput"
@@ -396,11 +464,15 @@
396464
const clearRequestsButton = document.getElementById('clearRequestsButton');
397465
const listRefreshButton = document.getElementById('listRefreshButton');
398466
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');
399470
const requestList = document.querySelector('aside .overflow-y-auto');
400471
let refreshInterval;
401472
let searchDebounceTimer;
402473
let currentPathFilter = new URLSearchParams(window.location.search).get('path') || '';
403474
let isAutoRefreshEnabled = true;
475+
let isPathSearchOpen = currentPathFilter !== '';
404476

405477
// --- Pagination controls ---
406478
const prevPageBtn = document.getElementById('prevPageBtn');
@@ -412,6 +484,41 @@
412484
pathSearchInput.value = currentPathFilter;
413485
}
414486

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+
415522
function startRefresh() {
416523
if (refreshInterval) clearInterval(refreshInterval);
417524
let interval = parseInt(intervalInput.value, 10);
@@ -667,6 +774,7 @@
667774
pathSearchInput.addEventListener('input', () => {
668775
currentPathFilter = pathSearchInput.value.trim();
669776
currentPage = 1;
777+
updatePathSearchToggleButton();
670778

671779
if (searchDebounceTimer) {
672780
clearTimeout(searchDebounceTimer);
@@ -679,6 +787,19 @@
679787
});
680788
}
681789

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+
682803
if (listRefreshButton) {
683804
listRefreshButton.addEventListener('click', () => {
684805
if (isUpdating) return;
@@ -719,6 +840,8 @@
719840
if (!Number.isNaN(initialPage) && initialPage > 0) {
720841
currentPage = initialPage;
721842
}
843+
setPathSearchOpen(isPathSearchOpen);
844+
updateSubtitlePathInputWidth();
722845
if (isAutoRefreshEnabled) {
723846
startRefresh();
724847
}
@@ -810,11 +933,8 @@
810933
const copySubtitleBtn = document.getElementById('copy-subtitle-btn');
811934
if (copySubtitleBtn) {
812935
copySubtitleBtn.addEventListener('click', async () => {
813-
const subtitleUrl = document.getElementById('subtitle-url');
814-
if (!subtitleUrl) return;
815-
816936
try {
817-
await copyText(subtitleUrl.textContent || subtitleUrl.innerText || '');
937+
await copyText(getSubtitleCopyUrl());
818938

819939
const originalContent = copySubtitleBtn.innerHTML;
820940
copySubtitleBtn.innerHTML = `
@@ -831,8 +951,9 @@
831951
copySubtitleBtn.innerHTML = originalContent;
832952
copySubtitleBtn.classList.remove('text-green-600');
833953
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);
836957
}, 2000);
837958
} catch (err) {
838959
console.error('Failed to copy subtitle URL: ', err);
@@ -852,8 +973,9 @@
852973
copySubtitleBtn.innerHTML = originalContent;
853974
copySubtitleBtn.classList.remove('text-red-600');
854975
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);
857979
}, 2000);
858980
}
859981
});

0 commit comments

Comments
 (0)