mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +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">
|
<main class="workspace">
|
||||||
<aside class="page-list-panel">
|
<aside class="page-list-panel">
|
||||||
<div class="panel-title">页面</div>
|
<div id="pageListTitle" class="panel-title">页面</div>
|
||||||
<div id="pageList" class="page-list"></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>
|
</aside>
|
||||||
|
|
||||||
<section class="viewer-panel" tabindex="0">
|
<section class="viewer-panel" tabindex="0">
|
||||||
|
|||||||
@@ -371,6 +371,84 @@ body[data-hide-meta] .meta-panel {
|
|||||||
line-height: 1;
|
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 {
|
.page-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -470,6 +548,8 @@ body[data-hide-meta] .meta-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-state[hidden],
|
.empty-state[hidden],
|
||||||
|
.navigation-tabs[hidden],
|
||||||
|
.page-list[hidden],
|
||||||
.page-frame[hidden],
|
.page-frame[hidden],
|
||||||
.progress-panel[hidden] {
|
.progress-panel[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -93,7 +93,12 @@ const el = {
|
|||||||
viewerPanel: document.querySelector(".viewer-panel"),
|
viewerPanel: document.querySelector(".viewer-panel"),
|
||||||
svgHost: document.querySelector("#svgHost"),
|
svgHost: document.querySelector("#svgHost"),
|
||||||
pageListPanel: document.querySelector(".page-list-panel"),
|
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"),
|
pageList: document.querySelector("#pageList"),
|
||||||
|
outlineList: document.querySelector("#outlineList"),
|
||||||
metaPanel: document.querySelector(".meta-panel"),
|
metaPanel: document.querySelector(".meta-panel"),
|
||||||
appPanel: document.querySelector("#appPanel"),
|
appPanel: document.querySelector("#appPanel"),
|
||||||
offlineStatus: document.querySelector("#offlineStatus"),
|
offlineStatus: document.querySelector("#offlineStatus"),
|
||||||
@@ -119,6 +124,16 @@ const el = {
|
|||||||
el.ofdButton.addEventListener("click", openOFDFile);
|
el.ofdButton.addEventListener("click", openOFDFile);
|
||||||
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
|
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
|
||||||
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
|
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.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);
|
||||||
@@ -1178,6 +1193,9 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
resetPageFlow();
|
resetPageFlow();
|
||||||
renderPageList();
|
renderPageList();
|
||||||
|
if (options.resetScroll || el.outlineList.childElementCount === 0) {
|
||||||
|
renderOutlines();
|
||||||
|
}
|
||||||
renderMeta();
|
renderMeta();
|
||||||
renderPageFlow();
|
renderPageFlow();
|
||||||
if (options.resetScroll) {
|
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() {
|
function renderPageList() {
|
||||||
el.pageList.replaceChildren();
|
el.pageList.replaceChildren();
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
|
|||||||
@@ -127,9 +127,13 @@ type DocumentInfo struct {
|
|||||||
Fonts []FontInfo `json:"fonts"`
|
Fonts []FontInfo `json:"fonts"`
|
||||||
Signatures []SignatureInfo `json:"signatures"`
|
Signatures []SignatureInfo `json:"signatures"`
|
||||||
Pages []PageInfo `json:"pages"`
|
Pages []PageInfo `json:"pages"`
|
||||||
|
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
||||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OutlineInfo 目录节点信息
|
||||||
|
type OutlineInfo = ofdgo.OutlineInfo
|
||||||
|
|
||||||
// PageInfo 页面信息
|
// PageInfo 页面信息
|
||||||
type PageInfo struct {
|
type PageInfo struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
@@ -257,6 +261,7 @@ func (s *Session) Summary() DocumentInfo {
|
|||||||
DocType: s.Reader.DocType(),
|
DocType: s.Reader.DocType(),
|
||||||
PageCount: len(s.doc.Pages.Page),
|
PageCount: len(s.doc.Pages.Page),
|
||||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||||
|
Outlines: s.doc.OutlineInfos(),
|
||||||
DetailsPending: true,
|
DetailsPending: true,
|
||||||
}
|
}
|
||||||
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||||
@@ -300,6 +305,7 @@ func (s *Session) Info() DocumentInfo {
|
|||||||
DocType: s.Reader.DocType(),
|
DocType: s.Reader.DocType(),
|
||||||
PageCount: len(s.doc.Pages.Page),
|
PageCount: len(s.doc.Pages.Page),
|
||||||
Pages: make([]PageInfo, 0, 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 {
|
if signatures, err := s.signatureInfos(); err == nil {
|
||||||
info.Signatures = signatures
|
info.Signatures = signatures
|
||||||
|
|||||||
@@ -102,6 +102,21 @@ type RegionCommand struct {
|
|||||||
EndPoint string
|
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 解析跳转目标
|
// UnmarshalXML 解析跳转目标
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
@@ -161,6 +161,14 @@ type OutlineElem struct {
|
|||||||
OutlineElem []OutlineElem `xml:"OutlineElem"`
|
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 权限声明
|
// Permissions 权限声明
|
||||||
type Permissions struct {
|
type Permissions struct {
|
||||||
Edit bool `xml:"Edit"`
|
Edit bool `xml:"Edit"`
|
||||||
@@ -182,6 +190,48 @@ type ValidPeriod struct {
|
|||||||
EndDate string `xml:"EndDate,attr"`
|
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 解析文档并应用权限默认值
|
// UnmarshalXML 解析文档并应用权限默认值
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
@@ -235,21 +235,6 @@ func appendGraphicActionSources(sources []pdfActionSource, object GraphicObject,
|
|||||||
return sources
|
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 获取大纲跳转目标
|
// outlineDest 获取大纲跳转目标
|
||||||
// 入参: outline 大纲节点, bookmarks 书签
|
// 入参: outline 大纲节点, bookmarks 书签
|
||||||
// 返回: *Dest 跳转目标
|
// 返回: *Dest 跳转目标
|
||||||
|
|||||||
Reference in new issue
Block a user