feat(前端界面): 完善键盘操作,优化阅读与搜索体验
build.yaml / build (push) Successful in 36s

This commit is contained in:
xiaoqidun committed 2026-09-14 09:09:12 +08:00
1 parent dd6818178c
commit a29fbbd31b
3 files changed
+100 -29

No files matched your search

+9 -9
View File
@@ -16,24 +16,24 @@
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple> <input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple> <input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple>
<div class="tool-group"> <div class="tool-group">
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开"><span class="open-icon" aria-hidden="true"></span></button> <button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开" aria-keyshortcuts="Control+O Meta+O"><span class="open-icon" aria-hidden="true"></span></button>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button> <button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button> <button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
</div> </div>
<div class="toolbar-space"></div> <div class="toolbar-space"></div>
<div class="tool-group"> <div class="tool-group">
<button id="prevButton" class="icon-button arrow-prev" type="button" title="上一页" aria-label="上一页" disabled></button> <button id="prevButton" class="icon-button arrow-prev" type="button" title="上一页" aria-label="上一页" aria-keyshortcuts="ArrowLeft" disabled></button>
<label class="page-control"> <label class="page-control">
<input id="pageInput" type="number" min="1" value="0" disabled> <input id="pageInput" type="number" min="1" value="0" disabled>
<span>/</span> <span>/</span>
<span id="pageTotal">0</span> <span id="pageTotal">0</span>
</label> </label>
<button id="nextButton" class="icon-button arrow-next" type="button" title="下一页" aria-label="下一页" disabled></button> <button id="nextButton" class="icon-button arrow-next" type="button" title="下一页" aria-label="下一页" aria-keyshortcuts="ArrowRight" disabled></button>
</div> </div>
<div class="tool-group"> <div class="tool-group">
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button> <button id="zoomOutButton" class="icon-button" type="button" title="缩小" aria-label="缩小" aria-keyshortcuts="-" disabled>-</button>
<span id="zoomLabel" class="zoom-label">100%</span> <span id="zoomLabel" class="zoom-label">100%</span>
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button> <button id="zoomInButton" class="icon-button" type="button" title="放大" aria-label="放大" aria-keyshortcuts="= Shift+= Plus" disabled>+</button>
<button id="fitButton" class="button" type="button" data-short="宽" aria-label="适宽" disabled>适宽</button> <button id="fitButton" class="button" type="button" data-short="宽" aria-label="适宽" disabled>适宽</button>
<button id="fitHeightButton" class="button" type="button" data-short="高" aria-label="适高" disabled>适高</button> <button id="fitHeightButton" class="button" type="button" data-short="高" aria-label="适高" disabled>适高</button>
<button id="rotateButton" class="button" type="button" data-short="转" title="顺时针旋转90°" aria-label="旋转" disabled>旋转</button> <button id="rotateButton" class="button" type="button" data-short="转" title="顺时针旋转90°" aria-label="旋转" disabled>旋转</button>
@@ -60,7 +60,7 @@
<div id="navigationTabs" class="navigation-tabs" role="tablist" aria-label="文档导航" hidden> <div id="navigationTabs" class="navigation-tabs" role="tablist" aria-label="文档导航" hidden>
<button id="pagesTab" type="button" role="tab" aria-selected="true" aria-controls="pageList">页面</button> <button id="pagesTab" type="button" role="tab" aria-selected="true" aria-controls="pageList">页面</button>
<button id="outlinesTab" type="button" role="tab" aria-selected="false" aria-controls="outlineList" tabindex="-1">目录</button> <button id="outlinesTab" type="button" role="tab" aria-selected="false" aria-controls="outlineList" tabindex="-1">目录</button>
<button id="searchTab" type="button" role="tab" aria-selected="false" aria-controls="searchPanel" tabindex="-1">搜索</button> <button id="searchTab" type="button" role="tab" aria-keyshortcuts="Control+F Meta+F" aria-selected="false" aria-controls="searchPanel" tabindex="-1">搜索</button>
</div> </div>
<div id="pageList" class="page-list" role="tabpanel" aria-label="页面"></div> <div id="pageList" class="page-list" role="tabpanel" aria-label="页面"></div>
<div id="outlineList" role="tabpanel" aria-label="目录" hidden></div> <div id="outlineList" role="tabpanel" aria-label="目录" hidden></div>
@@ -72,14 +72,14 @@
<div class="search-tools"> <div class="search-tools">
<span id="searchStatus" role="status" aria-live="polite">输入文字</span> <span id="searchStatus" role="status" aria-live="polite">输入文字</span>
<span id="searchCount">0 / 0</span> <span id="searchCount">0 / 0</span>
<button id="searchPrev" type="button" class="icon-button arrow-prev" title="上一处" aria-label="上一处" disabled></button> <button id="searchPrev" type="button" class="icon-button arrow-prev" title="上一处" aria-label="上一处" aria-keyshortcuts="Shift+F3" disabled></button>
<button id="searchNext" type="button" class="icon-button arrow-next" title="下一处" aria-label="下一处" disabled></button> <button id="searchNext" type="button" class="icon-button arrow-next" title="下一处" aria-label="下一处" aria-keyshortcuts="F3" disabled></button>
</div> </div>
<div id="searchResults" class="search-results"></div> <div id="searchResults" class="search-results"></div>
</div> </div>
</aside> </aside>
<section class="viewer-panel" tabindex="0"> <section class="viewer-panel" tabindex="0" aria-label="阅读区">
<div id="emptyState" class="empty-state">选择 OFD 文件</div> <div id="emptyState" class="empty-state">选择 OFD 文件</div>
<div id="progressPanel" class="progress-panel" role="status" aria-live="polite" hidden> <div id="progressPanel" class="progress-panel" role="status" aria-live="polite" hidden>
<div class="progress-spinner"></div> <div class="progress-spinner"></div>
+5
View File
@@ -659,6 +659,11 @@ button.outline-link:hover {
overflow-y: hidden; overflow-y: hidden;
} }
.viewer-panel:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -2px;
}
.viewer-panel.pan-mode, .viewer-panel.pan-mode,
.pan-mode .text-run > span { .pan-mode .text-run > span {
cursor: grab; cursor: grab;
+86 -20
View File
@@ -154,11 +154,11 @@ el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta")); el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab)); el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab));
el.outlinesTab.addEventListener("click", () => showNavigation(el.outlinesTab)); el.outlinesTab.addEventListener("click", () => showNavigation(el.outlinesTab));
el.searchTab.addEventListener("click", () => { el.searchTab.addEventListener("click", focusSearch);
showNavigation(el.searchTab);
el.searchInput.focus({ preventScroll: true });
});
el.navigationTabs.addEventListener("keydown", (event) => { el.navigationTabs.addEventListener("keydown", (event) => {
if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) { if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
event.preventDefault(); event.preventDefault();
const tabs = [el.pagesTab, el.outlinesTab, el.searchTab].filter((tab) => !tab.hidden); const tabs = [el.pagesTab, el.outlinesTab, el.searchTab].filter((tab) => !tab.hidden);
@@ -176,21 +176,7 @@ el.searchForm.addEventListener("submit", (event) => {
el.searchInput.addEventListener("input", () => resetSearch(false)); el.searchInput.addEventListener("input", () => resetSearch(false));
el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1)); el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1));
el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1)); el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1));
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", handleKeyDown);
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && state.doc) {
event.preventDefault();
if (!state.showPages) {
toggleSidebar("pages");
}
showNavigation(el.searchTab);
el.searchInput.focus({ preventScroll: true });
el.searchInput.select();
} else if (event.key === "Escape" && !el.searchPanel.hidden) {
resetSearch();
showNavigation(el.pagesTab);
el.pagesTab.focus();
}
});
el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput)); el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput));
el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput)); el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
el.localFontButton.addEventListener("click", loadLocalFonts); el.localFontButton.addEventListener("click", loadLocalFonts);
@@ -267,6 +253,74 @@ el.fontDirectoryButton.disabled = !("webkitdirectory" in el.fontDirectoryInput);
updateSidebarState(); updateSidebarState();
boot(); boot();
function handleKeyDown(event) {
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy")) {
return;
}
const key = event.key;
if (event.ctrlKey || event.metaKey) {
if (!event.shiftKey && key.toLowerCase() === "o") {
event.preventDefault();
openOFDFile();
} else if (!event.shiftKey && key.toLowerCase() === "f" && state.doc) {
event.preventDefault();
focusSearch();
}
return;
}
if (!state.doc) {
return;
}
const target = event.target;
if (key === "F3") {
event.preventDefault();
if (state.searchMatches.length) {
selectSearchMatch(event.shiftKey ? Math.max(0, state.searchIndex) - 1 : state.searchIndex + 1);
} else {
focusSearch();
}
return;
}
if (key === "Escape" && !event.shiftKey && !el.searchPanel.hidden && (el.pageListPanel.contains(target) || el.viewerPanel.contains(target))) {
event.preventDefault();
resetSearch();
showNavigation(el.pagesTab);
if (COMPACT_LAYOUT.matches && state.showPages) {
state.showPages = false;
updateSidebarState();
}
el.viewerPanel.focus({ preventScroll: true });
return;
}
if (key === "Enter" && target === el.pageInput && !event.shiftKey) {
event.preventDefault();
el.viewerPanel.focus({ preventScroll: true });
return;
}
if (key === "Enter" && target === el.searchInput && event.shiftKey && state.searchMatches.length) {
event.preventDefault();
selectSearchMatch(Math.max(0, state.searchIndex) - 1);
return;
}
if (!el.viewerPanel.contains(target) || target.closest("input, textarea, select, button, a, [contenteditable]")) {
return;
}
let button;
if (key === "+" || (key === "=" && !event.shiftKey)) {
button = el.zoomInButton;
} else if (key === "-" && !event.shiftKey) {
button = el.zoomOutButton;
} else if (!event.shiftKey && (key === "ArrowLeft" || key === "ArrowRight")) {
if (document.getSelection().isCollapsed && el.viewerPanel.scrollWidth <= el.viewerPanel.clientWidth) {
button = key === "ArrowLeft" ? el.prevButton : el.nextButton;
}
}
if (button) {
event.preventDefault();
button.click();
}
}
async function openOFDFile() { async function openOFDFile() {
if (document.body.hasAttribute("aria-busy")) { if (document.body.hasAttribute("aria-busy")) {
return; return;
@@ -1320,7 +1374,10 @@ async function openDocument(options = {}) {
} }
applyFit(false); applyFit(false);
await nextFrame(); await nextFrame();
await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq }); const page = await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
if (page && options.resetScroll) {
el.viewerPanel.focus({ preventScroll: true });
}
queueNearbyPages(pageIndex, openSeq); queueNearbyPages(pageIndex, openSeq);
loadDocumentDetails(openSeq); loadDocumentDetails(openSeq);
} catch (err) { } catch (err) {
@@ -2042,6 +2099,15 @@ function prefixSVGIds(svg, prefix) {
} }
} }
function focusSearch() {
if (!state.showPages) {
toggleSidebar("pages");
}
showNavigation(el.searchTab);
el.searchInput.focus({ preventScroll: true });
el.searchInput.select();
}
function showNavigation(selected) { function showNavigation(selected) {
for (const [tab, panel] of [[el.pagesTab, el.pageList], [el.outlinesTab, el.outlineList], [el.searchTab, el.searchPanel]]) { for (const [tab, panel] of [[el.pagesTab, el.pageList], [el.outlinesTab, el.outlineList], [el.searchTab, el.searchPanel]]) {
const active = tab === selected; const active = tab === selected;