mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
90bc7b93dc
commit
c2899deff1
7 files changed
+238
-17
No files matched your search
@@ -54,8 +54,13 @@
|
||||
|
||||
<main class="workspace">
|
||||
<aside class="page-list-panel">
|
||||
<div class="panel-title">页面</div>
|
||||
<div id="pageList" class="page-list"></div>
|
||||
<div id="pageListTitle" class="panel-title">页面</div>
|
||||
<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="outlinesTab" type="button" role="tab" aria-selected="false" aria-controls="outlineList" tabindex="-1">目录</button>
|
||||
</div>
|
||||
<div id="pageList" class="page-list" role="tabpanel" aria-label="页面"></div>
|
||||
<div id="outlineList" role="tabpanel" aria-label="目录" hidden></div>
|
||||
</aside>
|
||||
|
||||
<section class="viewer-panel" tabindex="0">
|
||||
|
||||
@@ -371,6 +371,84 @@ body[data-hide-meta] .meta-panel {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.navigation-tabs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.navigation-tabs button {
|
||||
height: 28px;
|
||||
padding: 0 0 6px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.navigation-tabs button[aria-selected="true"] {
|
||||
border-bottom-color: var(--accent);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.outline-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.outline-list .outline-list {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.outline-list summary {
|
||||
margin-left: 14px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
.outline-leaf {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
height: auto;
|
||||
padding: 5px 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.outline-link > :first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.outline-page {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
button.outline-link:hover {
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.page-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -470,6 +548,8 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.empty-state[hidden],
|
||||
.navigation-tabs[hidden],
|
||||
.page-list[hidden],
|
||||
.page-frame[hidden],
|
||||
.progress-panel[hidden] {
|
||||
display: none;
|
||||
|
||||
@@ -93,7 +93,12 @@ const el = {
|
||||
viewerPanel: document.querySelector(".viewer-panel"),
|
||||
svgHost: document.querySelector("#svgHost"),
|
||||
pageListPanel: document.querySelector(".page-list-panel"),
|
||||
pageListTitle: document.querySelector("#pageListTitle"),
|
||||
navigationTabs: document.querySelector("#navigationTabs"),
|
||||
pagesTab: document.querySelector("#pagesTab"),
|
||||
outlinesTab: document.querySelector("#outlinesTab"),
|
||||
pageList: document.querySelector("#pageList"),
|
||||
outlineList: document.querySelector("#outlineList"),
|
||||
metaPanel: document.querySelector(".meta-panel"),
|
||||
appPanel: document.querySelector("#appPanel"),
|
||||
offlineStatus: document.querySelector("#offlineStatus"),
|
||||
@@ -119,6 +124,16 @@ const el = {
|
||||
el.ofdButton.addEventListener("click", openOFDFile);
|
||||
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
|
||||
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
|
||||
el.pagesTab.addEventListener("click", () => showOutlines(false));
|
||||
el.outlinesTab.addEventListener("click", () => showOutlines(true));
|
||||
el.navigationTabs.addEventListener("keydown", (event) => {
|
||||
if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
const outlines = event.key === "End" || (event.key !== "Home" && el.outlineList.hidden);
|
||||
showOutlines(outlines);
|
||||
(outlines ? el.outlinesTab : el.pagesTab).focus();
|
||||
}
|
||||
});
|
||||
el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput));
|
||||
el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
|
||||
el.localFontButton.addEventListener("click", loadLocalFonts);
|
||||
@@ -1178,6 +1193,9 @@ async function openDocument(options = {}) {
|
||||
}
|
||||
resetPageFlow();
|
||||
renderPageList();
|
||||
if (options.resetScroll || el.outlineList.childElementCount === 0) {
|
||||
renderOutlines();
|
||||
}
|
||||
renderMeta();
|
||||
renderPageFlow();
|
||||
if (options.resetScroll) {
|
||||
@@ -1748,6 +1766,68 @@ function prefixSVGIds(svg, prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
function showOutlines(show) {
|
||||
el.pageList.hidden = show;
|
||||
el.outlineList.hidden = !show;
|
||||
el.pagesTab.setAttribute("aria-selected", String(!show));
|
||||
el.outlinesTab.setAttribute("aria-selected", String(show));
|
||||
el.pagesTab.tabIndex = show ? -1 : 0;
|
||||
el.outlinesTab.tabIndex = show ? 0 : -1;
|
||||
el.pageListPanel.scrollTop = 0;
|
||||
}
|
||||
|
||||
function renderOutlines() {
|
||||
const outlines = state.doc.outlines || [];
|
||||
el.pageListTitle.hidden = outlines.length > 0;
|
||||
el.navigationTabs.hidden = outlines.length === 0;
|
||||
el.outlineList.replaceChildren();
|
||||
if (outlines.length > 0) {
|
||||
el.outlineList.append(createOutlineList(outlines));
|
||||
}
|
||||
showOutlines(false);
|
||||
}
|
||||
|
||||
function createOutlineList(outlines) {
|
||||
const list = document.createElement("ul");
|
||||
list.className = "outline-list";
|
||||
for (const outline of outlines) {
|
||||
const item = document.createElement("li");
|
||||
const hasChildren = outline.children?.length > 0;
|
||||
const row = document.createElement(hasChildren ? "summary" : "div");
|
||||
if (!hasChildren) {
|
||||
row.className = "outline-leaf";
|
||||
}
|
||||
const link = document.createElement(outline.page ? "button" : "span");
|
||||
link.className = "outline-link";
|
||||
const title = document.createElement("span");
|
||||
title.textContent = outline.title;
|
||||
link.append(title);
|
||||
if (outline.page) {
|
||||
link.type = "button";
|
||||
const page = document.createElement("span");
|
||||
page.className = "outline-page";
|
||||
page.textContent = String(outline.page);
|
||||
page.setAttribute("aria-label", `第 ${outline.page} 页`);
|
||||
link.append(page);
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
renderPage(outline.page - 1);
|
||||
});
|
||||
}
|
||||
row.append(link);
|
||||
if (hasChildren) {
|
||||
const details = document.createElement("details");
|
||||
details.open = outline.expanded;
|
||||
details.append(row, createOutlineList(outline.children));
|
||||
item.append(details);
|
||||
} else {
|
||||
item.append(row);
|
||||
}
|
||||
list.append(item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function renderPageList() {
|
||||
el.pageList.replaceChildren();
|
||||
if (!state.doc) {
|
||||
|
||||
@@ -127,9 +127,13 @@ type DocumentInfo struct {
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Signatures []SignatureInfo `json:"signatures"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||
}
|
||||
|
||||
// OutlineInfo 目录节点信息
|
||||
type OutlineInfo = ofdgo.OutlineInfo
|
||||
|
||||
// PageInfo 页面信息
|
||||
type PageInfo struct {
|
||||
Index int `json:"index"`
|
||||
@@ -257,6 +261,7 @@ func (s *Session) Summary() DocumentInfo {
|
||||
DocType: s.Reader.DocType(),
|
||||
PageCount: len(s.doc.Pages.Page),
|
||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||
Outlines: s.doc.OutlineInfos(),
|
||||
DetailsPending: true,
|
||||
}
|
||||
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||
@@ -300,6 +305,7 @@ func (s *Session) Info() DocumentInfo {
|
||||
DocType: s.Reader.DocType(),
|
||||
PageCount: len(s.doc.Pages.Page),
|
||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||
Outlines: s.doc.OutlineInfos(),
|
||||
}
|
||||
if signatures, err := s.signatureInfos(); err == nil {
|
||||
info.Signatures = signatures
|
||||
|
||||
@@ -102,6 +102,21 @@ type RegionCommand struct {
|
||||
EndPoint string
|
||||
}
|
||||
|
||||
// gotoDest 获取文档内跳转目标
|
||||
// 入参: action 跳转动作, bookmarks 书签
|
||||
// 返回: *Dest 跳转目标
|
||||
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
|
||||
if action.Dest != nil {
|
||||
return action.Dest
|
||||
}
|
||||
if action.Bookmark != nil {
|
||||
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
|
||||
return &dest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析跳转目标
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -161,6 +161,14 @@ type OutlineElem struct {
|
||||
OutlineElem []OutlineElem `xml:"OutlineElem"`
|
||||
}
|
||||
|
||||
// OutlineInfo 目录节点信息,Page从1开始,0表示没有有效的文档内目标页
|
||||
type OutlineInfo struct {
|
||||
Title string `json:"title"`
|
||||
Expanded bool `json:"expanded"`
|
||||
Page int `json:"page,omitempty"`
|
||||
Children []OutlineInfo `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// Permissions 权限声明
|
||||
type Permissions struct {
|
||||
Edit bool `xml:"Edit"`
|
||||
@@ -182,6 +190,48 @@ type ValidPeriod struct {
|
||||
EndDate string `xml:"EndDate,attr"`
|
||||
}
|
||||
|
||||
// OutlineInfos 获取目录层级和目标页码
|
||||
// 返回: []OutlineInfo 目录节点信息
|
||||
func (doc *Document) OutlineInfos() []OutlineInfo {
|
||||
if len(doc.Outlines.OutlineElem) == 0 {
|
||||
return nil
|
||||
}
|
||||
bookmarks := make(map[string]Dest, len(doc.Bookmarks.Bookmark))
|
||||
for _, bookmark := range doc.Bookmarks.Bookmark {
|
||||
bookmarks[bookmark.Name] = bookmark.Dest
|
||||
}
|
||||
pages := make(map[string]int, len(doc.Pages.Page))
|
||||
for index, page := range doc.Pages.Page {
|
||||
pages[page.ID] = index + 1
|
||||
}
|
||||
return outlineInfos(doc.Outlines.OutlineElem, bookmarks, pages)
|
||||
}
|
||||
|
||||
// outlineInfos 转换目录节点信息
|
||||
// 入参: outlines 大纲节点, bookmarks 书签, pages 页码索引
|
||||
// 返回: []OutlineInfo 目录节点信息
|
||||
func outlineInfos(outlines []OutlineElem, bookmarks map[string]Dest, pages map[string]int) []OutlineInfo {
|
||||
infos := make([]OutlineInfo, 0, len(outlines))
|
||||
for _, outline := range outlines {
|
||||
info := OutlineInfo{Title: outline.Title, Expanded: outline.Expanded}
|
||||
for _, action := range outline.Actions {
|
||||
if action.Goto != nil {
|
||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||
info.Page = pages[dest.PageID]
|
||||
if info.Page != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(outline.OutlineElem) > 0 {
|
||||
info.Children = outlineInfos(outline.OutlineElem, bookmarks, pages)
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析文档并应用权限默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -235,21 +235,6 @@ func appendGraphicActionSources(sources []pdfActionSource, object GraphicObject,
|
||||
return sources
|
||||
}
|
||||
|
||||
// gotoDest 获取文档内跳转目标
|
||||
// 入参: action 跳转动作, bookmarks 书签
|
||||
// 返回: *Dest 跳转目标
|
||||
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
|
||||
if action.Dest != nil {
|
||||
return action.Dest
|
||||
}
|
||||
if action.Bookmark != nil {
|
||||
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
|
||||
return &dest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// outlineDest 获取大纲跳转目标
|
||||
// 入参: outline 大纲节点, bookmarks 书签
|
||||
// 返回: *Dest 跳转目标
|
||||
|
||||
Reference in new issue
Block a user