This commit is contained in:
parent
5e657df015
commit
2403d5107d
78 changed files with 9107 additions and 6578 deletions
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "in",
|
||||
"unitMillimeters": "mm",
|
||||
"additionalLayers": "Zusätzliche Ebenen",
|
||||
"thumbPage": "Miniaturansicht von Seite {{page}}",
|
||||
"thumbsTitle": "Miniaturansichten anzeigen",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "in",
|
||||
"unitMillimeters": "mm",
|
||||
"additionalLayers": "Additional Layers",
|
||||
"thumbPage": "Thumbnail of Page {{page}}",
|
||||
"thumbsTitle": "Show Thumbnails",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "pulgadas",
|
||||
"unitMillimeters": "milímetros",
|
||||
"additionalLayers": "Capas adicionales",
|
||||
"thumbPage": "Miniatura de la página {{page}}",
|
||||
"thumbsTitle": "Mostrar miniaturas",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "in",
|
||||
"unitMillimeters": "mm",
|
||||
"additionalLayers": "Calques additionnels",
|
||||
"thumbPage": "Vignette de la page {{page}}",
|
||||
"thumbsTitle": "Afficher les vignettes",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "אינצ'ים",
|
||||
"unitMillimeters": "מילימטרים",
|
||||
"additionalLayers": "שכבות נוספות",
|
||||
"thumbPage": "תמונת עמוד {{page}}",
|
||||
"thumbsTitle": "הצג תמונות מקטנות",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
@ -1549,7 +1548,7 @@
|
|||
"241": "אין תמיכה בגרירה לתת הכותרת",
|
||||
"242": "מעט מקום נותר [%s], נדרש לפחות [%s] כדי לבצע פעולה זו",
|
||||
"243": "רק רשום את %d התגים הראשונים (כולל תתי תגים), אם יש צורך להתאים, אנא שנה את [הגדרות - עץ המסמכים - מספר מקסימלי לרשימה]",
|
||||
"244": "אינדוקס הנתונים לא הושלם לאחר השימוש האחרון. אנא הפעל את [עץ מסמכים - שכתוב אינדקס]. אנא צא מהתוכנית לחלוטין לפני כיבוי המחשב.",
|
||||
"244": "אינדוקס הנתונים לא הושלם לאחר השימוש האחרון. אנא הפעל את [עץ מסמכים - שכתוב אינדקס]. אנא צא מהתוכנית לחלוטין לפני כיבוי המחשב.",
|
||||
"245": "אינדוקס הנתונים לא הושלם לאחר השימוש האחרון. אנא זכור לבצע [עץ המסמכים - שחזור אינדקס]. אנא השתמש [צא מהאפליקציה] בפאנל הטורי הימני כדי לצאת על פי סדר",
|
||||
"246": "כותרת המסמך לא יכולה להכיל / והחלפה ב- _ ",
|
||||
"247": "הקובץ [%s] גדול יותר מהמגבלה המקסימלית [%s], והוזנח להעלות בענן",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "in",
|
||||
"unitMillimeters": "mm",
|
||||
"additionalLayers": "Livelli aggiuntivi",
|
||||
"thumbPage": "Miniatura di pagina {{page}}",
|
||||
"thumbsTitle": "Mostra miniature",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
@ -1556,4 +1555,3 @@
|
|||
"248": "L'intestazione di destinazione si trova nel blocco contenitore e non può essere utilizzata come punto di rilascio"
|
||||
}
|
||||
}
|
||||
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "インチ",
|
||||
"unitMillimeters": "ミリ",
|
||||
"additionalLayers": "追加のレイヤー",
|
||||
"thumbPage": "ページ {{page}} のサムネイル",
|
||||
"thumbsTitle": "サムネイルを表示",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "in",
|
||||
"unitMillimeters": "mm",
|
||||
"additionalLayers": "Dodatkowe warstwy",
|
||||
"thumbPage": "Miniatura strony {{page}}",
|
||||
"thumbsTitle": "Pokaż miniatury",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
@ -1555,4 +1554,4 @@
|
|||
"247": "Plik [%s] jest większy niż maksymalne ograniczenie [%s], i został zignorowany przy przesyłaniu do chmury",
|
||||
"248": "Docelowy nagłówek znajduje się w bloku kontenera i nie może być użyty jako punkt upuszczenia"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "дюйм",
|
||||
"unitMillimeters": "мм",
|
||||
"additionalLayers": "Дополнительные слои",
|
||||
"thumbPage": "Миниатюра страницы {{page}}",
|
||||
"thumbsTitle": "Показать миниатюры",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "英寸",
|
||||
"unitMillimeters": "毫米",
|
||||
"additionalLayers": "其他圖層",
|
||||
"thumbPage": "頁面 {{page}} 的縮略圖",
|
||||
"thumbsTitle": "顯示縮略圖",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
|
@ -702,7 +702,6 @@
|
|||
"unitInches": "英寸",
|
||||
"unitMillimeters": "毫米",
|
||||
"additionalLayers": "其他图层",
|
||||
"thumbPage": "页面 {{page}} 的缩略图",
|
||||
"thumbsTitle": "显示缩略图",
|
||||
"document_properties_page_size_name_a3": "A3",
|
||||
"document_properties_page_size_name_a4": "A4",
|
||||
|
|
18
app/pnpm-lock.yaml
generated
18
app/pnpm-lock.yaml
generated
|
@ -3300,7 +3300,7 @@ snapshots:
|
|||
|
||||
app-builder-bin@5.0.0-alpha.7: {}
|
||||
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
app-builder-lib@24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
'@develar/schema-utils': 2.6.5
|
||||
'@electron/notarize': 2.2.1
|
||||
|
@ -3314,7 +3314,7 @@ snapshots:
|
|||
builder-util-runtime: 9.2.4
|
||||
chromium-pickle-js: 0.2.0
|
||||
debug: 4.3.4
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5)
|
||||
ejs: 3.1.9
|
||||
electron-builder-squirrel-windows: 25.0.5(dmg-builder@24.13.3)
|
||||
electron-publish: 24.13.1
|
||||
|
@ -3334,7 +3334,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
app-builder-lib@25.0.5(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
app-builder-lib@25.0.5(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
'@develar/schema-utils': 2.6.5
|
||||
'@electron/notarize': 2.3.2
|
||||
|
@ -3349,7 +3349,7 @@ snapshots:
|
|||
builder-util-runtime: 9.2.5
|
||||
chromium-pickle-js: 0.2.0
|
||||
debug: 4.3.4
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5)
|
||||
ejs: 3.1.9
|
||||
electron-builder-squirrel-windows: 25.0.5(dmg-builder@24.13.3)
|
||||
electron-publish: 25.0.3
|
||||
|
@ -3814,9 +3814,9 @@ snapshots:
|
|||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
fs-extra: 10.1.0
|
||||
|
@ -3891,7 +3891,7 @@ snapshots:
|
|||
|
||||
electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 25.0.5(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
app-builder-lib: 25.0.5(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
archiver: 5.3.2
|
||||
builder-util: 25.0.3
|
||||
fs-extra: 10.1.0
|
||||
|
@ -3902,11 +3902,11 @@ snapshots:
|
|||
|
||||
electron-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3)))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
app-builder-lib: 24.13.3(dmg-builder@24.13.3(electron-builder-squirrel-windows@25.0.5))(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
chalk: 4.1.2
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5(dmg-builder@24.13.3))
|
||||
dmg-builder: 24.13.3(electron-builder-squirrel-windows@25.0.5)
|
||||
fs-extra: 10.1.0
|
||||
is-ci: 3.0.1
|
||||
lazy-val: 1.0.5
|
||||
|
|
|
@ -8,8 +8,9 @@ import {Constants} from "../constants";
|
|||
import {Dialog} from "../dialog";
|
||||
import {showMessage} from "../dialog/message";
|
||||
|
||||
export const initAnno = (element: HTMLElement, pdf: any, pdfConfig: any) => {
|
||||
export const initAnno = (element: HTMLElement, pdf: any) => {
|
||||
getConfig(pdf);
|
||||
const pdfConfig = pdf.appConfig;
|
||||
const rectAnnoElement = pdfConfig.toolbar.rectAnno;
|
||||
rectAnnoElement.addEventListener("click", () => {
|
||||
if (rectAnnoElement.classList.contains("toggled")) {
|
||||
|
@ -644,16 +645,17 @@ export const getHighlight = (element: HTMLElement) => {
|
|||
const showHighlight = (selected: IPdfAnno, pdf: any, hl?: boolean) => {
|
||||
const pageIndex = selected.index;
|
||||
const page = pdf.pdfViewer.getPageView(pageIndex);
|
||||
let textLayerElement = page.textLayer.div;
|
||||
const textLayerElement = page.textLayer.div;
|
||||
if (!textLayerElement.lastElementChild) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport = page.viewport.clone({rotation: 0}); // rotation https://github.com/siyuan-note/siyuan/issues/9831
|
||||
if (textLayerElement.lastElementChild.classList.contains("endOfContent")) {
|
||||
textLayerElement.insertAdjacentHTML("beforeend", "<div></div>");
|
||||
let rectsElement = textLayerElement.querySelector(".pdf__rects");
|
||||
if (!rectsElement) {
|
||||
textLayerElement.insertAdjacentHTML("beforeend", "<div class='pdf__rects'></div>");
|
||||
rectsElement = textLayerElement.querySelector(".pdf__rects");
|
||||
}
|
||||
textLayerElement = textLayerElement.lastElementChild;
|
||||
let html = `<div class="pdf__rect popover__block" data-node-id="${selected.id}" data-relations="${selected.ids || ""}" data-mode="${selected.mode}">`;
|
||||
selected.coords.forEach((rect) => {
|
||||
const bounds = viewport.convertToViewportRectangle(rect);
|
||||
|
@ -666,17 +668,17 @@ const showHighlight = (selected: IPdfAnno, pdf: any, hl?: boolean) => {
|
|||
style = `border: 2px solid ${selected.color};`;
|
||||
}
|
||||
html += `<div style="${style}
|
||||
left:${Math.min(bounds[0], bounds[2])}px;
|
||||
top:${Math.min(bounds[1], bounds[3])}px;
|
||||
width:${width}px;
|
||||
height: ${Math.abs(bounds[1] - bounds[3])}px"></div>`;
|
||||
left:${Math.min(bounds[0], bounds[2])}px;
|
||||
top:${Math.min(bounds[1], bounds[3])}px;
|
||||
width:${width}px;
|
||||
height: ${Math.abs(bounds[1] - bounds[3])}px"></div>`;
|
||||
});
|
||||
textLayerElement.insertAdjacentHTML("beforeend", html + "</div>");
|
||||
textLayerElement.lastElementChild.setAttribute("data-content", selected.content);
|
||||
rectsElement.insertAdjacentHTML("beforeend", html + "</div>");
|
||||
rectsElement.lastElementChild.setAttribute("data-content", selected.content);
|
||||
if (hl) {
|
||||
hlPDFRect(textLayerElement, selected.id);
|
||||
hlPDFRect(rectsElement, selected.id);
|
||||
}
|
||||
return textLayerElement.lastElementChild;
|
||||
return rectsElement.lastElementChild;
|
||||
};
|
||||
|
||||
export const hlPDFRect = (element: HTMLElement, id: string) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {setModelsHash} from "../window/setHeader";
|
|||
// @ts-ignore
|
||||
import {webViewerLoad} from "./pdf/viewer";
|
||||
// @ts-ignore
|
||||
import {webViewerPageNumberChanged} from "./pdf/app";
|
||||
import {onPageNumberChanged} from "./pdf/app";
|
||||
/// #endif
|
||||
import {fetchPost} from "../util/fetch";
|
||||
import {setStorageVal, updateHotkeyTip} from "../protyle/util/compatibility";
|
||||
|
@ -73,12 +73,12 @@ export class Asset extends Model {
|
|||
/// #if !MOBILE
|
||||
if (typeof pdfId === "string") {
|
||||
this.getPdfId(() => {
|
||||
webViewerPageNumberChanged({value: this.pdfPage, pdfInstance: this.pdfObject, id: this.pdfId});
|
||||
onPageNumberChanged({value: this.pdfPage, pdfInstance: this.pdfObject, id: this.pdfId});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof pdfId === "number" && !isNaN(pdfId)) {
|
||||
webViewerPageNumberChanged({value: this.pdfId, pdfInstance: this.pdfObject});
|
||||
onPageNumberChanged({value: this.pdfId, pdfInstance: this.pdfObject});
|
||||
}
|
||||
/// #endif
|
||||
}
|
||||
|
@ -135,10 +135,10 @@ export class Asset extends Model {
|
|||
<div class="findbar b3-menu fn__hidden doorHanger" id="findbar">
|
||||
<input id="findInput" class="toolbarField b3-text-field" placeholder="${window.siyuan.languages.search}">
|
||||
<div class="fn__space"></div>
|
||||
<button id="findPrevious" class="toolbarButton findPrevious b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.previous}">
|
||||
<button id="findPreviousButton" class="toolbarButton findPrevious b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.previous}">
|
||||
<svg><use xlink:href="#iconUp"></use></svg>
|
||||
</button>
|
||||
<button id="findNext" class="toolbarButton findNext b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.next}">
|
||||
<button id="findNextButton" class="toolbarButton findNext b3-tooltips b3-tooltips__n" aria-label="${window.siyuan.languages.next}">
|
||||
<svg><use xlink:href="#iconDown"></use></svg>
|
||||
</button>
|
||||
<label class="b3-button b3-button--outline b3-button--small">
|
||||
|
@ -196,12 +196,12 @@ export class Asset extends Model {
|
|||
<span class="b3-menu__accelerator">End</span>
|
||||
</button>
|
||||
<div class="horizontalToolbarSeparator b3-menu__separator"></div>
|
||||
<button id="zoomOut" class="secondaryToolbarButton b3-menu__item zoomOut">
|
||||
<button id="zoomOutButton" class="secondaryToolbarButton b3-menu__item zoomOut">
|
||||
<svg class="b3-menu__icon"><use xlink:href="#iconLine"></use></svg>
|
||||
<span class="b3-menu__label">${window.siyuan.languages.zoomOut}</span>
|
||||
<span class="b3-menu__accelerator">${updateHotkeyTip("⌘-")}</span>
|
||||
</button>
|
||||
<button id="zoomIn" class="secondaryToolbarButton b3-menu__item zoomIn">
|
||||
<button id="zoomInButton" class="secondaryToolbarButton b3-menu__item zoomIn">
|
||||
<svg class="b3-menu__icon"><use xlink:href="#iconAdd"></use></svg>
|
||||
<span class="b3-menu__label">${window.siyuan.languages.zoomIn}</span>
|
||||
<span class="b3-menu__accelerator">${updateHotkeyTip("⌘=")}</span>
|
||||
|
@ -273,10 +273,10 @@ export class Asset extends Model {
|
|||
<div class="pdf__toolbar">
|
||||
<div id="toolbarContainer">
|
||||
<div id="toolbarViewer">
|
||||
<button id="sidebarToggle" class="toolbarButton b3-tooltips b3-tooltips__se" aria-expanded="false" aria-controls="sidebarContainer" aria-label="${window.siyuan.languages.toggleSidebarNotification2Title} ${updateHotkeyTip("F4")}">
|
||||
<button id="sidebarToggleButton" class="toolbarButton b3-tooltips b3-tooltips__se" aria-expanded="false" aria-controls="sidebarContainer" aria-label="${window.siyuan.languages.toggleSidebarNotification2Title} ${updateHotkeyTip("F4")}">
|
||||
<svg><use xlink:href="#iconLayoutRight"></use></svg>
|
||||
</button>
|
||||
<button id="viewFind" class="toolbarButton b3-tooltips b3-tooltips__se" aria-expanded="false" aria-controls="findbar" aria-label="${window.siyuan.languages.search} ${updateHotkeyTip("⌘F")}">
|
||||
<button id="viewFindButton" class="toolbarButton b3-tooltips b3-tooltips__se" aria-expanded="false" aria-controls="findbar" aria-label="${window.siyuan.languages.search} ${updateHotkeyTip("⌘F")}">
|
||||
<svg><use xlink:href="#iconSearch"></use></svg>
|
||||
</button>
|
||||
<button id="rectAnno" class="toolbarButton b3-tooltips b3-tooltips__se" aria-expanded="false" aria-controls="findbar" aria-label="${window.siyuan.languages.rectAnnotation} ${updateHotkeyTip("⌘D")}/${updateHotkeyTip("⌥D")}">
|
||||
|
@ -303,11 +303,11 @@ export class Asset extends Model {
|
|||
</select>
|
||||
</span>
|
||||
<span id="scrollPage" class="fn__none"></span>
|
||||
<span id="print" class="fn__none"></span>
|
||||
<span id="printButton" class="fn__none"></span>
|
||||
<span id="secondaryPrint" class="fn__none"></span>
|
||||
<span id="viewBookmark" class="fn__none"></span>
|
||||
<span id="secondaryViewBookmark" class="fn__none"></span>
|
||||
<button id="secondaryToolbarToggle" class="toolbarButton b3-tooltips b3-tooltips__sw" aria-label="${window.siyuan.languages.more}" aria-expanded="false" aria-controls="secondaryToolbar">
|
||||
<button id="secondaryToolbarToggleButton" class="toolbarButton b3-tooltips b3-tooltips__sw" aria-label="${window.siyuan.languages.more}" aria-expanded="false" aria-controls="secondaryToolbar">
|
||||
<svg><use xlink:href="#iconMore"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -453,11 +453,18 @@ export class Asset extends Model {
|
|||
<input id="editorInkColor">
|
||||
<input id="editorInkThickness">
|
||||
<input id="editorInkOpacity">
|
||||
<input id="download">
|
||||
<input id="editorStampAddImage">
|
||||
<input id="editorFreeHighlightThickness">
|
||||
<input id="editorHighlightShowAll">
|
||||
<input id="downloadButton">
|
||||
<input id="secondaryDownload">
|
||||
<input id="editorFreeText">
|
||||
<input id="editorFreeTextButton">
|
||||
<input id="openFile">
|
||||
<input id="editorInk">
|
||||
<input id="editorInkButton">
|
||||
<input id="editorStampButton">
|
||||
<input id="editorHighlightButton">
|
||||
<input id="imageAltTextSettings">
|
||||
<input id="secondaryOpenFile">
|
||||
</div>
|
||||
</div> <!-- outerContainer -->
|
||||
<div id="printContainer"></div>`;
|
||||
|
|
|
@ -21,34 +21,58 @@
|
|||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
|
||||
|
||||
import { AnnotationEditorLayer } from "./pdfjs";
|
||||
import { NullL10n } from "./l10n_utils.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
||||
* @property {AnnotationEditorUIManager} [uiManager]
|
||||
* @property {HTMLDivElement} pageDiv
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {IL10n} [l10n]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationLayer} [annotationLayer]
|
||||
* @property {TextLayer} [textLayer]
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
* @property {function} [onAppend]
|
||||
*/
|
||||
|
||||
class AnnotationEditorLayerBuilder {
|
||||
#annotationLayer = null;
|
||||
|
||||
#drawLayer = null;
|
||||
|
||||
#onAppend = null;
|
||||
|
||||
#structTreeLayer = null;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#uiManager;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pageDiv = options.pageDiv;
|
||||
this.pdfPage = options.pdfPage;
|
||||
this.accessibilityManager = options.accessibilityManager;
|
||||
this.l10n = options.l10n || NullL10n;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
}
|
||||
this.annotationEditorLayer = null;
|
||||
this.div = null;
|
||||
this._cancelled = false;
|
||||
this.#uiManager = options.uiManager;
|
||||
this.#annotationLayer = options.annotationLayer || null;
|
||||
this.#textLayer = options.textLayer || null;
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
this.#onAppend = options.onAppend || null;
|
||||
this.#structTreeLayer = options.structTreeLayer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,17 +98,21 @@ class AnnotationEditorLayerBuilder {
|
|||
// Create an AnnotationEditor layer div
|
||||
const div = (this.div = document.createElement("div"));
|
||||
div.className = "annotationEditorLayer";
|
||||
div.tabIndex = 0;
|
||||
div.hidden = true;
|
||||
this.pageDiv.append(div);
|
||||
div.dir = this.#uiManager.direction;
|
||||
this.#onAppend?.(div);
|
||||
|
||||
this.annotationEditorLayer = new AnnotationEditorLayer({
|
||||
uiManager: this.#uiManager,
|
||||
div,
|
||||
structTreeLayer: this.#structTreeLayer,
|
||||
accessibilityManager: this.accessibilityManager,
|
||||
pageIndex: this.pdfPage.pageNumber - 1,
|
||||
l10n: this.l10n,
|
||||
viewport: clonedViewport,
|
||||
annotationLayer: this.#annotationLayer,
|
||||
textLayer: this.#textLayer,
|
||||
drawLayer: this.#drawLayer,
|
||||
});
|
||||
|
||||
const parameters = {
|
||||
|
@ -104,9 +132,7 @@ class AnnotationEditorLayerBuilder {
|
|||
if (!this.div) {
|
||||
return;
|
||||
}
|
||||
this.pageDiv = null;
|
||||
this.annotationEditorLayer.destroy();
|
||||
this.div.remove();
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
@ -117,7 +143,7 @@ class AnnotationEditorLayerBuilder {
|
|||
}
|
||||
|
||||
show() {
|
||||
if (!this.div || this.annotationEditorLayer.isEmpty) {
|
||||
if (!this.div || this.annotationEditorLayer.isInvisible) {
|
||||
return;
|
||||
}
|
||||
this.div.hidden = false;
|
||||
|
|
|
@ -13,8 +13,22 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
|
||||
import { AnnotationEditorParamsType } from "./pdfjs";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorParamsOptions
|
||||
* @property {HTMLInputElement} editorFreeTextFontSize
|
||||
* @property {HTMLInputElement} editorFreeTextColor
|
||||
* @property {HTMLInputElement} editorInkColor
|
||||
* @property {HTMLInputElement} editorInkThickness
|
||||
* @property {HTMLInputElement} editorInkOpacity
|
||||
* @property {HTMLButtonElement} editorStampAddImage
|
||||
* @property {HTMLInputElement} editorFreeHighlightThickness
|
||||
* @property {HTMLButtonElement} editorHighlightShowAll
|
||||
*/
|
||||
|
||||
class AnnotationEditorParams {
|
||||
/**
|
||||
* @param {AnnotationEditorParamsOptions} options
|
||||
|
@ -25,47 +39,58 @@ class AnnotationEditorParams {
|
|||
this.#bindListeners(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorParamsOptions} options
|
||||
*/
|
||||
#bindListeners({
|
||||
editorFreeTextFontSize,
|
||||
editorFreeTextColor,
|
||||
editorInkColor,
|
||||
editorInkThickness,
|
||||
editorInkOpacity,
|
||||
editorStampAddImage,
|
||||
editorFreeHighlightThickness,
|
||||
editorHighlightShowAll,
|
||||
}) {
|
||||
editorFreeTextFontSize.addEventListener("input", evt => {
|
||||
const dispatchEvent = (typeStr, value) => {
|
||||
this.eventBus.dispatch("switchannotationeditorparams", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType.FREETEXT_SIZE,
|
||||
value: editorFreeTextFontSize.valueAsNumber,
|
||||
type: AnnotationEditorParamsType[typeStr],
|
||||
value,
|
||||
});
|
||||
};
|
||||
editorFreeTextFontSize.addEventListener("input", function () {
|
||||
dispatchEvent("FREETEXT_SIZE", this.valueAsNumber);
|
||||
});
|
||||
editorFreeTextColor.addEventListener("input", evt => {
|
||||
this.eventBus.dispatch("switchannotationeditorparams", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType.FREETEXT_COLOR,
|
||||
value: editorFreeTextColor.value,
|
||||
});
|
||||
editorFreeTextColor.addEventListener("input", function () {
|
||||
dispatchEvent("FREETEXT_COLOR", this.value);
|
||||
});
|
||||
editorInkColor.addEventListener("input", evt => {
|
||||
this.eventBus.dispatch("switchannotationeditorparams", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType.INK_COLOR,
|
||||
value: editorInkColor.value,
|
||||
});
|
||||
editorInkColor.addEventListener("input", function () {
|
||||
dispatchEvent("INK_COLOR", this.value);
|
||||
});
|
||||
editorInkThickness.addEventListener("input", evt => {
|
||||
this.eventBus.dispatch("switchannotationeditorparams", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType.INK_THICKNESS,
|
||||
value: editorInkThickness.valueAsNumber,
|
||||
});
|
||||
editorInkThickness.addEventListener("input", function () {
|
||||
dispatchEvent("INK_THICKNESS", this.valueAsNumber);
|
||||
});
|
||||
editorInkOpacity.addEventListener("input", evt => {
|
||||
this.eventBus.dispatch("switchannotationeditorparams", {
|
||||
editorInkOpacity.addEventListener("input", function () {
|
||||
dispatchEvent("INK_OPACITY", this.valueAsNumber);
|
||||
});
|
||||
editorStampAddImage.addEventListener("click", () => {
|
||||
this.eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
type: AnnotationEditorParamsType.INK_OPACITY,
|
||||
value: editorInkOpacity.valueAsNumber,
|
||||
details: {
|
||||
type: "editing",
|
||||
data: { action: "pdfjs.image.add_image_click" },
|
||||
},
|
||||
});
|
||||
dispatchEvent("CREATE");
|
||||
});
|
||||
editorFreeHighlightThickness.addEventListener("input", function () {
|
||||
dispatchEvent("HIGHLIGHT_THICKNESS", this.valueAsNumber);
|
||||
});
|
||||
editorHighlightShowAll.addEventListener("click", function () {
|
||||
const checked = this.getAttribute("aria-pressed") === "true";
|
||||
this.setAttribute("aria-pressed", !checked);
|
||||
dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked);
|
||||
});
|
||||
|
||||
this.eventBus._on("annotationeditorparamschanged", evt => {
|
||||
|
@ -86,6 +111,15 @@ class AnnotationEditorParams {
|
|||
case AnnotationEditorParamsType.INK_OPACITY:
|
||||
editorInkOpacity.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
|
||||
editorFreeHighlightThickness.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_FREE:
|
||||
editorFreeHighlightThickness.disabled = !value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL:
|
||||
editorHighlightShowAll.setAttribute("aria-pressed", value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,72 +16,75 @@
|
|||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./textaccessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
|
||||
import { AnnotationLayer } from "./pdfjs";
|
||||
import { NullL10n } from "./l10n_utils.js";
|
||||
import { PresentationModeState } from "./ui_utils.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} AnnotationLayerBuilderOptions
|
||||
* @property {HTMLDivElement} pageDiv
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {AnnotationStorage} [annotationStorage]
|
||||
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||
* for annotation icons. Include trailing slash.
|
||||
* @property {boolean} renderForms
|
||||
* @property {IPDFLinkService} linkService
|
||||
* @property {IDownloadManager} downloadManager
|
||||
* @property {IL10n} l10n - Localization service.
|
||||
* @property {IDownloadManager} [downloadManager]
|
||||
* @property {boolean} [enableScripting]
|
||||
* @property {Promise<boolean>} [hasJSActionsPromise]
|
||||
* @property {Promise<Object<string, Array<Object>> | null>}
|
||||
* [fieldObjectsPromise]
|
||||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||
* @property {function} [onAppend]
|
||||
*/
|
||||
|
||||
class AnnotationLayerBuilder {
|
||||
#numAnnotations = 0;
|
||||
#onAppend = null;
|
||||
|
||||
#onPresentationModeChanged = null;
|
||||
#eventAbortController = null;
|
||||
|
||||
/**
|
||||
* @param {AnnotationLayerBuilderOptions} options
|
||||
*/
|
||||
constructor({
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
linkService,
|
||||
downloadManager,
|
||||
annotationStorage = null,
|
||||
imageResourcesPath = "",
|
||||
renderForms = true,
|
||||
l10n = NullL10n,
|
||||
enableScripting = false,
|
||||
hasJSActionsPromise = null,
|
||||
fieldObjectsPromise = null,
|
||||
annotationCanvasMap = null,
|
||||
accessibilityManager = null,
|
||||
annotationEditorUIManager = null,
|
||||
onAppend = null,
|
||||
}) {
|
||||
this.pageDiv = pageDiv;
|
||||
this.pdfPage = pdfPage;
|
||||
this.linkService = linkService;
|
||||
this.downloadManager = downloadManager;
|
||||
this.imageResourcesPath = imageResourcesPath;
|
||||
this.renderForms = renderForms;
|
||||
this.l10n = l10n;
|
||||
this.annotationStorage = annotationStorage;
|
||||
this.enableScripting = enableScripting;
|
||||
this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
|
||||
this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
|
||||
this._annotationCanvasMap = annotationCanvasMap;
|
||||
this._accessibilityManager = accessibilityManager;
|
||||
this._annotationEditorUIManager = annotationEditorUIManager;
|
||||
this.#onAppend = onAppend;
|
||||
|
||||
this.annotationLayer = null;
|
||||
this.div = null;
|
||||
this._cancelled = false;
|
||||
this._eventBus = linkService.eventBus;
|
||||
|
@ -89,21 +92,20 @@ class AnnotationLayerBuilder {
|
|||
|
||||
/**
|
||||
* @param {PageViewport} viewport
|
||||
* @param {Object} options
|
||||
* @param {string} intent (default value is 'display')
|
||||
* @returns {Promise<void>} A promise that is resolved when rendering of the
|
||||
* annotations is complete.
|
||||
*/
|
||||
async render(viewport, intent = "display") {
|
||||
async render(viewport, options, intent = "display") {
|
||||
if (this.div) {
|
||||
if (this._cancelled || this.#numAnnotations === 0) {
|
||||
if (this._cancelled || !this.annotationLayer) {
|
||||
return;
|
||||
}
|
||||
// If an annotationLayer already exists, refresh its children's
|
||||
// transformation matrices.
|
||||
AnnotationLayer.update({
|
||||
this.annotationLayer.update({
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
div: this.div,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -116,23 +118,30 @@ class AnnotationLayerBuilder {
|
|||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
this.#numAnnotations = annotations.length;
|
||||
|
||||
// Create an annotation layer div and render the annotations
|
||||
// if there is at least one annotation.
|
||||
this.div = document.createElement("div");
|
||||
this.div.className = "annotationLayer";
|
||||
this.pageDiv.append(this.div);
|
||||
const div = (this.div = document.createElement("div"));
|
||||
div.className = "annotationLayer";
|
||||
this.#onAppend?.(div);
|
||||
|
||||
if (this.#numAnnotations === 0) {
|
||||
if (annotations.length === 0) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
AnnotationLayer.render({
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
div: this.div,
|
||||
annotations,
|
||||
|
||||
this.annotationLayer = new AnnotationLayer({
|
||||
div,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
annotationEditorUIManager: this._annotationEditorUIManager,
|
||||
page: this.pdfPage,
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
structTreeLayer: options?.structTreeLayer || null,
|
||||
});
|
||||
|
||||
await this.annotationLayer.render({
|
||||
annotations,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
renderForms: this.renderForms,
|
||||
linkService: this.linkService,
|
||||
|
@ -141,23 +150,22 @@ class AnnotationLayerBuilder {
|
|||
enableScripting: this.enableScripting,
|
||||
hasJSActions,
|
||||
fieldObjects,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
});
|
||||
// NOTE this.l10n.translate(this.div);
|
||||
|
||||
// Ensure that interactive form elements in the annotationLayer are
|
||||
// disabled while PresentationMode is active (see issue 12232).
|
||||
if (this.linkService.isInPresentationMode) {
|
||||
this.#updatePresentationModeState(PresentationModeState.FULLSCREEN);
|
||||
}
|
||||
if (!this.#onPresentationModeChanged) {
|
||||
this.#onPresentationModeChanged = evt => {
|
||||
this.#updatePresentationModeState(evt.state);
|
||||
};
|
||||
if (!this.#eventAbortController) {
|
||||
this.#eventAbortController = new AbortController();
|
||||
|
||||
this._eventBus?._on(
|
||||
"presentationmodechanged",
|
||||
this.#onPresentationModeChanged
|
||||
evt => {
|
||||
this.#updatePresentationModeState(evt.state);
|
||||
},
|
||||
{ signal: this.#eventAbortController.signal }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -165,13 +173,8 @@ class AnnotationLayerBuilder {
|
|||
cancel() {
|
||||
this._cancelled = true;
|
||||
|
||||
if (this.#onPresentationModeChanged) {
|
||||
this._eventBus?._off(
|
||||
"presentationmodechanged",
|
||||
this.#onPresentationModeChanged
|
||||
);
|
||||
this.#onPresentationModeChanged = null;
|
||||
}
|
||||
this.#eventAbortController?.abort();
|
||||
this.#eventAbortController = null;
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
@ -181,6 +184,10 @@ class AnnotationLayerBuilder {
|
|||
this.div.hidden = true;
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
|
||||
#updatePresentationModeState(state) {
|
||||
if (!this.div) {
|
||||
return;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,10 +12,10 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Constants } from '../../constants'
|
||||
|
||||
const compatibilityParams = Object.create(null);
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// eslint-disable-next-line no-var
|
||||
var compatParams = new Map();
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("LIB") &&
|
||||
|
@ -34,26 +34,117 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|||
|
||||
// Limit canvas size to 5 mega-pixels on mobile.
|
||||
// Support: Android, iOS
|
||||
(function checkCanvasSizeLimitation() {
|
||||
(function () {
|
||||
if (isIOS || isAndroid) {
|
||||
compatibilityParams.maxCanvasPixels = 5242880;
|
||||
compatParams.set("maxCanvasPixels", 5242880);
|
||||
}
|
||||
})();
|
||||
|
||||
// Don't use system fonts on Android (issue 18210).
|
||||
// Support: Android
|
||||
(function () {
|
||||
if (isAndroid) {
|
||||
compatParams.set("useSystemFonts", false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const OptionKind = {
|
||||
BROWSER: 0x01,
|
||||
VIEWER: 0x02,
|
||||
API: 0x04,
|
||||
WORKER: 0x08,
|
||||
EVENT_DISPATCH: 0x10,
|
||||
PREFERENCE: 0x80,
|
||||
};
|
||||
|
||||
// Should only be used with options that allow multiple types.
|
||||
const Type = {
|
||||
BOOLEAN: 0x01,
|
||||
NUMBER: 0x02,
|
||||
OBJECT: 0x04,
|
||||
STRING: 0x08,
|
||||
UNDEFINED: 0x10,
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTE: These options are used to generate the `default_preferences.json` file,
|
||||
* see `OptionKind.PREFERENCE`, hence the values below must use only
|
||||
* primitive types and cannot rely on any imported types.
|
||||
*/
|
||||
const defaultOptions = {
|
||||
allowedGlobalEvents: {
|
||||
/** @type {Object} */
|
||||
value: null,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
canvasMaxAreaInBytes: {
|
||||
/** @type {number} */
|
||||
value: -1,
|
||||
kind: OptionKind.BROWSER + OptionKind.API,
|
||||
},
|
||||
isInAutomation: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
localeProperties: {
|
||||
/** @type {Object} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
|
||||
? { lang: navigator.language || "en-US" }
|
||||
: null,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
nimbusDataStr: {
|
||||
/** @type {string} */
|
||||
value: "",
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsCaretBrowsingMode: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsDocumentFonts: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsIntegratedFind: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsMouseWheelZoomCtrlKey: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsMouseWheelZoomMetaKey: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsPinchToZoom: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
toolbarDensity: {
|
||||
/** @type {number} */
|
||||
value: 0, // 0 = "normal", 1 = "compact", 2 = "touch"
|
||||
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
|
||||
altTextLearnMoreUrl: {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-alt-text"
|
||||
: "",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
annotationEditorMode: {
|
||||
/** @type {number} */
|
||||
value: 0,
|
||||
|
@ -69,6 +160,11 @@ const defaultOptions = {
|
|||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
debuggerSrc: {
|
||||
/** @type {string} */
|
||||
value: "./debugger.mjs",
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
defaultZoomDelay: {
|
||||
/** @type {number} */
|
||||
value: 400,
|
||||
|
@ -89,6 +185,34 @@ const defaultOptions = {
|
|||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableAltText: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableAltTextModelDownload: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
enableGuessAltText: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
|
||||
},
|
||||
enableHighlightFloatingButton: {
|
||||
// We'll probably want to make some experiments before enabling this
|
||||
// in Firefox release, but it has to be temporary.
|
||||
// TODO: remove it when unnecessary.
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableNewAltTextWhenAddingImage: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enablePermissions: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
|
@ -104,6 +228,14 @@ const defaultOptions = {
|
|||
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableUpdatedAddImage: {
|
||||
// We'll probably want to make some experiments before enabling this
|
||||
// in Firefox release, but it has to be temporary.
|
||||
// TODO: remove it when unnecessary.
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
externalLinkRel: {
|
||||
/** @type {string} */
|
||||
value: "noopener noreferrer nofollow",
|
||||
|
@ -114,6 +246,11 @@ const defaultOptions = {
|
|||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
highlightEditorColors: {
|
||||
/** @type {string} */
|
||||
value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
historyUpdateUrl: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
|
@ -126,12 +263,15 @@ const defaultOptions = {
|
|||
},
|
||||
imageResourcesPath: {
|
||||
/** @type {string} */
|
||||
value: "./images/",
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/images/"
|
||||
: "./images/",
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
maxCanvasPixels: {
|
||||
/** @type {number} */
|
||||
value: 16777216,
|
||||
value: 2 ** 25,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
forcePageColors: {
|
||||
|
@ -151,7 +291,7 @@ const defaultOptions = {
|
|||
},
|
||||
pdfBugEnabled: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION"),
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
printResolution: {
|
||||
|
@ -179,16 +319,6 @@ const defaultOptions = {
|
|||
value: 1,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
useOnlyCssZoom: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
viewerCssTheme: {
|
||||
/** @type {number} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME") ? 2 : 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
viewOnLoad: {
|
||||
/** @type {boolean} */
|
||||
value: 0,
|
||||
|
@ -202,7 +332,13 @@ const defaultOptions = {
|
|||
},
|
||||
cMapUrl: {
|
||||
/** @type {string} */
|
||||
value: 'cmaps/', // NOTE
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../external/bcmaps/"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/cmaps/"
|
||||
: "../web/cmaps/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
disableAutoFetch: {
|
||||
|
@ -227,9 +363,14 @@ const defaultOptions = {
|
|||
},
|
||||
docBaseUrl: {
|
||||
/** @type {string} */
|
||||
value: "",
|
||||
value: typeof PDFJSDev === "undefined" ? document.URL.split("#", 1)[0] : "",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
enableHWA: {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL"),
|
||||
kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableXfa: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
|
@ -262,9 +403,29 @@ const defaultOptions = {
|
|||
},
|
||||
standardFontDataUrl: {
|
||||
/** @type {string} */
|
||||
value: "standard_fonts/", // NOTE
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../external/standard_fonts/"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/web/standard_fonts/"
|
||||
: "../web/standard_fonts/",
|
||||
kind: OptionKind.API,
|
||||
},
|
||||
useSystemFonts: {
|
||||
// On Android, there is almost no chance to have the font we want so we
|
||||
// don't use the system fonts in this case (bug 1882613).
|
||||
/** @type {boolean|undefined} */
|
||||
value: (
|
||||
typeof PDFJSDev === "undefined"
|
||||
? window.isGECKOVIEW
|
||||
: PDFJSDev.test("GECKOVIEW")
|
||||
)
|
||||
? false
|
||||
: undefined,
|
||||
kind: OptionKind.API,
|
||||
type: Type.BOOLEAN + Type.UNDEFINED,
|
||||
},
|
||||
verbosity: {
|
||||
/** @type {number} */
|
||||
value: 1,
|
||||
|
@ -278,131 +439,207 @@ const defaultOptions = {
|
|||
},
|
||||
workerSrc: {
|
||||
/** @type {string} */
|
||||
// NOTE
|
||||
value: `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.js?v=3.5.141`,
|
||||
value:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../src/pdf.worker.js"
|
||||
: PDFJSDev.test("MOZCENTRAL")
|
||||
? "resource://pdf.js/build/pdf.worker.mjs"
|
||||
: "../build/pdf.worker.mjs",
|
||||
kind: OptionKind.WORKER,
|
||||
},
|
||||
};
|
||||
if (
|
||||
typeof PDFJSDev === "undefined" ||
|
||||
PDFJSDev.test("!PRODUCTION || GENERIC")
|
||||
) {
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
defaultOptions.defaultUrl = {
|
||||
/** @type {string} */
|
||||
value: "compressed.tracemonkey-pldi-09.pdf",
|
||||
value:
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")
|
||||
? ""
|
||||
: "compressed.tracemonkey-pldi-09.pdf",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.sandboxBundleSrc = {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined"
|
||||
? "../build/dev-sandbox/pdf.sandbox.mjs"
|
||||
: "../build/pdf.sandbox.mjs",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.viewerCssTheme = {
|
||||
/** @type {number} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME") ? 2 : 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
};
|
||||
defaultOptions.enableFakeMLManager = {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
defaultOptions.disablePreferences = {
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.locale = {
|
||||
/** @type {string} */
|
||||
value: navigator.language || "en-US",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.renderer = {
|
||||
/** @type {string} */
|
||||
value: "canvas",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
};
|
||||
defaultOptions.sandboxBundleSrc = {
|
||||
/** @type {string} */
|
||||
value:
|
||||
typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")
|
||||
? "../build/dev-sandbox/pdf.sandbox.js"
|
||||
: "../build/pdf.sandbox.js",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
} else if (PDFJSDev.test("CHROME")) {
|
||||
defaultOptions.defaultUrl = {
|
||||
/** @type {string} */
|
||||
value: "",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
defaultOptions.disableTelemetry = {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
};
|
||||
defaultOptions.sandboxBundleSrc = {
|
||||
/** @type {string} */
|
||||
value: "../build/pdf.sandbox.js",
|
||||
kind: OptionKind.VIEWER,
|
||||
};
|
||||
}
|
||||
|
||||
const userOptions = Object.create(null);
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) {
|
||||
// Ensure that the `defaultOptions` are correctly specified.
|
||||
for (const name in defaultOptions) {
|
||||
const { value, kind, type } = defaultOptions[name];
|
||||
|
||||
if (kind & OptionKind.PREFERENCE) {
|
||||
if (kind === OptionKind.PREFERENCE) {
|
||||
throw new Error(`Cannot use only "PREFERENCE" kind: ${name}`);
|
||||
}
|
||||
if (kind & OptionKind.BROWSER) {
|
||||
throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`);
|
||||
}
|
||||
if (type !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot have \`type\`-field for "PREFERENCE" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (typeof compatParams === "object" && compatParams.has(name)) {
|
||||
throw new Error(
|
||||
`Should not have compatibility-value for "PREFERENCE" kind: ${name}`
|
||||
);
|
||||
}
|
||||
// Only "simple" preference-values are allowed.
|
||||
if (
|
||||
typeof value !== "boolean" &&
|
||||
typeof value !== "string" &&
|
||||
!Number.isInteger(value)
|
||||
) {
|
||||
throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`);
|
||||
}
|
||||
} else if (kind & OptionKind.BROWSER) {
|
||||
if (type !== undefined) {
|
||||
throw new Error(
|
||||
`Cannot have \`type\`-field for "BROWSER" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (typeof compatParams === "object" && compatParams.has(name)) {
|
||||
throw new Error(
|
||||
`Should not have compatibility-value for "BROWSER" kind: ${name}`
|
||||
);
|
||||
}
|
||||
if (value === undefined) {
|
||||
throw new Error(`Invalid value for "BROWSER" kind: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppOptions {
|
||||
static eventBus;
|
||||
|
||||
static #opts = new Map();
|
||||
|
||||
static {
|
||||
// Initialize all the user-options.
|
||||
for (const name in defaultOptions) {
|
||||
this.#opts.set(name, defaultOptions[name].value);
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// Apply any compatibility-values to the user-options.
|
||||
for (const [name, value] of compatParams) {
|
||||
this.#opts.set(name, value);
|
||||
}
|
||||
this._hasInvokedSet = false;
|
||||
|
||||
this._checkDisablePreferences = () => {
|
||||
if (this.get("disablePreferences")) {
|
||||
// Give custom implementations of the default viewer a simpler way to
|
||||
// opt-out of having the `Preferences` override existing `AppOptions`.
|
||||
return true;
|
||||
}
|
||||
if (this._hasInvokedSet) {
|
||||
console.warn(
|
||||
"The Preferences may override manually set AppOptions; " +
|
||||
'please use the "disablePreferences"-option to prevent that.'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
throw new Error("Cannot initialize AppOptions.");
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
throw new Error("Cannot initialize AppOptions.");
|
||||
}
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
const userOption = userOptions[name];
|
||||
if (userOption !== undefined) {
|
||||
return userOption;
|
||||
}
|
||||
const defaultOption = defaultOptions[name];
|
||||
if (defaultOption !== undefined) {
|
||||
return compatibilityParams[name] ?? defaultOption.value;
|
||||
}
|
||||
return undefined;
|
||||
return this.#opts.get(name);
|
||||
}
|
||||
|
||||
static getAll(kind = null) {
|
||||
static getAll(kind = null, defaultOnly = false) {
|
||||
const options = Object.create(null);
|
||||
for (const name in defaultOptions) {
|
||||
const defaultOption = defaultOptions[name];
|
||||
if (kind) {
|
||||
if ((kind & defaultOption.kind) === 0) {
|
||||
continue;
|
||||
}
|
||||
if (kind === OptionKind.PREFERENCE) {
|
||||
const value = defaultOption.value,
|
||||
valueType = typeof value;
|
||||
const defaultOpt = defaultOptions[name];
|
||||
|
||||
if (
|
||||
valueType === "boolean" ||
|
||||
valueType === "string" ||
|
||||
(valueType === "number" && Number.isInteger(value))
|
||||
) {
|
||||
options[name] = value;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Invalid type for preference: ${name}`);
|
||||
}
|
||||
if (kind && !(kind & defaultOpt.kind)) {
|
||||
continue;
|
||||
}
|
||||
const userOption = userOptions[name];
|
||||
options[name] =
|
||||
userOption !== undefined
|
||||
? userOption
|
||||
: compatibilityParams[name] ?? defaultOption.value;
|
||||
options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
static set(name, value) {
|
||||
userOptions[name] = value;
|
||||
this.setAll({ [name]: value });
|
||||
}
|
||||
|
||||
static setAll(options) {
|
||||
static setAll(options, prefs = false) {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this._hasInvokedSet ||= true;
|
||||
}
|
||||
let events;
|
||||
|
||||
for (const name in options) {
|
||||
userOptions[name] = options[name];
|
||||
const defaultOpt = defaultOptions[name],
|
||||
userOpt = options[name];
|
||||
|
||||
if (
|
||||
!defaultOpt ||
|
||||
!(
|
||||
typeof userOpt === typeof defaultOpt.value ||
|
||||
Type[(typeof userOpt).toUpperCase()] & defaultOpt.type
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const { kind } = defaultOpt;
|
||||
|
||||
if (
|
||||
prefs &&
|
||||
!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) {
|
||||
(events ||= new Map()).set(name, userOpt);
|
||||
}
|
||||
this.#opts.set(name, userOpt);
|
||||
}
|
||||
|
||||
if (events) {
|
||||
for (const [name, value] of events) {
|
||||
this.eventBus.dispatch(name.toLowerCase(), { source: this, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static remove(name) {
|
||||
delete userOptions[name];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
AppOptions._hasUserOptions = function () {
|
||||
return Object.keys(userOptions).length > 0;
|
||||
};
|
||||
}
|
||||
|
||||
export { AppOptions, compatibilityParams, OptionKind };
|
||||
export { AppOptions, OptionKind };
|
||||
|
|
|
@ -20,11 +20,15 @@ const TREEITEM_SELECTED_CLASS = "selected";
|
|||
|
||||
class BaseTreeViewer {
|
||||
constructor(options) {
|
||||
if (this.constructor === BaseTreeViewer) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BaseTreeViewer
|
||||
) {
|
||||
throw new Error("Cannot initialize BaseTreeViewer.");
|
||||
}
|
||||
this.container = options.container;
|
||||
this.eventBus = options.eventBus;
|
||||
this._l10n = options.l10n;
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
@ -42,14 +46,14 @@ class BaseTreeViewer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_dispatchEvent(count) {
|
||||
throw new Error("Not implemented: _dispatchEvent");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_bindLink(element, params) {
|
||||
throw new Error("Not implemented: _bindLink");
|
||||
|
@ -70,7 +74,9 @@ class BaseTreeViewer {
|
|||
/**
|
||||
* Prepend a button before a tree item which allows the user to collapse or
|
||||
* expand all tree items at that level; see `_toggleTreeItem`.
|
||||
* @private
|
||||
* @param {HTMLDivElement} div
|
||||
* @param {boolean|object} [hidden]
|
||||
* @protected
|
||||
*/
|
||||
_addToggleButton(div, hidden = false) {
|
||||
const toggler = document.createElement("div");
|
||||
|
@ -101,10 +107,14 @@ class BaseTreeViewer {
|
|||
* @private
|
||||
*/
|
||||
_toggleTreeItem(root, show = false) {
|
||||
// Pause translation when collapsing/expanding the subtree.
|
||||
this._l10n.pause();
|
||||
|
||||
this._lastToggleIsShow = show;
|
||||
for (const toggler of root.querySelectorAll(".treeItemToggler")) {
|
||||
toggler.classList.toggle("treeItemsHidden", !show);
|
||||
}
|
||||
this._l10n.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,7 +134,10 @@ class BaseTreeViewer {
|
|||
|
||||
this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden");
|
||||
}
|
||||
// Pause translation when inserting the tree into the DOM.
|
||||
this._l10n.pause();
|
||||
this.container.append(fragment);
|
||||
this._l10n.resume();
|
||||
|
||||
this._dispatchEvent(count);
|
||||
}
|
||||
|
@ -155,6 +168,8 @@ class BaseTreeViewer {
|
|||
if (!treeItem) {
|
||||
return;
|
||||
}
|
||||
// Pause translation when expanding the treeItem.
|
||||
this._l10n.pause();
|
||||
// Ensure that the treeItem is *fully* expanded, such that it will first of
|
||||
// all be visible and secondly that scrolling it into view works correctly.
|
||||
let currentNode = treeItem.parentNode;
|
||||
|
@ -165,6 +180,8 @@ class BaseTreeViewer {
|
|||
}
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
this._l10n.resume();
|
||||
|
||||
this._updateCurrentTreeItem(treeItem);
|
||||
|
||||
this.container.scrollTo(
|
||||
|
|
365
app/src/asset/pdf/caret_browsing.js
Normal file
365
app/src/asset/pdf/caret_browsing.js
Normal file
|
@ -0,0 +1,365 @@
|
|||
/* Copyright 2024 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Used to compare floats: there is no exact equality due to rounding errors.
|
||||
const PRECISION = 1e-1;
|
||||
|
||||
class CaretBrowsingMode {
|
||||
#mainContainer;
|
||||
|
||||
#toolBarHeight = 0;
|
||||
|
||||
#viewerContainer;
|
||||
|
||||
constructor(abortSignal, mainContainer, viewerContainer, toolbarContainer) {
|
||||
this.#mainContainer = mainContainer;
|
||||
this.#viewerContainer = viewerContainer;
|
||||
|
||||
if (!toolbarContainer) {
|
||||
return;
|
||||
}
|
||||
this.#toolBarHeight = toolbarContainer.getBoundingClientRect().height;
|
||||
|
||||
const toolbarObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === toolbarContainer) {
|
||||
this.#toolBarHeight = Math.floor(entry.borderBoxSize[0].blockSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
toolbarObserver.observe(toolbarContainer);
|
||||
|
||||
abortSignal.addEventListener("abort", () => toolbarObserver.disconnect(), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the two rectangles are on the same line.
|
||||
* @param {DOMRect} rect1
|
||||
* @param {DOMRect} rect2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isOnSameLine(rect1, rect2) {
|
||||
const top1 = rect1.y;
|
||||
const bot1 = rect1.bottom;
|
||||
const mid1 = rect1.y + rect1.height / 2;
|
||||
|
||||
const top2 = rect2.y;
|
||||
const bot2 = rect2.bottom;
|
||||
const mid2 = rect2.y + rect2.height / 2;
|
||||
|
||||
return (top1 <= mid2 && mid2 <= bot1) || (top2 <= mid1 && mid1 <= bot2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if the rectangle is:
|
||||
* - under the caret when `isUp === false`.
|
||||
* - over the caret when `isUp === true`.
|
||||
* @param {DOMRect} rect
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {boolean} isUp
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isUnderOver(rect, x, y, isUp) {
|
||||
const midY = rect.y + rect.height / 2;
|
||||
return (
|
||||
(isUp ? y >= midY : y <= midY) &&
|
||||
rect.x - PRECISION <= x &&
|
||||
x <= rect.right + PRECISION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the rectangle is visible.
|
||||
* @param {DOMRect} rect
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isVisible(rect) {
|
||||
return (
|
||||
rect.top >= this.#toolBarHeight &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of the caret.
|
||||
* @param {Selection} selection
|
||||
* @param {boolean} isUp
|
||||
* @returns {Array<number>}
|
||||
*/
|
||||
#getCaretPosition(selection, isUp) {
|
||||
const { focusNode, focusOffset } = selection;
|
||||
const range = document.createRange();
|
||||
range.setStart(focusNode, focusOffset);
|
||||
range.setEnd(focusNode, focusOffset);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
return [rect.x, isUp ? rect.top : rect.bottom];
|
||||
}
|
||||
|
||||
static #caretPositionFromPoint(x, y) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
|
||||
!document.caretPositionFromPoint
|
||||
) {
|
||||
const { startContainer: offsetNode, startOffset: offset } =
|
||||
document.caretRangeFromPoint(x, y);
|
||||
return { offsetNode, offset };
|
||||
}
|
||||
return document.caretPositionFromPoint(x, y);
|
||||
}
|
||||
|
||||
#setCaretPositionHelper(selection, caretX, select, element, rect) {
|
||||
rect ||= element.getBoundingClientRect();
|
||||
if (caretX <= rect.x + PRECISION) {
|
||||
if (select) {
|
||||
selection.extend(element.firstChild, 0);
|
||||
} else {
|
||||
selection.setPosition(element.firstChild, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rect.right - PRECISION <= caretX) {
|
||||
const { lastChild } = element;
|
||||
if (select) {
|
||||
selection.extend(lastChild, lastChild.length);
|
||||
} else {
|
||||
selection.setPosition(lastChild, lastChild.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const midY = rect.y + rect.height / 2;
|
||||
let caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY);
|
||||
let parentElement = caretPosition.offsetNode?.parentElement;
|
||||
if (parentElement && parentElement !== element) {
|
||||
// There is an element on top of the one in the text layer, so we
|
||||
// need to hide all the elements (except the one in the text layer)
|
||||
// at this position in order to get the correct caret position.
|
||||
const elementsAtPoint = document.elementsFromPoint(caretX, midY);
|
||||
const savedVisibilities = [];
|
||||
for (const el of elementsAtPoint) {
|
||||
if (el === element) {
|
||||
break;
|
||||
}
|
||||
const { style } = el;
|
||||
savedVisibilities.push([el, style.visibility]);
|
||||
style.visibility = "hidden";
|
||||
}
|
||||
caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY);
|
||||
parentElement = caretPosition.offsetNode?.parentElement;
|
||||
for (const [el, visibility] of savedVisibilities) {
|
||||
el.style.visibility = visibility;
|
||||
}
|
||||
}
|
||||
if (parentElement !== element) {
|
||||
// The element targeted by caretPositionFromPoint isn't in the text
|
||||
// layer.
|
||||
if (select) {
|
||||
selection.extend(element.firstChild, 0);
|
||||
} else {
|
||||
selection.setPosition(element.firstChild, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (select) {
|
||||
selection.extend(caretPosition.offsetNode, caretPosition.offset);
|
||||
} else {
|
||||
selection.setPosition(caretPosition.offsetNode, caretPosition.offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the caret position or extend the selection (it depends on the select
|
||||
* parameter).
|
||||
* @param {boolean} select
|
||||
* @param {Selection} selection
|
||||
* @param {Element} newLineElement
|
||||
* @param {DOMRect} newLineElementRect
|
||||
* @param {number} caretX
|
||||
*/
|
||||
#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
) {
|
||||
if (this.#isVisible(newLineElementRect)) {
|
||||
this.#setCaretPositionHelper(
|
||||
selection,
|
||||
caretX,
|
||||
select,
|
||||
newLineElement,
|
||||
newLineElementRect
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#mainContainer.addEventListener(
|
||||
"scrollend",
|
||||
this.#setCaretPositionHelper.bind(
|
||||
this,
|
||||
selection,
|
||||
caretX,
|
||||
select,
|
||||
newLineElement,
|
||||
null
|
||||
),
|
||||
{ once: true }
|
||||
);
|
||||
newLineElement.scrollIntoView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node on the next page.
|
||||
* @param {Element} textLayer
|
||||
* @param {boolean} isUp
|
||||
* @returns {Node}
|
||||
*/
|
||||
#getNodeOnNextPage(textLayer, isUp) {
|
||||
while (true) {
|
||||
const page = textLayer.closest(".page");
|
||||
const pageNumber = parseInt(page.getAttribute("data-page-number"));
|
||||
const nextPage = isUp ? pageNumber - 1 : pageNumber + 1;
|
||||
textLayer = this.#viewerContainer.querySelector(
|
||||
`.page[data-page-number="${nextPage}"] .textLayer`
|
||||
);
|
||||
if (!textLayer) {
|
||||
return null;
|
||||
}
|
||||
const walker = document.createTreeWalker(textLayer, NodeFilter.SHOW_TEXT);
|
||||
const node = isUp ? walker.lastChild() : walker.firstChild();
|
||||
if (node) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the caret in the given direction.
|
||||
* @param {boolean} isUp
|
||||
* @param {boolean} select
|
||||
*/
|
||||
moveCaret(isUp, select) {
|
||||
const selection = document.getSelection();
|
||||
if (selection.rangeCount === 0) {
|
||||
return;
|
||||
}
|
||||
const { focusNode } = selection;
|
||||
const focusElement =
|
||||
focusNode.nodeType !== Node.ELEMENT_NODE
|
||||
? focusNode.parentElement
|
||||
: focusNode;
|
||||
const root = focusElement.closest(".textLayer");
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
walker.currentNode = focusNode;
|
||||
|
||||
// Move to the next element which is not on the same line as the focus
|
||||
// element.
|
||||
const focusRect = focusElement.getBoundingClientRect();
|
||||
let newLineElement = null;
|
||||
const nodeIterator = (
|
||||
isUp ? walker.previousSibling : walker.nextSibling
|
||||
).bind(walker);
|
||||
while (nodeIterator()) {
|
||||
const element = walker.currentNode.parentElement;
|
||||
if (!this.#isOnSameLine(focusRect, element.getBoundingClientRect())) {
|
||||
newLineElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!newLineElement) {
|
||||
// Need to find the next line on the next page.
|
||||
const node = this.#getNodeOnNextPage(root, isUp);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (select) {
|
||||
const lastNode =
|
||||
(isUp ? walker.firstChild() : walker.lastChild()) || focusNode;
|
||||
selection.extend(lastNode, isUp ? 0 : lastNode.length);
|
||||
const range = document.createRange();
|
||||
range.setStart(node, isUp ? node.length : 0);
|
||||
range.setEnd(node, isUp ? node.length : 0);
|
||||
selection.addRange(range);
|
||||
return;
|
||||
}
|
||||
const [caretX] = this.#getCaretPosition(selection, isUp);
|
||||
const { parentElement } = node;
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
parentElement,
|
||||
parentElement.getBoundingClientRect(),
|
||||
caretX
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We've a candidate for the next line now we want to find the first element
|
||||
// which is under/over the caret.
|
||||
const [caretX, caretY] = this.#getCaretPosition(selection, isUp);
|
||||
const newLineElementRect = newLineElement.getBoundingClientRect();
|
||||
|
||||
// Maybe the element on the new line is a valid candidate.
|
||||
if (this.#isUnderOver(newLineElementRect, caretX, caretY, isUp)) {
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
while (nodeIterator()) {
|
||||
// Search an element on the same line as newLineElement
|
||||
// which could be under/over the caret.
|
||||
const element = walker.currentNode.parentElement;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (!this.#isOnSameLine(newLineElementRect, elementRect)) {
|
||||
break;
|
||||
}
|
||||
if (this.#isUnderOver(elementRect, caretX, caretY, isUp)) {
|
||||
// We found the element.
|
||||
this.#setCaretPosition(select, selection, element, elementRect, caretX);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No element has been found so just put the caret on the element on the new
|
||||
// line.
|
||||
this.#setCaretPosition(
|
||||
select,
|
||||
selection,
|
||||
newLineElement,
|
||||
newLineElementRect,
|
||||
caretX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CaretBrowsingMode };
|
125
app/src/asset/pdf/download_manager.js
Normal file
125
app/src/asset/pdf/download_manager.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/* Copyright 2013 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
|
||||
|
||||
import { createValidAbsoluteUrl, isPdfFile } from "./pdfjs";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
|
||||
throw new Error(
|
||||
'Module "pdfjs-web/download_manager" shall not be used ' +
|
||||
"outside CHROME and GENERIC builds."
|
||||
);
|
||||
}
|
||||
|
||||
function download(blobUrl, filename) {
|
||||
const a = document.createElement("a");
|
||||
if (!a.click) {
|
||||
throw new Error('DownloadManager: "a.click()" is not supported.');
|
||||
}
|
||||
a.href = blobUrl;
|
||||
a.target = "_parent";
|
||||
// Use a.download if available. This increases the likelihood that
|
||||
// the file is downloaded instead of opened by another PDF plugin.
|
||||
if ("download" in a) {
|
||||
a.download = filename;
|
||||
}
|
||||
// <a> must be in the document for recent Firefox versions,
|
||||
// otherwise .click() is ignored.
|
||||
(document.body || document.documentElement).append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {IDownloadManager}
|
||||
*/
|
||||
class DownloadManager {
|
||||
#openBlobUrls = new WeakMap();
|
||||
|
||||
downloadData(data, filename, contentType) {
|
||||
const blobUrl = URL.createObjectURL(
|
||||
new Blob([data], { type: contentType })
|
||||
);
|
||||
download(blobUrl, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Indicating if the data was opened.
|
||||
*/
|
||||
openOrDownloadData(data, filename, dest = null) {
|
||||
const isPdfData = isPdfFile(filename);
|
||||
const contentType = isPdfData ? "application/pdf" : "";
|
||||
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")) &&
|
||||
isPdfData
|
||||
) {
|
||||
let blobUrl = this.#openBlobUrls.get(data);
|
||||
if (!blobUrl) {
|
||||
blobUrl = URL.createObjectURL(new Blob([data], { type: contentType }));
|
||||
this.#openBlobUrls.set(data, blobUrl);
|
||||
}
|
||||
let viewerUrl;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// The current URL is the viewer, let's use it and append the file.
|
||||
viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename);
|
||||
} else if (PDFJSDev.test("CHROME")) {
|
||||
// In the Chrome extension, the URL is rewritten using the history API
|
||||
// in viewer.js, so an absolute URL must be generated.
|
||||
viewerUrl =
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.runtime.getURL("/content/web/viewer.html") +
|
||||
"?file=" +
|
||||
encodeURIComponent(blobUrl + "#" + filename);
|
||||
}
|
||||
if (dest) {
|
||||
viewerUrl += `#${escape(dest)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
window.open(viewerUrl);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
console.error(`openOrDownloadData: ${ex}`);
|
||||
// Release the `blobUrl`, since opening it failed, and fallback to
|
||||
// downloading the PDF file.
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
this.#openBlobUrls.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadData(data, filename, contentType);
|
||||
return false;
|
||||
}
|
||||
|
||||
download(data, url, filename) {
|
||||
let blobUrl;
|
||||
if (data) {
|
||||
blobUrl = URL.createObjectURL(
|
||||
new Blob([data], { type: "application/pdf" })
|
||||
);
|
||||
} else {
|
||||
if (!createValidAbsoluteUrl(url, "http://example.com")) {
|
||||
console.error(`download - not a valid URL: ${url}`);
|
||||
return;
|
||||
}
|
||||
blobUrl = url + "#pdfjs.action=download";
|
||||
}
|
||||
download(blobUrl, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export { DownloadManager };
|
64
app/src/asset/pdf/draw_layer_builder.js
Normal file
64
app/src/asset/pdf/draw_layer_builder.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/* Copyright 2022 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DrawLayer } from "./pdfjs";
|
||||
|
||||
/**
|
||||
* @typedef {Object} DrawLayerBuilderOptions
|
||||
* @property {number} pageIndex
|
||||
*/
|
||||
|
||||
class DrawLayerBuilder {
|
||||
#drawLayer = null;
|
||||
|
||||
/**
|
||||
* @param {DrawLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pageIndex = options.pageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} intent (default value is 'display')
|
||||
*/
|
||||
async render(intent = "display") {
|
||||
if (intent !== "display" || this.#drawLayer || this._cancelled) {
|
||||
return;
|
||||
}
|
||||
this.#drawLayer = new DrawLayer({
|
||||
pageIndex: this.pageIndex,
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._cancelled = true;
|
||||
|
||||
if (!this.#drawLayer) {
|
||||
return;
|
||||
}
|
||||
this.#drawLayer.destroy();
|
||||
this.#drawLayer = null;
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
this.#drawLayer?.setParent(parent);
|
||||
}
|
||||
|
||||
getDrawLayer() {
|
||||
return this.#drawLayer;
|
||||
}
|
||||
}
|
||||
|
||||
export { DrawLayerBuilder };
|
|
@ -35,39 +35,32 @@ const WaitOnType = {
|
|||
* @param {WaitOnEventOrTimeoutParameters}
|
||||
* @returns {Promise} A promise that is resolved with a {WaitOnType} value.
|
||||
*/
|
||||
function waitOnEventOrTimeout({ target, name, delay = 0 }) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (
|
||||
typeof target !== "object" ||
|
||||
!(name && typeof name === "string") ||
|
||||
!(Number.isInteger(delay) && delay >= 0)
|
||||
) {
|
||||
throw new Error("waitOnEventOrTimeout - invalid parameters.");
|
||||
}
|
||||
async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
|
||||
if (
|
||||
typeof target !== "object" ||
|
||||
!(name && typeof name === "string") ||
|
||||
!(Number.isInteger(delay) && delay >= 0)
|
||||
) {
|
||||
throw new Error("waitOnEventOrTimeout - invalid parameters.");
|
||||
}
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
const ac = new AbortController();
|
||||
|
||||
function handler(type) {
|
||||
if (target instanceof EventBus) {
|
||||
target._off(name, eventHandler);
|
||||
} else {
|
||||
target.removeEventListener(name, eventHandler);
|
||||
}
|
||||
function handler(type) {
|
||||
ac.abort(); // Remove event listener.
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
resolve(type);
|
||||
}
|
||||
resolve(type);
|
||||
}
|
||||
|
||||
const eventHandler = handler.bind(null, WaitOnType.EVENT);
|
||||
if (target instanceof EventBus) {
|
||||
target._on(name, eventHandler);
|
||||
} else {
|
||||
target.addEventListener(name, eventHandler);
|
||||
}
|
||||
|
||||
const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
|
||||
const timeout = setTimeout(timeoutHandler, delay);
|
||||
const evtMethod = target instanceof EventBus ? "_on" : "addEventListener";
|
||||
target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), {
|
||||
signal: ac.signal,
|
||||
});
|
||||
|
||||
const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,6 +79,7 @@ class EventBus {
|
|||
this._on(eventName, listener, {
|
||||
external: true,
|
||||
once: options?.once,
|
||||
signal: options?.signal,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -95,10 +89,7 @@ class EventBus {
|
|||
* @param {Object} [options]
|
||||
*/
|
||||
off(eventName, listener, options = null) {
|
||||
this._off(eventName, listener, {
|
||||
external: true,
|
||||
once: options?.once,
|
||||
});
|
||||
this._off(eventName, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,11 +128,25 @@ class EventBus {
|
|||
* @ignore
|
||||
*/
|
||||
_on(eventName, listener, options = null) {
|
||||
let rmAbort = null;
|
||||
if (options?.signal instanceof AbortSignal) {
|
||||
const { signal } = options;
|
||||
if (signal.aborted) {
|
||||
console.error("Cannot use an `aborted` signal.");
|
||||
return;
|
||||
}
|
||||
const onAbort = () => this._off(eventName, listener);
|
||||
rmAbort = () => signal.removeEventListener("abort", onAbort);
|
||||
|
||||
signal.addEventListener("abort", onAbort);
|
||||
}
|
||||
|
||||
const eventListeners = (this.#listeners[eventName] ||= []);
|
||||
eventListeners.push({
|
||||
listener,
|
||||
external: options?.external === true,
|
||||
once: options?.once === true,
|
||||
rmAbort,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -154,7 +159,9 @@ class EventBus {
|
|||
return;
|
||||
}
|
||||
for (let i = 0, ii = eventListeners.length; i < ii; i++) {
|
||||
if (eventListeners[i].listener === listener) {
|
||||
const evt = eventListeners[i];
|
||||
if (evt.listener === listener) {
|
||||
evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed.
|
||||
eventListeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
|
@ -163,32 +170,57 @@ class EventBus {
|
|||
}
|
||||
|
||||
/**
|
||||
* NOTE: Only used to support various PDF viewer tests in `mozilla-central`.
|
||||
* NOTE: Only used in the Firefox build-in pdf viewer.
|
||||
*/
|
||||
class AutomationEventBus extends EventBus {
|
||||
class FirefoxEventBus extends EventBus {
|
||||
#externalServices;
|
||||
|
||||
#globalEventNames;
|
||||
|
||||
#isInAutomation;
|
||||
|
||||
constructor(globalEventNames, externalServices, isInAutomation) {
|
||||
super();
|
||||
this.#globalEventNames = globalEventNames;
|
||||
this.#externalServices = externalServices;
|
||||
this.#isInAutomation = isInAutomation;
|
||||
}
|
||||
|
||||
dispatch(eventName, data) {
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) {
|
||||
throw new Error("Not implemented: AutomationEventBus.dispatch");
|
||||
throw new Error("Not implemented: FirefoxEventBus.dispatch");
|
||||
}
|
||||
super.dispatch(eventName, data);
|
||||
|
||||
const details = Object.create(null);
|
||||
if (data) {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (key === "source") {
|
||||
if (value === window || value === document) {
|
||||
return; // No need to re-dispatch (already) global events.
|
||||
if (this.#isInAutomation) {
|
||||
const detail = Object.create(null);
|
||||
if (data) {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (key === "source") {
|
||||
if (value === window || value === document) {
|
||||
return; // No need to re-dispatch (already) global events.
|
||||
}
|
||||
continue; // Ignore the `source` property.
|
||||
}
|
||||
continue; // Ignore the `source` property.
|
||||
detail[key] = value;
|
||||
}
|
||||
details[key] = value;
|
||||
}
|
||||
const event = new CustomEvent(eventName, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (this.#globalEventNames?.has(eventName)) {
|
||||
this.#externalServices.dispatchGlobalEvent({
|
||||
eventName,
|
||||
detail: data,
|
||||
});
|
||||
}
|
||||
const event = document.createEvent("CustomEvent");
|
||||
event.initCustomEvent(eventName, true, true, details);
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
export { AutomationEventBus, EventBus, waitOnEventOrTimeout, WaitOnType };
|
||||
export { EventBus, FirefoxEventBus, waitOnEventOrTimeout, WaitOnType };
|
||||
|
|
54
app/src/asset/pdf/external_services.js
Normal file
54
app/src/asset/pdf/external_services.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/* Copyright 2024 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces.js").IL10n} IL10n */
|
||||
|
||||
class BaseExternalServices {
|
||||
constructor() {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BaseExternalServices
|
||||
) {
|
||||
throw new Error("Cannot initialize BaseExternalServices.");
|
||||
}
|
||||
}
|
||||
|
||||
updateFindControlState(data) {}
|
||||
|
||||
updateFindMatchesCount(data) {}
|
||||
|
||||
initPassiveLoading() {}
|
||||
|
||||
reportTelemetry(data) {}
|
||||
|
||||
/**
|
||||
* @returns {Promise<IL10n>}
|
||||
*/
|
||||
async createL10n() {
|
||||
throw new Error("Not implemented: createL10n");
|
||||
}
|
||||
|
||||
createScripting() {
|
||||
throw new Error("Not implemented: createScripting");
|
||||
}
|
||||
|
||||
updateEditorStates(data) {
|
||||
throw new Error("Not implemented: updateEditorStates");
|
||||
}
|
||||
|
||||
dispatchGlobalEvent(_event) {}
|
||||
}
|
||||
|
||||
export { BaseExternalServices };
|
|
@ -13,11 +13,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getPdfFilenameFromUrl, loadScript } from "./pdfjs";
|
||||
import { getPdfFilenameFromUrl } from "./pdfjs";
|
||||
|
||||
async function docPropertiesLookup(pdfDocument) {
|
||||
async function docProperties(pdfDocument) {
|
||||
const url = "",
|
||||
baseUrl = url.split("#")[0];
|
||||
baseUrl = url.split("#", 1)[0];
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { info, metadata, contentDispositionFilename, contentLength } =
|
||||
await pdfDocument.getMetadata();
|
||||
|
@ -41,11 +41,17 @@ async function docPropertiesLookup(pdfDocument) {
|
|||
|
||||
class GenericScripting {
|
||||
constructor(sandboxBundleSrc) {
|
||||
this._ready = loadScript(
|
||||
sandboxBundleSrc,
|
||||
/* removeScriptElement = */ true
|
||||
).then(() => {
|
||||
return window.pdfjsSandbox.QuickJSSandbox();
|
||||
this._ready = new Promise((resolve, reject) => {
|
||||
// NOTE
|
||||
// const sandbox =
|
||||
// typeof PDFJSDev === "undefined"
|
||||
// ? import(sandboxBundleSrc) // eslint-disable-line no-unsanitized/method
|
||||
// : __non_webpack_import__(sandboxBundleSrc);
|
||||
// sandbox
|
||||
// .then(pdfjsSandbox => {
|
||||
// resolve(pdfjsSandbox.QuickJSSandbox());
|
||||
// })
|
||||
// .catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,4 +71,4 @@ class GenericScripting {
|
|||
}
|
||||
}
|
||||
|
||||
export { docPropertiesLookup, GenericScripting };
|
||||
export { docProperties, GenericScripting };
|
||||
|
|
|
@ -13,9 +13,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { BaseExternalServices } from "./external_services.js";
|
||||
import { BasePreferences } from "./preferences.js";
|
||||
import { GenericL10n } from "./genericl10n.js";
|
||||
import { GenericScripting } from "./generic_scripting.js";
|
||||
import {shadow} from "./pdfjs";
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
|
||||
throw new Error(
|
||||
|
@ -23,76 +25,121 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
|
|||
);
|
||||
}
|
||||
|
||||
const GenericCom = {};
|
||||
function initCom(app) {}
|
||||
|
||||
class GenericPreferences extends BasePreferences {
|
||||
class Preferences extends BasePreferences {
|
||||
async _writeToStorage(prefObj) {
|
||||
localStorage.setItem("pdfjs.preferences", JSON.stringify(prefObj));
|
||||
}
|
||||
|
||||
async _readFromStorage(prefObj) {
|
||||
return JSON.parse(localStorage.getItem("pdfjs.preferences"));
|
||||
return { prefs: JSON.parse(localStorage.getItem("pdfjs.preferences")) };
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultExternalServices {
|
||||
constructor() {
|
||||
throw new Error("Cannot initialize DefaultExternalServices.");
|
||||
class ExternalServices extends BaseExternalServices {
|
||||
async createL10n() {
|
||||
return new GenericL10n(AppOptions.get("localeProperties")?.lang);
|
||||
}
|
||||
|
||||
static updateFindControlState(data) {}
|
||||
createScripting() {
|
||||
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
||||
}
|
||||
}
|
||||
|
||||
static updateFindMatchesCount(data) {}
|
||||
|
||||
static initPassiveLoading(callbacks) {}
|
||||
|
||||
static reportTelemetry(data) {}
|
||||
|
||||
static get supportsPinchToZoom() {
|
||||
return shadow(this, "supportsPinchToZoom", true);
|
||||
class MLManager {
|
||||
async isEnabledFor(_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static get supportsIntegratedFind() {
|
||||
return shadow(this, "supportsIntegratedFind", false);
|
||||
async deleteModel(_service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
static get supportsDocumentFonts() {
|
||||
return shadow(this, "supportsDocumentFonts", true);
|
||||
isReady(_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static get supportedMouseWheelZoomModifierKeys() {
|
||||
return shadow(this, "supportedMouseWheelZoomModifierKeys", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
guess(_data) {}
|
||||
|
||||
toggleService(_name, _enabled) {}
|
||||
|
||||
static getFakeMLManager(options) {
|
||||
return new FakeMLManager(options);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMLManager {
|
||||
eventBus = null;
|
||||
|
||||
hasProgress = false;
|
||||
|
||||
constructor({ enableGuessAltText, enableAltTextModelDownload }) {
|
||||
this.enableGuessAltText = enableGuessAltText;
|
||||
this.enableAltTextModelDownload = enableAltTextModelDownload;
|
||||
}
|
||||
|
||||
setEventBus(eventBus, abortSignal) {
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
async isEnabledFor(_name) {
|
||||
return this.enableGuessAltText;
|
||||
}
|
||||
|
||||
async deleteModel(_name) {
|
||||
this.enableAltTextModelDownload = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
async loadModel(_name) {}
|
||||
|
||||
async downloadModel(_name) {
|
||||
// Simulate downloading the model but with progress.
|
||||
// The progress can be seen in the new alt-text dialog.
|
||||
this.hasProgress = true;
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
const total = 1e8;
|
||||
const end = 1.5 * total;
|
||||
const increment = 5e6;
|
||||
let loaded = 0;
|
||||
const id = setInterval(() => {
|
||||
loaded += increment;
|
||||
if (loaded <= end) {
|
||||
this.eventBus.dispatch("loadaiengineprogress", {
|
||||
source: this,
|
||||
detail: {
|
||||
total,
|
||||
totalLoaded: loaded,
|
||||
finished: loaded + increment >= end,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
clearInterval(id);
|
||||
this.hasProgress = false;
|
||||
this.enableAltTextModelDownload = true;
|
||||
resolve(true);
|
||||
}, 900);
|
||||
return promise;
|
||||
}
|
||||
|
||||
isReady(_name) {
|
||||
return this.enableAltTextModelDownload;
|
||||
}
|
||||
|
||||
guess({ request: { data } }) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(data ? { output: "Fake alt text" } : { error: true });
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
static get isInAutomation() {
|
||||
return shadow(this, "isInAutomation", false);
|
||||
}
|
||||
|
||||
static updateEditorStates(data) {
|
||||
throw new Error("Not implemented: updateEditorStates");
|
||||
}
|
||||
|
||||
static createDownloadManager() {
|
||||
// NOTE return new DownloadManager();
|
||||
}
|
||||
|
||||
static createPreferences() {
|
||||
return new GenericPreferences();
|
||||
}
|
||||
|
||||
static createL10n({ locale = "en-US" }) {
|
||||
// NOTE return new GenericL10n(locale);
|
||||
}
|
||||
|
||||
static createScripting({ sandboxBundleSrc }) {
|
||||
return new GenericScripting(sandboxBundleSrc);
|
||||
toggleService(_name, enabled) {
|
||||
this.enableGuessAltText = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE
|
||||
// PDFViewerApplication.externalServices = GenericExternalServices;
|
||||
|
||||
export { GenericCom, DefaultExternalServices };
|
||||
export { ExternalServices, initCom, MLManager, Preferences };
|
||||
|
|
56
app/src/asset/pdf/genericl10n.js
Normal file
56
app/src/asset/pdf/genericl10n.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/* Copyright 2017 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
|
||||
// NOTE
|
||||
|
||||
import {L10n} from "./l10n.js";
|
||||
|
||||
/**
|
||||
* @implements {IL10n}
|
||||
*/
|
||||
class GenericL10n extends L10n {
|
||||
constructor(lang) {
|
||||
super({lang});
|
||||
this._setL10n({
|
||||
formatMessages: (msg) => {
|
||||
return new Promise(resolve => {
|
||||
let lang = window.siyuan.languages[msg[0].id] ||msg[0].id
|
||||
if (msg[0].args) {
|
||||
Object.keys(msg[0].args).forEach(key => {
|
||||
lang = lang.replace('${' + key + '}', msg[0].args[key]);
|
||||
});
|
||||
}
|
||||
resolve([{value: lang}]);
|
||||
});
|
||||
},
|
||||
connectRoot: () => {
|
||||
},
|
||||
translateRoots: () => {
|
||||
},
|
||||
translateElements: () => {
|
||||
},
|
||||
disconnectRoot: () => {
|
||||
},
|
||||
pauseObserving: () => {
|
||||
},
|
||||
resumeObserving: () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {GenericL10n};
|
|
@ -17,22 +17,19 @@
|
|||
// Class name of element which can be grabbed.
|
||||
const CSS_CLASS_GRAB = "grab-to-pan-grab";
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrabToPanOptions
|
||||
* @property {HTMLElement} element
|
||||
*/
|
||||
|
||||
class GrabToPan {
|
||||
/**
|
||||
* Construct a GrabToPan instance for a given HTML element.
|
||||
* @param {Element} options.element
|
||||
* @param {function} [options.ignoreTarget] - See `ignoreTarget(node)`.
|
||||
* @param {function(boolean)} [options.onActiveChanged] - Called when
|
||||
* grab-to-pan is (de)activated. The first argument is a boolean that
|
||||
* shows whether grab-to-pan is activated.
|
||||
* @param {GrabToPanOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.element = options.element;
|
||||
this.document = options.element.ownerDocument;
|
||||
if (typeof options.ignoreTarget === "function") {
|
||||
this.ignoreTarget = options.ignoreTarget;
|
||||
}
|
||||
this.onActiveChanged = options.onActiveChanged;
|
||||
constructor({ element }) {
|
||||
this.element = element;
|
||||
this.document = element.ownerDocument;
|
||||
|
||||
// Bind the contexts to ensure that `this` always points to
|
||||
// the GrabToPan instance.
|
||||
|
@ -57,8 +54,6 @@ class GrabToPan {
|
|||
this.active = true;
|
||||
this.element.addEventListener("mousedown", this._onMouseDown, true);
|
||||
this.element.classList.add(CSS_CLASS_GRAB);
|
||||
|
||||
this.onActiveChanged?.(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,8 +66,6 @@ class GrabToPan {
|
|||
this.element.removeEventListener("mousedown", this._onMouseDown, true);
|
||||
this._endPan();
|
||||
this.element.classList.remove(CSS_CLASS_GRAB);
|
||||
|
||||
this.onActiveChanged?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +99,7 @@ class GrabToPan {
|
|||
try {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
event.originalTarget.tagName;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Mozilla-specific: element is a scrollbar (XUL element)
|
||||
return;
|
||||
}
|
||||
|
@ -140,18 +133,12 @@ class GrabToPan {
|
|||
}
|
||||
const xDiff = event.clientX - this.clientXStart;
|
||||
const yDiff = event.clientY - this.clientYStart;
|
||||
const scrollTop = this.scrollTopStart - yDiff;
|
||||
const scrollLeft = this.scrollLeftStart - xDiff;
|
||||
if (this.element.scrollTo) {
|
||||
this.element.scrollTo({
|
||||
top: scrollTop,
|
||||
left: scrollLeft,
|
||||
behavior: "instant",
|
||||
});
|
||||
} else {
|
||||
this.element.scrollTop = scrollTop;
|
||||
this.element.scrollLeft = scrollLeft;
|
||||
}
|
||||
this.element.scrollTo({
|
||||
top: this.scrollTopStart - yDiff,
|
||||
left: this.scrollLeftStart - xDiff,
|
||||
behavior: "instant",
|
||||
});
|
||||
|
||||
if (!this.overlay.parentNode) {
|
||||
document.body.append(this.overlay);
|
||||
}
|
||||
|
|
143
app/src/asset/pdf/l10n.js
Normal file
143
app/src/asset/pdf/l10n.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
/* Copyright 2023 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
|
||||
/**
|
||||
* NOTE: The L10n-implementations should use lowercase language-codes
|
||||
* internally.
|
||||
* @implements {IL10n}
|
||||
*/
|
||||
class L10n {
|
||||
#dir;
|
||||
|
||||
#elements = new Set();
|
||||
|
||||
#lang;
|
||||
|
||||
#l10n;
|
||||
|
||||
constructor({ lang, isRTL }, l10n = null) {
|
||||
this.#lang = L10n.#fixupLangCode(lang);
|
||||
this.#l10n = l10n;
|
||||
this.#dir = (isRTL ?? L10n.#isRTL(this.#lang)) ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
_setL10n(l10n) {
|
||||
this.#l10n = l10n;
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||
document.l10n = l10n;
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getLanguage() {
|
||||
return this.#lang;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getDirection() {
|
||||
return this.#dir;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async get(ids, args = null, fallback) {
|
||||
if (Array.isArray(ids)) {
|
||||
ids = ids.map(id => ({ id }));
|
||||
const messages = await this.#l10n.formatMessages(ids);
|
||||
return messages.map(message => message.value);
|
||||
}
|
||||
|
||||
const messages = await this.#l10n.formatMessages([
|
||||
{
|
||||
id: ids,
|
||||
args,
|
||||
},
|
||||
]);
|
||||
return messages[0]?.value || fallback;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async translate(element) {
|
||||
this.#elements.add(element);
|
||||
try {
|
||||
this.#l10n.connectRoot(element);
|
||||
await this.#l10n.translateRoots();
|
||||
} catch {
|
||||
// Element is under an existing root, so there is no need to add it again.
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async translateOnce(element) {
|
||||
try {
|
||||
await this.#l10n.translateElements([element]);
|
||||
} catch (ex) {
|
||||
console.error(`translateOnce: "${ex}".`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async destroy() {
|
||||
for (const element of this.#elements) {
|
||||
this.#l10n.disconnectRoot(element);
|
||||
}
|
||||
this.#elements.clear();
|
||||
this.#l10n.pauseObserving();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
pause() {
|
||||
this.#l10n.pauseObserving();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
resume() {
|
||||
this.#l10n.resumeObserving();
|
||||
}
|
||||
|
||||
static #fixupLangCode(langCode) {
|
||||
// Use only lowercase language-codes internally, and fallback to English.
|
||||
langCode = langCode?.toLowerCase() || "en-us";
|
||||
|
||||
// Try to support "incompletely" specified language codes (see issue 13689).
|
||||
const PARTIAL_LANG_CODES = {
|
||||
en: "en-us",
|
||||
es: "es-es",
|
||||
fy: "fy-nl",
|
||||
ga: "ga-ie",
|
||||
gu: "gu-in",
|
||||
hi: "hi-in",
|
||||
hy: "hy-am",
|
||||
nb: "nb-no",
|
||||
ne: "ne-np",
|
||||
nn: "nn-no",
|
||||
pa: "pa-in",
|
||||
pt: "pt-pt",
|
||||
sv: "sv-se",
|
||||
zh: "zh-cn",
|
||||
};
|
||||
return PARTIAL_LANG_CODES[langCode] || langCode;
|
||||
}
|
||||
|
||||
static #isRTL(lang) {
|
||||
const shortCode = lang.split("-", 1)[0];
|
||||
return ["ar", "he", "fa", "ps", "ur"].includes(shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
const GenericL10n = null;
|
||||
|
||||
export { GenericL10n, L10n };
|
696
app/src/asset/pdf/new_alt_text_manager.js
Normal file
696
app/src/asset/pdf/new_alt_text_manager.js
Normal file
|
@ -0,0 +1,696 @@
|
|||
/* Copyright 2024 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { noContextMenu } from "./pdfjs";
|
||||
|
||||
class NewAltTextManager {
|
||||
#boundCancel = this.#cancel.bind(this);
|
||||
|
||||
#createAutomaticallyButton;
|
||||
|
||||
#currentEditor = null;
|
||||
|
||||
#cancelButton;
|
||||
|
||||
#descriptionContainer;
|
||||
|
||||
#dialog;
|
||||
|
||||
#disclaimer;
|
||||
|
||||
#downloadModel;
|
||||
|
||||
#downloadModelDescription;
|
||||
|
||||
#eventBus;
|
||||
|
||||
#firstTime = false;
|
||||
|
||||
#guessedAltText;
|
||||
|
||||
#hasAI = null;
|
||||
|
||||
#isEditing = null;
|
||||
|
||||
#imagePreview;
|
||||
|
||||
#imageData;
|
||||
|
||||
#isAILoading = false;
|
||||
|
||||
#wasAILoading = false;
|
||||
|
||||
#learnMore;
|
||||
|
||||
#notNowButton;
|
||||
|
||||
#overlayManager;
|
||||
|
||||
#textarea;
|
||||
|
||||
#title;
|
||||
|
||||
#uiManager;
|
||||
|
||||
#previousAltText = null;
|
||||
|
||||
constructor(
|
||||
{
|
||||
descriptionContainer,
|
||||
dialog,
|
||||
imagePreview,
|
||||
cancelButton,
|
||||
disclaimer,
|
||||
notNowButton,
|
||||
saveButton,
|
||||
textarea,
|
||||
learnMore,
|
||||
errorCloseButton,
|
||||
createAutomaticallyButton,
|
||||
downloadModel,
|
||||
downloadModelDescription,
|
||||
title,
|
||||
},
|
||||
overlayManager,
|
||||
eventBus
|
||||
) {
|
||||
this.#cancelButton = cancelButton;
|
||||
this.#createAutomaticallyButton = createAutomaticallyButton;
|
||||
this.#descriptionContainer = descriptionContainer;
|
||||
this.#dialog = dialog;
|
||||
this.#disclaimer = disclaimer;
|
||||
this.#notNowButton = notNowButton;
|
||||
this.#imagePreview = imagePreview;
|
||||
this.#textarea = textarea;
|
||||
this.#learnMore = learnMore;
|
||||
this.#title = title;
|
||||
this.#downloadModel = downloadModel;
|
||||
this.#downloadModelDescription = downloadModelDescription;
|
||||
this.#overlayManager = overlayManager;
|
||||
this.#eventBus = eventBus;
|
||||
|
||||
dialog.addEventListener("close", this.#close.bind(this));
|
||||
dialog.addEventListener("contextmenu", event => {
|
||||
if (event.target !== this.#textarea) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
cancelButton.addEventListener("click", this.#boundCancel);
|
||||
notNowButton.addEventListener("click", this.#boundCancel);
|
||||
saveButton.addEventListener("click", this.#save.bind(this));
|
||||
errorCloseButton.addEventListener("click", () => {
|
||||
this.#toggleError(false);
|
||||
});
|
||||
createAutomaticallyButton.addEventListener("click", async () => {
|
||||
const checked =
|
||||
createAutomaticallyButton.getAttribute("aria-pressed") !== "true";
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.alt_text.ai_generation_check",
|
||||
data: { status: checked },
|
||||
});
|
||||
|
||||
if (this.#uiManager) {
|
||||
this.#uiManager.setPreference("enableGuessAltText", checked);
|
||||
await this.#uiManager.mlManager.toggleService("altText", checked);
|
||||
}
|
||||
this.#toggleGuessAltText(checked, /* isInitial = */ false);
|
||||
});
|
||||
textarea.addEventListener("focus", () => {
|
||||
this.#wasAILoading = this.#isAILoading;
|
||||
this.#toggleLoading(false);
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
});
|
||||
textarea.addEventListener("blur", () => {
|
||||
if (!textarea.value) {
|
||||
this.#toggleLoading(this.#wasAILoading);
|
||||
}
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
});
|
||||
textarea.addEventListener("input", () => {
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
});
|
||||
|
||||
eventBus._on("enableguessalttext", ({ value }) => {
|
||||
this.#toggleGuessAltText(value, /* isInitial = */ false);
|
||||
});
|
||||
|
||||
this.#overlayManager.register(dialog);
|
||||
|
||||
this.#learnMore.addEventListener("click", () => {
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.alt_text.info",
|
||||
data: { topic: "alt_text" },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#toggleLoading(value) {
|
||||
if (!this.#uiManager || this.#isAILoading === value) {
|
||||
return;
|
||||
}
|
||||
this.#isAILoading = value;
|
||||
this.#descriptionContainer.classList.toggle("loading", value);
|
||||
}
|
||||
|
||||
#toggleError(value) {
|
||||
if (!this.#uiManager) {
|
||||
return;
|
||||
}
|
||||
this.#dialog.classList.toggle("error", value);
|
||||
}
|
||||
|
||||
async #toggleGuessAltText(value, isInitial = false) {
|
||||
if (!this.#uiManager) {
|
||||
return;
|
||||
}
|
||||
this.#dialog.classList.toggle("aiDisabled", !value);
|
||||
this.#createAutomaticallyButton.setAttribute("aria-pressed", value);
|
||||
|
||||
if (value) {
|
||||
const { altTextLearnMoreUrl } = this.#uiManager.mlManager;
|
||||
if (altTextLearnMoreUrl) {
|
||||
this.#learnMore.href = altTextLearnMoreUrl;
|
||||
}
|
||||
this.#mlGuessAltText(isInitial);
|
||||
} else {
|
||||
this.#toggleLoading(false);
|
||||
this.#isAILoading = false;
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
}
|
||||
}
|
||||
|
||||
#toggleNotNow() {
|
||||
this.#notNowButton.classList.toggle("hidden", !this.#firstTime);
|
||||
this.#cancelButton.classList.toggle("hidden", this.#firstTime);
|
||||
}
|
||||
|
||||
#toggleAI(value) {
|
||||
if (!this.#uiManager || this.#hasAI === value) {
|
||||
return;
|
||||
}
|
||||
this.#hasAI = value;
|
||||
this.#dialog.classList.toggle("noAi", !value);
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
}
|
||||
|
||||
#toggleTitleAndDisclaimer() {
|
||||
// Disclaimer is visible when the AI is loading or the user didn't change
|
||||
// the guessed alt text.
|
||||
const visible =
|
||||
this.#isAILoading ||
|
||||
(this.#guessedAltText && this.#guessedAltText === this.#textarea.value);
|
||||
this.#disclaimer.hidden = !visible;
|
||||
|
||||
// The title changes depending if the text area is empty or not.
|
||||
const isEditing = this.#isAILoading || !!this.#textarea.value;
|
||||
if (this.#isEditing === isEditing) {
|
||||
return;
|
||||
}
|
||||
this.#isEditing = isEditing;
|
||||
this.#title.setAttribute(
|
||||
"data-l10n-id",
|
||||
isEditing
|
||||
? "pdfjs-editor-new-alt-text-dialog-edit-label"
|
||||
: "pdfjs-editor-new-alt-text-dialog-add-label"
|
||||
);
|
||||
}
|
||||
|
||||
async #mlGuessAltText(isInitial) {
|
||||
if (this.#isAILoading) {
|
||||
// We're still loading the previous guess.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#textarea.value) {
|
||||
// The user has already set an alt text.
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInitial && this.#previousAltText !== null) {
|
||||
// The user has already set an alt text (empty or not).
|
||||
return;
|
||||
}
|
||||
|
||||
this.#guessedAltText = this.#currentEditor.guessedAltText;
|
||||
if (this.#previousAltText === null && this.#guessedAltText) {
|
||||
// We have a guessed alt text and the user didn't change it.
|
||||
this.#addAltText(this.#guessedAltText);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#toggleLoading(true);
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
|
||||
let hasError = false;
|
||||
try {
|
||||
// When calling #mlGuessAltText we don't wait for it, so we must take care
|
||||
// that the alt text dialog can have been closed before the response is.
|
||||
|
||||
const altText = await this.#currentEditor.mlGuessAltText(
|
||||
this.#imageData,
|
||||
/* updateAltTextData = */ false
|
||||
);
|
||||
if (altText) {
|
||||
this.#guessedAltText = altText;
|
||||
this.#wasAILoading = this.#isAILoading;
|
||||
if (this.#isAILoading) {
|
||||
this.#addAltText(altText);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
this.#toggleLoading(false);
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
|
||||
if (hasError && this.#uiManager) {
|
||||
this.#toggleError(true);
|
||||
}
|
||||
}
|
||||
|
||||
#addAltText(altText) {
|
||||
if (!this.#uiManager || this.#textarea.value) {
|
||||
return;
|
||||
}
|
||||
this.#textarea.value = altText;
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
}
|
||||
|
||||
#setProgress() {
|
||||
// Show the download model progress.
|
||||
this.#downloadModel.classList.toggle("hidden", false);
|
||||
|
||||
const callback = async ({ detail: { finished, total, totalLoaded } }) => {
|
||||
const ONE_MEGA_BYTES = 1e6;
|
||||
// totalLoaded can be greater than total if the download is compressed.
|
||||
// So we cheat to avoid any confusion.
|
||||
totalLoaded = Math.min(0.99 * total, totalLoaded);
|
||||
|
||||
// Update the progress.
|
||||
const totalSize = (this.#downloadModelDescription.ariaValueMax =
|
||||
Math.round(total / ONE_MEGA_BYTES));
|
||||
const downloadedSize = (this.#downloadModelDescription.ariaValueNow =
|
||||
Math.round(totalLoaded / ONE_MEGA_BYTES));
|
||||
this.#downloadModelDescription.setAttribute(
|
||||
"data-l10n-args",
|
||||
JSON.stringify({ totalSize, downloadedSize })
|
||||
);
|
||||
if (!finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're done, remove the listener and hide the download model progress.
|
||||
this.#eventBus._off("loadaiengineprogress", callback);
|
||||
this.#downloadModel.classList.toggle("hidden", true);
|
||||
|
||||
this.#toggleAI(true);
|
||||
if (!this.#uiManager) {
|
||||
return;
|
||||
}
|
||||
const { mlManager } = this.#uiManager;
|
||||
|
||||
// The model has been downloaded, we can now enable the AI service.
|
||||
mlManager.toggleService("altText", true);
|
||||
this.#toggleGuessAltText(
|
||||
await mlManager.isEnabledFor("altText"),
|
||||
/* isInitial = */ true
|
||||
);
|
||||
};
|
||||
this.#eventBus._on("loadaiengineprogress", callback);
|
||||
}
|
||||
|
||||
async editAltText(uiManager, editor, firstTime) {
|
||||
if (this.#currentEditor || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTime && editor.hasAltTextData()) {
|
||||
editor.altTextFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
this.#firstTime = firstTime;
|
||||
let { mlManager } = uiManager;
|
||||
let hasAI = !!mlManager;
|
||||
this.#toggleTitleAndDisclaimer();
|
||||
|
||||
if (mlManager && !mlManager.isReady("altText")) {
|
||||
hasAI = false;
|
||||
if (mlManager.hasProgress) {
|
||||
this.#setProgress();
|
||||
} else {
|
||||
mlManager = null;
|
||||
}
|
||||
} else {
|
||||
this.#downloadModel.classList.toggle("hidden", true);
|
||||
}
|
||||
|
||||
const isAltTextEnabledPromise = mlManager?.isEnabledFor("altText");
|
||||
|
||||
this.#currentEditor = editor;
|
||||
this.#uiManager = uiManager;
|
||||
this.#uiManager.removeEditListeners();
|
||||
|
||||
({ altText: this.#previousAltText } = editor.altTextData);
|
||||
this.#textarea.value = this.#previousAltText ?? "";
|
||||
|
||||
// TODO: get this value from Firefox
|
||||
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184)
|
||||
const AI_MAX_IMAGE_DIMENSION = 224;
|
||||
const MAX_PREVIEW_DIMENSION = 180;
|
||||
|
||||
// The max dimension of the preview in the dialog is 180px, so we keep 224px
|
||||
// and rescale it thanks to css.
|
||||
|
||||
let canvas, width, height;
|
||||
if (mlManager) {
|
||||
({
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
imageData: this.#imageData,
|
||||
} = editor.copyCanvas(
|
||||
AI_MAX_IMAGE_DIMENSION,
|
||||
MAX_PREVIEW_DIMENSION,
|
||||
/* createImageData = */ true
|
||||
));
|
||||
if (hasAI) {
|
||||
this.#toggleGuessAltText(
|
||||
await isAltTextEnabledPromise,
|
||||
/* isInitial = */ true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
({ canvas, width, height } = editor.copyCanvas(
|
||||
AI_MAX_IMAGE_DIMENSION,
|
||||
MAX_PREVIEW_DIMENSION,
|
||||
/* createImageData = */ false
|
||||
));
|
||||
}
|
||||
|
||||
canvas.setAttribute("role", "presentation");
|
||||
const { style } = canvas;
|
||||
style.width = `${width}px`;
|
||||
style.height = `${height}px`;
|
||||
this.#imagePreview.append(canvas);
|
||||
|
||||
this.#toggleNotNow();
|
||||
this.#toggleAI(hasAI);
|
||||
this.#toggleError(false);
|
||||
|
||||
try {
|
||||
await this.#overlayManager.open(this.#dialog);
|
||||
} catch (ex) {
|
||||
this.#close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
#cancel() {
|
||||
this.#currentEditor.altTextData = {
|
||||
cancel: true,
|
||||
};
|
||||
const altText = this.#textarea.value.trim();
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.alt_text.dismiss",
|
||||
data: {
|
||||
alt_text_type: altText ? "present" : "empty",
|
||||
flow: this.#firstTime ? "image_add" : "alt_text_edit",
|
||||
},
|
||||
});
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.image_added",
|
||||
data: { alt_text_modal: true, alt_text_type: "skipped" },
|
||||
});
|
||||
this.#finish();
|
||||
}
|
||||
|
||||
#finish() {
|
||||
if (this.#overlayManager.active === this.#dialog) {
|
||||
this.#overlayManager.close(this.#dialog);
|
||||
}
|
||||
}
|
||||
|
||||
#close() {
|
||||
const canvas = this.#imagePreview.firstChild;
|
||||
canvas.remove();
|
||||
canvas.width = canvas.height = 0;
|
||||
this.#imageData = null;
|
||||
|
||||
this.#toggleLoading(false);
|
||||
|
||||
this.#uiManager?.addEditListeners();
|
||||
this.#currentEditor.altTextFinish();
|
||||
this.#uiManager?.setSelected(this.#currentEditor);
|
||||
this.#currentEditor = null;
|
||||
this.#uiManager = null;
|
||||
}
|
||||
|
||||
#save() {
|
||||
const altText = this.#textarea.value.trim();
|
||||
this.#currentEditor.altTextData = {
|
||||
altText,
|
||||
decorative: false,
|
||||
};
|
||||
this.#currentEditor.altTextData.guessedAltText = this.#guessedAltText;
|
||||
|
||||
if (this.#guessedAltText && this.#guessedAltText !== altText) {
|
||||
const guessedWords = new Set(this.#guessedAltText.split(/\s+/));
|
||||
const words = new Set(altText.split(/\s+/));
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.alt_text.user_edit",
|
||||
data: {
|
||||
total_words: guessedWords.size,
|
||||
words_removed: guessedWords.difference(words).size,
|
||||
words_added: words.difference(guessedWords).size,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.image_added",
|
||||
data: {
|
||||
alt_text_modal: true,
|
||||
alt_text_type: altText ? "present" : "empty",
|
||||
},
|
||||
});
|
||||
|
||||
this.#currentEditor._reportTelemetry({
|
||||
action: "pdfjs.image.alt_text.save",
|
||||
data: {
|
||||
alt_text_type: altText ? "present" : "empty",
|
||||
flow: this.#firstTime ? "image_add" : "alt_text_edit",
|
||||
},
|
||||
});
|
||||
|
||||
this.#finish();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#uiManager = null; // Avoid re-adding the edit listeners.
|
||||
this.#finish();
|
||||
}
|
||||
}
|
||||
|
||||
class ImageAltTextSettings {
|
||||
#aiModelSettings;
|
||||
|
||||
#createModelButton;
|
||||
|
||||
#downloadModelButton;
|
||||
|
||||
#dialog;
|
||||
|
||||
#eventBus;
|
||||
|
||||
#mlManager;
|
||||
|
||||
#overlayManager;
|
||||
|
||||
#showAltTextDialogButton;
|
||||
|
||||
constructor(
|
||||
{
|
||||
dialog,
|
||||
createModelButton,
|
||||
aiModelSettings,
|
||||
learnMore,
|
||||
closeButton,
|
||||
deleteModelButton,
|
||||
downloadModelButton,
|
||||
showAltTextDialogButton,
|
||||
},
|
||||
overlayManager,
|
||||
eventBus,
|
||||
mlManager
|
||||
) {
|
||||
this.#dialog = dialog;
|
||||
this.#aiModelSettings = aiModelSettings;
|
||||
this.#createModelButton = createModelButton;
|
||||
this.#downloadModelButton = downloadModelButton;
|
||||
this.#showAltTextDialogButton = showAltTextDialogButton;
|
||||
this.#overlayManager = overlayManager;
|
||||
this.#eventBus = eventBus;
|
||||
this.#mlManager = mlManager;
|
||||
|
||||
const { altTextLearnMoreUrl } = mlManager;
|
||||
if (altTextLearnMoreUrl) {
|
||||
learnMore.href = altTextLearnMoreUrl;
|
||||
}
|
||||
|
||||
dialog.addEventListener("contextmenu", noContextMenu);
|
||||
|
||||
createModelButton.addEventListener("click", async e => {
|
||||
const checked = this.#togglePref("enableGuessAltText", e);
|
||||
await mlManager.toggleService("altText", checked);
|
||||
this.#reportTelemetry({
|
||||
type: "stamp",
|
||||
action: "pdfjs.image.alt_text.settings_ai_generation_check",
|
||||
data: { status: checked },
|
||||
});
|
||||
});
|
||||
|
||||
showAltTextDialogButton.addEventListener("click", e => {
|
||||
const checked = this.#togglePref("enableNewAltTextWhenAddingImage", e);
|
||||
this.#reportTelemetry({
|
||||
type: "stamp",
|
||||
action: "pdfjs.image.alt_text.settings_edit_alt_text_check",
|
||||
data: { status: checked },
|
||||
});
|
||||
});
|
||||
|
||||
deleteModelButton.addEventListener("click", this.#delete.bind(this, true));
|
||||
downloadModelButton.addEventListener(
|
||||
"click",
|
||||
this.#download.bind(this, true)
|
||||
);
|
||||
|
||||
closeButton.addEventListener("click", this.#finish.bind(this));
|
||||
|
||||
learnMore.addEventListener("click", () => {
|
||||
this.#reportTelemetry({
|
||||
type: "stamp",
|
||||
action: "pdfjs.image.alt_text.info",
|
||||
data: { topic: "ai_generation" },
|
||||
});
|
||||
});
|
||||
|
||||
eventBus._on("enablealttextmodeldownload", ({ value }) => {
|
||||
if (value) {
|
||||
this.#download(false);
|
||||
} else {
|
||||
this.#delete(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.#overlayManager.register(dialog);
|
||||
}
|
||||
|
||||
#reportTelemetry(data) {
|
||||
this.#eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: {
|
||||
type: "editing",
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async #download(isFromUI = false) {
|
||||
if (isFromUI) {
|
||||
this.#downloadModelButton.disabled = true;
|
||||
const span = this.#downloadModelButton.firstChild;
|
||||
span.setAttribute(
|
||||
"data-l10n-id",
|
||||
"pdfjs-editor-alt-text-settings-downloading-model-button"
|
||||
);
|
||||
|
||||
await this.#mlManager.downloadModel("altText");
|
||||
|
||||
span.setAttribute(
|
||||
"data-l10n-id",
|
||||
"pdfjs-editor-alt-text-settings-download-model-button"
|
||||
);
|
||||
|
||||
this.#createModelButton.disabled = false;
|
||||
this.#setPref("enableGuessAltText", true);
|
||||
this.#mlManager.toggleService("altText", true);
|
||||
this.#setPref("enableAltTextModelDownload", true);
|
||||
this.#downloadModelButton.disabled = false;
|
||||
}
|
||||
|
||||
this.#aiModelSettings.classList.toggle("download", false);
|
||||
this.#createModelButton.setAttribute("aria-pressed", true);
|
||||
}
|
||||
|
||||
async #delete(isFromUI = false) {
|
||||
if (isFromUI) {
|
||||
await this.#mlManager.deleteModel("altText");
|
||||
this.#setPref("enableGuessAltText", false);
|
||||
this.#setPref("enableAltTextModelDownload", false);
|
||||
}
|
||||
|
||||
this.#aiModelSettings.classList.toggle("download", true);
|
||||
this.#createModelButton.disabled = true;
|
||||
this.#createModelButton.setAttribute("aria-pressed", false);
|
||||
}
|
||||
|
||||
async open({ enableGuessAltText, enableNewAltTextWhenAddingImage }) {
|
||||
const { enableAltTextModelDownload } = this.#mlManager;
|
||||
this.#createModelButton.disabled = !enableAltTextModelDownload;
|
||||
this.#createModelButton.setAttribute(
|
||||
"aria-pressed",
|
||||
enableAltTextModelDownload && enableGuessAltText
|
||||
);
|
||||
this.#showAltTextDialogButton.setAttribute(
|
||||
"aria-pressed",
|
||||
enableNewAltTextWhenAddingImage
|
||||
);
|
||||
this.#aiModelSettings.classList.toggle(
|
||||
"download",
|
||||
!enableAltTextModelDownload
|
||||
);
|
||||
|
||||
await this.#overlayManager.open(this.#dialog);
|
||||
this.#reportTelemetry({
|
||||
type: "stamp",
|
||||
action: "pdfjs.image.alt_text.settings_displayed",
|
||||
});
|
||||
}
|
||||
|
||||
#togglePref(name, { target }) {
|
||||
const checked = target.getAttribute("aria-pressed") !== "true";
|
||||
this.#setPref(name, checked);
|
||||
target.setAttribute("aria-pressed", checked);
|
||||
return checked;
|
||||
}
|
||||
|
||||
#setPref(name, value) {
|
||||
this.#eventBus.dispatch("setpreference", {
|
||||
source: this,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
#finish() {
|
||||
if (this.#overlayManager.active === this.#dialog) {
|
||||
this.#overlayManager.close(this.#dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ImageAltTextSettings, NewAltTextManager };
|
|
@ -37,44 +37,11 @@ class OverlayManager {
|
|||
}
|
||||
this.#overlays.set(dialog, { canForceClose });
|
||||
|
||||
// NOTE
|
||||
// if (
|
||||
// typeof PDFJSDev !== "undefined" &&
|
||||
// PDFJSDev.test("GENERIC && !SKIP_BABEL") &&
|
||||
// !dialog.showModal
|
||||
// ) {
|
||||
// const dialogPolyfill = require("dialog-polyfill/dist/dialog-polyfill.js");
|
||||
// dialogPolyfill.registerDialog(dialog);
|
||||
//
|
||||
// if (!this._dialogPolyfillCSS) {
|
||||
// this._dialogPolyfillCSS = true;
|
||||
//
|
||||
// const style = document.createElement("style");
|
||||
// style.textContent = PDFJSDev.eval("DIALOG_POLYFILL_CSS");
|
||||
//
|
||||
// document.head.prepend(style);
|
||||
// }
|
||||
// }
|
||||
|
||||
dialog.addEventListener("cancel", evt => {
|
||||
this.#active = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDialogElement} dialog - The overlay's DOM element.
|
||||
* @returns {Promise} A promise that is resolved when the overlay has been
|
||||
* unregistered.
|
||||
*/
|
||||
async unregister(dialog) {
|
||||
if (!this.#overlays.has(dialog)) {
|
||||
throw new Error("The overlay does not exist.");
|
||||
} else if (this.#active === dialog) {
|
||||
throw new Error("The overlay cannot be removed while it is active.");
|
||||
}
|
||||
this.#overlays.delete(dialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDialogElement} dialog - The overlay's DOM element.
|
||||
* @returns {Promise} A promise that is resolved when the overlay has been
|
||||
|
@ -95,6 +62,7 @@ class OverlayManager {
|
|||
this.#active = dialog;
|
||||
// NOTE
|
||||
dialog.classList.add("dialog--open")
|
||||
// dialog.showModal();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,6 +80,7 @@ class OverlayManager {
|
|||
}
|
||||
// NOTE
|
||||
dialog.classList.remove("dialog--open")
|
||||
// dialog.close();
|
||||
this.#active = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPromiseCapability, PasswordResponses } from "./pdfjs";
|
||||
/** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */
|
||||
|
||||
import { PasswordResponses } from "./pdfjs";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PasswordPromptOptions
|
||||
|
@ -37,18 +39,16 @@ class PasswordPrompt {
|
|||
/**
|
||||
* @param {PasswordPromptOptions} options
|
||||
* @param {OverlayManager} overlayManager - Manager for the viewer overlays.
|
||||
* @param {IL10n} l10n - Localization service.
|
||||
* @param {boolean} [isViewerEmbedded] - If the viewer is embedded, in e.g.
|
||||
* an <iframe> or an <object>. The default value is `false`.
|
||||
*/
|
||||
constructor(options, overlayManager, l10n, isViewerEmbedded = false) {
|
||||
constructor(options, overlayManager, isViewerEmbedded = false) {
|
||||
this.dialog = options.dialog;
|
||||
this.label = options.label;
|
||||
this.input = options.input;
|
||||
this.submitButton = options.submitButton;
|
||||
this.cancelButton = options.cancelButton;
|
||||
this.overlayManager = overlayManager;
|
||||
this.l10n = l10n;
|
||||
this._isViewerEmbedded = isViewerEmbedded;
|
||||
|
||||
// Attach the event listeners.
|
||||
|
@ -66,15 +66,13 @@ class PasswordPrompt {
|
|||
}
|
||||
|
||||
async open() {
|
||||
if (this.#activeCapability) {
|
||||
await this.#activeCapability.promise;
|
||||
}
|
||||
this.#activeCapability = createPromiseCapability();
|
||||
await this.#activeCapability?.promise;
|
||||
this.#activeCapability = Promise.withResolvers();
|
||||
|
||||
try {
|
||||
await this.overlayManager.open(this.dialog);
|
||||
} catch (ex) {
|
||||
this.#activeCapability = null;
|
||||
this.#activeCapability.resolve();
|
||||
throw ex;
|
||||
}
|
||||
|
||||
|
@ -86,6 +84,10 @@ class PasswordPrompt {
|
|||
}
|
||||
// NOTE
|
||||
this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect ? 'invalid' : 'label'}`]
|
||||
// this.label.setAttribute(
|
||||
// "data-l10n-id",
|
||||
// passwordIncorrect ? "pdfjs-password-invalid" : "pdfjs-password-label"
|
||||
// );
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
|
|
@ -13,7 +13,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPromiseCapability, getFilenameFromUrl } from "./pdfjs";
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
|
||||
|
||||
import { BaseTreeViewer } from "./base_tree_viewer.js";
|
||||
import { waitOnEventOrTimeout } from "./event_utils.js";
|
||||
|
||||
|
@ -27,6 +30,7 @@ import { waitOnEventOrTimeout } from "./event_utils.js";
|
|||
/**
|
||||
* @typedef {Object} PDFAttachmentViewerRenderParameters
|
||||
* @property {Object|null} attachments - A lookup table of attachment objects.
|
||||
* @property {boolean} [keepRenderedCapability]
|
||||
*/
|
||||
|
||||
class PDFAttachmentViewer extends BaseTreeViewer {
|
||||
|
@ -50,13 +54,13 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||
if (!keepRenderedCapability) {
|
||||
// The only situation in which the `_renderedCapability` should *not* be
|
||||
// replaced is when appending FileAttachment annotations.
|
||||
this._renderedCapability = createPromiseCapability();
|
||||
this._renderedCapability = Promise.withResolvers();
|
||||
}
|
||||
this._pendingDispatchEvent = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
async _dispatchEvent(attachmentsCount) {
|
||||
this._renderedCapability.resolve();
|
||||
|
@ -87,11 +91,14 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_bindLink(element, { content, filename }) {
|
||||
_bindLink(element, { content, description, filename }) {
|
||||
if (description) {
|
||||
element.title = description;
|
||||
}
|
||||
element.onclick = () => {
|
||||
this.downloadManager.openOrDownloadData(element, content, filename);
|
||||
this.downloadManager.openOrDownloadData(content, filename);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
@ -109,26 +116,18 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||
this._dispatchEvent(/* attachmentsCount = */ 0);
|
||||
return;
|
||||
}
|
||||
const names = Object.keys(attachments).sort(function (a, b) {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let attachmentsCount = 0;
|
||||
for (const name of names) {
|
||||
for (const name in attachments) {
|
||||
const item = attachments[name];
|
||||
const content = item.content,
|
||||
filename = getFilenameFromUrl(
|
||||
item.filename,
|
||||
/* onlyStripPath = */ true
|
||||
);
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "treeItem";
|
||||
|
||||
const element = document.createElement("a");
|
||||
this._bindLink(element, { content, filename });
|
||||
element.textContent = this._normalizeTextContent(filename);
|
||||
this._bindLink(element, item);
|
||||
element.textContent = this._normalizeTextContent(item.filename);
|
||||
|
||||
div.append(element);
|
||||
|
||||
|
@ -142,7 +141,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||
/**
|
||||
* Used to append FileAttachment annotations to the sidebar.
|
||||
*/
|
||||
#appendAttachment({ filename, content }) {
|
||||
#appendAttachment(item) {
|
||||
const renderedPromise = this._renderedCapability.promise;
|
||||
|
||||
renderedPromise.then(() => {
|
||||
|
@ -152,14 +151,12 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||
const attachments = this._attachments || Object.create(null);
|
||||
|
||||
for (const name in attachments) {
|
||||
if (filename === name) {
|
||||
if (item.filename === name) {
|
||||
return; // Ignore the new attachment if it already exists.
|
||||
}
|
||||
}
|
||||
attachments[filename] = {
|
||||
filename,
|
||||
content,
|
||||
};
|
||||
attachments[item.filename] = item;
|
||||
|
||||
this.render({
|
||||
attachments,
|
||||
keepRenderedCapability: true,
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
|
||||
import { AnnotationEditorType, shadow } from "./pdfjs";
|
||||
import { CursorTool, PresentationModeState } from "./ui_utils.js";
|
||||
import { AnnotationEditorType } from "./pdfjs";
|
||||
import { GrabToPan } from "./grab_to_pan.js";
|
||||
|
||||
/**
|
||||
|
@ -27,6 +29,10 @@ import { GrabToPan } from "./grab_to_pan.js";
|
|||
*/
|
||||
|
||||
class PDFCursorTools {
|
||||
#active = CursorTool.SELECT;
|
||||
|
||||
#prevActive = null;
|
||||
|
||||
/**
|
||||
* @param {PDFCursorToolsOptions} options
|
||||
*/
|
||||
|
@ -34,13 +40,6 @@ class PDFCursorTools {
|
|||
this.container = container;
|
||||
this.eventBus = eventBus;
|
||||
|
||||
this.active = CursorTool.SELECT;
|
||||
this.previouslyActive = null;
|
||||
|
||||
this.handTool = new GrabToPan({
|
||||
element: this.container,
|
||||
});
|
||||
|
||||
this.#addEventListeners();
|
||||
|
||||
// Defer the initial `switchTool` call, to give other viewer components
|
||||
|
@ -54,7 +53,7 @@ class PDFCursorTools {
|
|||
* @type {number} One of the values in {CursorTool}.
|
||||
*/
|
||||
get activeTool() {
|
||||
return this.active;
|
||||
return this.#active;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,20 +61,32 @@ class PDFCursorTools {
|
|||
* must be one of the values in {CursorTool}.
|
||||
*/
|
||||
switchTool(tool) {
|
||||
if (this.previouslyActive !== null) {
|
||||
if (this.#prevActive !== null) {
|
||||
// Cursor tools cannot be used in PresentationMode/AnnotationEditor.
|
||||
return;
|
||||
}
|
||||
if (tool === this.active) {
|
||||
this.#switchTool(tool);
|
||||
}
|
||||
|
||||
#switchTool(tool, disabled = false) {
|
||||
if (tool === this.#active) {
|
||||
if (this.#prevActive !== null) {
|
||||
// Ensure that the `disabled`-attribute of the buttons will be updated.
|
||||
this.eventBus.dispatch("cursortoolchanged", {
|
||||
source: this,
|
||||
tool,
|
||||
disabled,
|
||||
});
|
||||
}
|
||||
return; // The requested tool is already active.
|
||||
}
|
||||
|
||||
const disableActiveTool = () => {
|
||||
switch (this.active) {
|
||||
switch (this.#active) {
|
||||
case CursorTool.SELECT:
|
||||
break;
|
||||
case CursorTool.HAND:
|
||||
this.handTool.deactivate();
|
||||
this._handTool.deactivate();
|
||||
break;
|
||||
case CursorTool.ZOOM:
|
||||
/* falls through */
|
||||
|
@ -89,7 +100,7 @@ class PDFCursorTools {
|
|||
break;
|
||||
case CursorTool.HAND:
|
||||
disableActiveTool();
|
||||
this.handTool.activate();
|
||||
this._handTool.activate();
|
||||
break;
|
||||
case CursorTool.ZOOM:
|
||||
/* falls through */
|
||||
|
@ -99,47 +110,20 @@ class PDFCursorTools {
|
|||
}
|
||||
// Update the active tool *after* it has been validated above,
|
||||
// in order to prevent setting it to an invalid state.
|
||||
this.active = tool;
|
||||
this.#active = tool;
|
||||
|
||||
this.#dispatchEvent();
|
||||
}
|
||||
|
||||
#dispatchEvent() {
|
||||
this.eventBus.dispatch("cursortoolchanged", {
|
||||
source: this,
|
||||
tool: this.active,
|
||||
tool,
|
||||
disabled,
|
||||
});
|
||||
}
|
||||
|
||||
#addEventListeners() {
|
||||
this.eventBus._on("switchcursortool", evt => {
|
||||
this.switchTool(evt.tool);
|
||||
});
|
||||
|
||||
let annotationEditorMode = AnnotationEditorType.NONE,
|
||||
presentationModeState = PresentationModeState.NORMAL;
|
||||
|
||||
const disableActive = () => {
|
||||
const previouslyActive = this.active;
|
||||
|
||||
this.switchTool(CursorTool.SELECT);
|
||||
this.previouslyActive ??= previouslyActive; // Keep track of the first one.
|
||||
};
|
||||
const enableActive = () => {
|
||||
const previouslyActive = this.previouslyActive;
|
||||
|
||||
if (
|
||||
previouslyActive !== null &&
|
||||
annotationEditorMode === AnnotationEditorType.NONE &&
|
||||
presentationModeState === PresentationModeState.NORMAL
|
||||
) {
|
||||
this.previouslyActive = null;
|
||||
this.switchTool(previouslyActive);
|
||||
}
|
||||
};
|
||||
|
||||
this.eventBus._on("secondarytoolbarreset", evt => {
|
||||
if (this.previouslyActive !== null) {
|
||||
if (!evt.reset) {
|
||||
this.switchTool(evt.tool);
|
||||
} else if (this.#prevActive !== null) {
|
||||
annotationEditorMode = AnnotationEditorType.NONE;
|
||||
presentationModeState = PresentationModeState.NORMAL;
|
||||
|
||||
|
@ -147,6 +131,24 @@ class PDFCursorTools {
|
|||
}
|
||||
});
|
||||
|
||||
let annotationEditorMode = AnnotationEditorType.NONE,
|
||||
presentationModeState = PresentationModeState.NORMAL;
|
||||
|
||||
const disableActive = () => {
|
||||
this.#prevActive ??= this.#active; // Keep track of the first one.
|
||||
this.#switchTool(CursorTool.SELECT, /* disabled = */ true);
|
||||
};
|
||||
const enableActive = () => {
|
||||
if (
|
||||
this.#prevActive !== null &&
|
||||
annotationEditorMode === AnnotationEditorType.NONE &&
|
||||
presentationModeState === PresentationModeState.NORMAL
|
||||
) {
|
||||
this.#switchTool(this.#prevActive);
|
||||
this.#prevActive = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.eventBus._on("annotationeditormodechanged", ({ mode }) => {
|
||||
annotationEditorMode = mode;
|
||||
|
||||
|
@ -167,6 +169,19 @@ class PDFCursorTools {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
get _handTool() {
|
||||
return shadow(
|
||||
this,
|
||||
"_handTool",
|
||||
new GrabToPan({
|
||||
element: this.container,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { PDFCursorTools };
|
||||
|
|
|
@ -13,23 +13,28 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPromiseCapability, PDFDateString } from "./pdfjs";
|
||||
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
/** @typedef {import("./interfaces.js").IL10n} IL10n */
|
||||
/** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
|
||||
|
||||
const DEFAULT_FIELD_CONTENT = "-";
|
||||
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
|
||||
import { PDFDateString } from "./pdfjs";
|
||||
|
||||
// See https://en.wikibooks.org/wiki/Lentis/Conversion_to_the_Metric_Standard_in_the_United_States
|
||||
const NON_METRIC_LOCALES = ["en-us", "en-lr", "my"];
|
||||
|
||||
// Should use the format: `width x height`, in portrait orientation.
|
||||
// Should use the format: `width x height`, in portrait orientation. The names,
|
||||
// which are l10n-ids, should be lowercase.
|
||||
// See https://en.wikipedia.org/wiki/Paper_size
|
||||
const US_PAGE_NAMES = {
|
||||
"8.5x11": "Letter",
|
||||
"8.5x14": "Legal",
|
||||
"8.5x11": "pdfjs-document-properties-page-size-name-letter",
|
||||
"8.5x14": "pdfjs-document-properties-page-size-name-legal",
|
||||
};
|
||||
const METRIC_PAGE_NAMES = {
|
||||
"297x420": "A3",
|
||||
"210x297": "A4",
|
||||
"297x420": "pdfjs-document-properties-page-size-name-a-three",
|
||||
"210x297": "pdfjs-document-properties-page-size-name-a-four",
|
||||
};
|
||||
|
||||
function getPageName(size, isPortrait, pageNames) {
|
||||
|
@ -82,12 +87,6 @@ class PDFDocumentProperties {
|
|||
eventBus._on("rotationchanging", evt => {
|
||||
this._pagesRotation = evt.pagesRotation;
|
||||
});
|
||||
|
||||
this._isNonMetricLocale = true; // The default viewer locale is 'en-us'.
|
||||
// NOTE
|
||||
// l10n.getLanguage().then(locale => {
|
||||
// this._isNonMetricLocale = NON_METRIC_LOCALES.includes(locale);
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,6 +131,7 @@ class PDFDocumentProperties {
|
|||
this.#parseFileSize(contentLength),
|
||||
this.#parseDate(info.CreationDate),
|
||||
this.#parseDate(info.ModDate),
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
this.pdfDocument.getPage(currentPageNumber).then(pdfPage => {
|
||||
return this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation);
|
||||
}),
|
||||
|
@ -188,7 +188,7 @@ class PDFDocumentProperties {
|
|||
setDocument(pdfDocument) {
|
||||
if (this.pdfDocument) {
|
||||
this.#reset();
|
||||
this.#updateUI(true);
|
||||
this.#updateUI();
|
||||
}
|
||||
if (!pdfDocument) {
|
||||
return;
|
||||
|
@ -202,7 +202,7 @@ class PDFDocumentProperties {
|
|||
this.pdfDocument = null;
|
||||
|
||||
this.#fieldData = null;
|
||||
this._dataAvailableCapability = createPromiseCapability();
|
||||
this._dataAvailableCapability = Promise.withResolvers();
|
||||
this._currentPageNumber = 1;
|
||||
this._pagesRotation = 0;
|
||||
}
|
||||
|
@ -210,38 +210,29 @@ class PDFDocumentProperties {
|
|||
/**
|
||||
* Always updates all of the dialog fields, to prevent inconsistent UI state.
|
||||
* NOTE: If the contents of a particular field is neither a non-empty string,
|
||||
* nor a number, it will fall back to `DEFAULT_FIELD_CONTENT`.
|
||||
* nor a number, it will fall back to "-".
|
||||
*/
|
||||
#updateUI(reset = false) {
|
||||
if (reset || !this.#fieldData) {
|
||||
for (const id in this.fields) {
|
||||
this.fields[id].textContent = DEFAULT_FIELD_CONTENT;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.overlayManager.active !== this.dialog) {
|
||||
// Don't bother updating the dialog if has already been closed,
|
||||
#updateUI() {
|
||||
if (this.#fieldData && this.overlayManager.active !== this.dialog) {
|
||||
// Don't bother updating the dialog if it's already been closed,
|
||||
// unless it's being reset (i.e. `this.#fieldData === null`),
|
||||
// since it will be updated the next time `this.open` is called.
|
||||
return;
|
||||
}
|
||||
for (const id in this.fields) {
|
||||
const content = this.#fieldData[id];
|
||||
this.fields[id].textContent =
|
||||
content || content === 0 ? content : DEFAULT_FIELD_CONTENT;
|
||||
const content = this.#fieldData?.[id];
|
||||
this.fields[id].textContent = content || content === 0 ? content : "-";
|
||||
}
|
||||
}
|
||||
|
||||
async #parseFileSize(fileSize = 0) {
|
||||
const kb = fileSize / 1024,
|
||||
async #parseFileSize(b = 0) {
|
||||
const kb = b / 1024,
|
||||
mb = kb / 1024;
|
||||
if (!kb) {
|
||||
return undefined;
|
||||
}
|
||||
// NOTE
|
||||
if (mb >= 1) {
|
||||
return `${mb >= 1 && (+mb.toPrecision(3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes`
|
||||
}
|
||||
return `${mb < 1 && (+kb.toPrecision(3)).toLocaleString()} KB (${fileSize.toLocaleString()} bytes`
|
||||
|
||||
return kb
|
||||
// NOTE
|
||||
? mb >= 1 ? `${mb.toPrecision(3)} MB ${b} bytes` : `${kb.toPrecision(3)} KB ${b} bytes`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async #parsePageSize(pageSizeInches, pagesRotation) {
|
||||
|
@ -255,7 +246,8 @@ class PDFDocumentProperties {
|
|||
height: pageSizeInches.width,
|
||||
};
|
||||
}
|
||||
const isPortrait = isPortraitOrientation(pageSizeInches);
|
||||
const isPortrait = isPortraitOrientation(pageSizeInches),
|
||||
nonMetric = NON_METRIC_LOCALES.includes(this.l10n.getLanguage());
|
||||
|
||||
let sizeInches = {
|
||||
width: Math.round(pageSizeInches.width * 100) / 100,
|
||||
|
@ -267,12 +259,12 @@ class PDFDocumentProperties {
|
|||
height: Math.round(pageSizeInches.height * 25.4 * 10) / 10,
|
||||
};
|
||||
|
||||
let rawName =
|
||||
let nameId =
|
||||
getPageName(sizeInches, isPortrait, US_PAGE_NAMES) ||
|
||||
getPageName(sizeMillimeters, isPortrait, METRIC_PAGE_NAMES);
|
||||
|
||||
if (
|
||||
!rawName &&
|
||||
!nameId &&
|
||||
!(
|
||||
Number.isInteger(sizeMillimeters.width) &&
|
||||
Number.isInteger(sizeMillimeters.height)
|
||||
|
@ -295,8 +287,8 @@ class PDFDocumentProperties {
|
|||
Math.abs(exactMillimeters.width - intMillimeters.width) < 0.1 &&
|
||||
Math.abs(exactMillimeters.height - intMillimeters.height) < 0.1
|
||||
) {
|
||||
rawName = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES);
|
||||
if (rawName) {
|
||||
nameId = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES);
|
||||
if (nameId) {
|
||||
// Update *both* sizes, computed above, to ensure that the displayed
|
||||
// dimensions always correspond to the detected page name.
|
||||
sizeInches = {
|
||||
|
@ -308,32 +300,42 @@ class PDFDocumentProperties {
|
|||
}
|
||||
}
|
||||
|
||||
// NOTE
|
||||
const [{ width, height }, unit, name, orientation] = await Promise.all([
|
||||
this._isNonMetricLocale ? sizeInches : sizeMillimeters,
|
||||
this._isNonMetricLocale ? window.siyuan.languages.unitInches : window.siyuan.languages.unitMillimeters,
|
||||
rawName &&
|
||||
window.siyuan.languages[`document_properties_page_size_name_${rawName.toLowerCase()}`],
|
||||
window.siyuan.languages[`document_properties_page_size_orientation_${isPortrait ? 'portrait' : 'landscape'}`],
|
||||
nonMetric ? sizeInches : sizeMillimeters,
|
||||
// NOTE
|
||||
this.l10n.get(
|
||||
nonMetric
|
||||
? "unitInches"
|
||||
: "unitMillimeters"
|
||||
),
|
||||
nameId && this.l10n.get(nameId),
|
||||
this.l10n.get(
|
||||
isPortrait
|
||||
? "document_properties_page_size_orientation_portrait"
|
||||
: "document_properties_page_size_orientation_landscape"
|
||||
),
|
||||
]);
|
||||
if (name) {
|
||||
return `${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${name}, ${orientation})`
|
||||
}
|
||||
return `${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${orientation})`
|
||||
|
||||
// NOTE
|
||||
return name ?`${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${name}, ${orientation})`:
|
||||
`${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${orientation})`;
|
||||
}
|
||||
|
||||
async #parseDate(inputDate) {
|
||||
const dateObject = PDFDateString.toDateObject(inputDate);
|
||||
if (!dateObject) {
|
||||
return undefined;
|
||||
}
|
||||
// NOTE
|
||||
return `${dateObject.toLocaleDateString()}, ${dateObject.toLocaleTimeString()}`
|
||||
const dateObj = PDFDateString.toDateObject(inputDate);
|
||||
return dateObj
|
||||
// NOTE
|
||||
? `${dateObj.toLocaleDateString()}, ${dateObj.toLocaleTimeString()}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
#parseLinearization(isLinearized) {
|
||||
// NOTE
|
||||
return isLinearized ? 'Yes' : 'No'
|
||||
return this.l10n.get(
|
||||
isLinearized
|
||||
// NOTE
|
||||
? "Yes"
|
||||
: "No"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
*/
|
||||
|
||||
import { FindState } from "./pdf_find_controller.js";
|
||||
import { toggleExpandedBtn } from "./ui_utils.js";
|
||||
|
||||
const MATCHES_COUNT_LIMIT = 1000;
|
||||
|
||||
|
@ -24,7 +25,11 @@ const MATCHES_COUNT_LIMIT = 1000;
|
|||
* is done by PDFFindController.
|
||||
*/
|
||||
class PDFFindBar {
|
||||
constructor(options, eventBus, l10n) {
|
||||
#mainContainer;
|
||||
|
||||
#resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this));
|
||||
|
||||
constructor(options, mainContainer, eventBus) {
|
||||
this.opened = false;
|
||||
|
||||
this.bar = options.bar;
|
||||
|
@ -39,7 +44,7 @@ class PDFFindBar {
|
|||
this.findPreviousButton = options.findPreviousButton;
|
||||
this.findNextButton = options.findNextButton;
|
||||
this.eventBus = eventBus;
|
||||
this.l10n = l10n;
|
||||
this.#mainContainer = mainContainer;
|
||||
|
||||
// Add event listeners to the DOM elements.
|
||||
this.toggleButton.addEventListener("click", () => {
|
||||
|
@ -110,8 +115,6 @@ class PDFFindBar {
|
|||
this.matchDiacritics.parentElement.classList.add("b3-button--outline")
|
||||
}
|
||||
});
|
||||
|
||||
this.eventBus._on("resize", this.#adjustWidth.bind(this));
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
@ -123,7 +126,6 @@ class PDFFindBar {
|
|||
source: this,
|
||||
type,
|
||||
query: this.findField.value,
|
||||
phraseSearch: true,
|
||||
caseSensitive: this.caseSensitive.checked,
|
||||
entireWord: this.entireWord.checked,
|
||||
highlightAll: this.highlightAll.checked,
|
||||
|
@ -133,9 +135,9 @@ class PDFFindBar {
|
|||
}
|
||||
|
||||
updateUIState(state, previous, matchesCount) {
|
||||
// NOTE
|
||||
let findMsg = "";
|
||||
let status = "";
|
||||
const { findField, findMsg } = this;
|
||||
let findMsgId = "",
|
||||
status = "";
|
||||
|
||||
switch (state) {
|
||||
case FindState.FOUND:
|
||||
|
@ -145,65 +147,83 @@ class PDFFindBar {
|
|||
break;
|
||||
case FindState.NOT_FOUND:
|
||||
// NOTE
|
||||
findMsg = window.siyuan.languages.find_not_found
|
||||
findMsgId = window.siyuan.languages.find_not_found
|
||||
// findMsgId = "pdfjs-find-not-found";
|
||||
status = "notFound";
|
||||
break;
|
||||
case FindState.WRAPPED:
|
||||
// NOTE
|
||||
findMsg = window.siyuan.languages.find_not_found[`find_reached_${previous ? 'top' : 'bottom'}`]
|
||||
findMsgId = window.siyuan.languages.find_not_found[`find_reached_${previous ? 'top' : 'bottom'}`]
|
||||
// findMsgId = previous
|
||||
// ? "pdfjs-find-reached-top"
|
||||
// : "pdfjs-find-reached-bottom";
|
||||
break;
|
||||
}
|
||||
this.findField.setAttribute("data-status", status);
|
||||
this.findField.setAttribute("aria-invalid", state === FindState.NOT_FOUND);
|
||||
findField.setAttribute("data-status", status);
|
||||
findField.setAttribute("aria-invalid", state === FindState.NOT_FOUND);
|
||||
|
||||
findMsg.setAttribute("data-status", status);
|
||||
if (findMsgId) {
|
||||
// NOTE
|
||||
findMsg.textContent = findMsgId;
|
||||
// findMsg.setAttribute("data-l10n-id", findMsgId);
|
||||
} else {
|
||||
findMsg.removeAttribute("data-l10n-id");
|
||||
findMsg.textContent = "";
|
||||
}
|
||||
|
||||
// NOTE
|
||||
this.findMsg.textContent = findMsg
|
||||
this.#adjustWidth()
|
||||
this.updateResultsCount(matchesCount);
|
||||
}
|
||||
|
||||
updateResultsCount({ current = 0, total = 0 } = {}) {
|
||||
const limit = MATCHES_COUNT_LIMIT;
|
||||
// NOTE
|
||||
let matchCountMsg = "";
|
||||
const { findResultsCount } = this;
|
||||
|
||||
if (total > 0) {
|
||||
if (total > limit) {
|
||||
matchCountMsg = window.siyuan.languages.find_match_count_limit.replace(
|
||||
'{{limit}}', limit)
|
||||
} else {
|
||||
matchCountMsg = window.siyuan.languages.find_match_count.replace('{{current}}',
|
||||
current).replace('{{total}}', total)
|
||||
}
|
||||
const limit = MATCHES_COUNT_LIMIT;
|
||||
// NOTE
|
||||
findResultsCount.textContent = total > limit ?
|
||||
window.siyuan.languages.find_match_count_limit.replace('{{limit}}', limit) :
|
||||
window.siyuan.languages.find_match_count.replace('{{current}}', current).replace('{{total}}', total);
|
||||
// findResultsCount.setAttribute(
|
||||
// "data-l10n-id",
|
||||
// total > limit
|
||||
// ? "pdfjs-find-match-count-limit"
|
||||
// : "pdfjs-find-match-count"
|
||||
// );
|
||||
// findResultsCount.setAttribute(
|
||||
// "data-l10n-args",
|
||||
// JSON.stringify({ limit, current, total })
|
||||
// );
|
||||
} else {
|
||||
findResultsCount.removeAttribute("data-l10n-id");
|
||||
findResultsCount.textContent = "";
|
||||
}
|
||||
|
||||
this.findResultsCount.textContent = matchCountMsg
|
||||
this.#adjustWidth()
|
||||
}
|
||||
|
||||
open() {
|
||||
if (!this.opened) {
|
||||
// Potentially update the findbar layout, row vs column, when:
|
||||
// - The width of the viewer itself changes.
|
||||
// - The width of the findbar changes, by toggling the visibility
|
||||
// (or localization) of find count/status messages.
|
||||
this.#resizeObserver.observe(this.#mainContainer);
|
||||
this.#resizeObserver.observe(this.bar);
|
||||
|
||||
this.opened = true;
|
||||
this.toggleButton.classList.add("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "true");
|
||||
// NOTE
|
||||
this.bar.classList.remove("fn__hidden");
|
||||
toggleExpandedBtn(this.toggleButton, true, this.bar);
|
||||
}
|
||||
this.findField.select();
|
||||
this.findField.focus();
|
||||
|
||||
this.#adjustWidth();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
this.#resizeObserver.disconnect();
|
||||
|
||||
this.opened = false;
|
||||
this.toggleButton.classList.remove("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "false");
|
||||
// NOTE
|
||||
this.bar.classList.add("fn__hidden");
|
||||
toggleExpandedBtn(this.toggleButton, false, this.bar);
|
||||
|
||||
this.eventBus.dispatch("findbarclose", { source: this });
|
||||
}
|
||||
|
@ -216,25 +236,22 @@ class PDFFindBar {
|
|||
}
|
||||
}
|
||||
|
||||
#adjustWidth() {
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
#resizeObserverCallback() {
|
||||
const { bar } = this;
|
||||
// The find bar has an absolute position and thus the browser extends
|
||||
// its width to the maximum possible width once the find bar does not fit
|
||||
// entirely within the window anymore (and its elements are automatically
|
||||
// wrapped). Here we detect and fix that.
|
||||
this.bar.classList.remove("wrapContainers");
|
||||
bar.classList.remove("wrapContainers");
|
||||
|
||||
const findbarHeight = this.bar.clientHeight;
|
||||
const inputContainerHeight = this.bar.firstElementChild.clientHeight;
|
||||
const findbarHeight = bar.clientHeight;
|
||||
const inputContainerHeight = bar.firstElementChild.clientHeight;
|
||||
|
||||
if (findbarHeight > inputContainerHeight) {
|
||||
// The findbar is taller than the input container, which means that
|
||||
// the browser wrapped some of the elements. For a consistent look,
|
||||
// wrap all of them to adjust the width of the find bar.
|
||||
this.bar.classList.add("wrapContainers");
|
||||
bar.classList.add("wrapContainers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,7 @@
|
|||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
|
||||
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
|
||||
import { createPromiseCapability } from "./pdfjs";
|
||||
import { getCharacterType } from "./pdf_find_utils.js";
|
||||
import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
|
||||
|
||||
const FindState = {
|
||||
FOUND: 0,
|
||||
|
@ -126,18 +125,14 @@ function normalize(text) {
|
|||
} else {
|
||||
// Compile the regular expression for text normalization once.
|
||||
const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join("");
|
||||
const toNormalizeWithNFKC =
|
||||
"\u2460-\u2473" + // Circled numbers.
|
||||
"\u24b6-\u24ff" + // Circled letters/numbers.
|
||||
"\u3244-\u32bf" + // Circled ideograms/numbers.
|
||||
"\u32d0-\u32fe" + // Circled ideograms.
|
||||
"\uff00-\uffef"; // Halfwidth, fullwidth forms.
|
||||
const toNormalizeWithNFKC = getNormalizeWithNFKC();
|
||||
|
||||
// 3040-309F: Hiragana
|
||||
// 30A0-30FF: Katakana
|
||||
const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])";
|
||||
const HKDiacritics = "(?:\u3099|\u309A)";
|
||||
const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(${CJK}\\n)|(\\n)`;
|
||||
const CompoundWord = "\\p{Ll}-\\n\\p{Lu}";
|
||||
const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(${CompoundWord})|(\\S-\\n)|(${CJK}\\n)|(\\n)`;
|
||||
|
||||
if (syllablePositions.length === 0) {
|
||||
// Most of the syllables belong to Hangul so there are no need
|
||||
|
@ -199,7 +194,7 @@ function normalize(text) {
|
|||
|
||||
normalized = normalized.replace(
|
||||
normalizationRegex,
|
||||
(match, p1, p2, p3, p4, p5, p6, p7, p8, i) => {
|
||||
(match, p1, p2, p3, p4, p5, p6, p7, p8, p9, i) => {
|
||||
i -= shiftOrigin;
|
||||
if (p1) {
|
||||
// Maybe fractions or quotations mark...
|
||||
|
@ -273,7 +268,7 @@ function normalize(text) {
|
|||
|
||||
if (hasTrailingDashEOL) {
|
||||
// Diacritics are followed by a -\n.
|
||||
// See comments in `if (p5)` block.
|
||||
// See comments in `if (p6)` block.
|
||||
i += len - 1;
|
||||
positions.push([i - shift + 1, 1 + shift]);
|
||||
shift += 1;
|
||||
|
@ -286,32 +281,41 @@ function normalize(text) {
|
|||
}
|
||||
|
||||
if (p5) {
|
||||
// Compound word with a line break after the hyphen.
|
||||
positions.push([i - shift + 3, 1 + shift]);
|
||||
shift += 1;
|
||||
shiftOrigin += 1;
|
||||
eol += 1;
|
||||
return p5.replace("\n", "");
|
||||
}
|
||||
|
||||
if (p6) {
|
||||
// "X-\n" is removed because an hyphen at the end of a line
|
||||
// with not a space before is likely here to mark a break
|
||||
// in a word.
|
||||
// If X is encoded with UTF-32 then it can have a length greater than 1.
|
||||
// The \n isn't in the original text so here y = i, n = X.len - 2 and
|
||||
// o = X.len - 1.
|
||||
const len = p5.length - 2;
|
||||
const len = p6.length - 2;
|
||||
positions.push([i - shift + len, 1 + shift]);
|
||||
shift += 1;
|
||||
shiftOrigin += 1;
|
||||
eol += 1;
|
||||
return p5.slice(0, -2);
|
||||
}
|
||||
|
||||
if (p6) {
|
||||
// An ideographic at the end of a line doesn't imply adding an extra
|
||||
// white space.
|
||||
// A CJK can be encoded in UTF-32, hence their length isn't always 1.
|
||||
const len = p6.length - 1;
|
||||
positions.push([i - shift + len, shift]);
|
||||
shiftOrigin += 1;
|
||||
eol += 1;
|
||||
return p6.slice(0, -1);
|
||||
return p6.slice(0, -2);
|
||||
}
|
||||
|
||||
if (p7) {
|
||||
// An ideographic at the end of a line doesn't imply adding an extra
|
||||
// white space.
|
||||
// A CJK can be encoded in UTF-32, hence their length isn't always 1.
|
||||
const len = p7.length - 1;
|
||||
positions.push([i - shift + len, shift]);
|
||||
shiftOrigin += 1;
|
||||
eol += 1;
|
||||
return p7.slice(0, -1);
|
||||
}
|
||||
|
||||
if (p8) {
|
||||
// eol is replaced by space: "foo\nbar" is likely equivalent to
|
||||
// "foo bar".
|
||||
positions.push([i - shift + 1, shift - 1]);
|
||||
|
@ -333,7 +337,7 @@ function normalize(text) {
|
|||
shift -= newCharLen;
|
||||
shiftOrigin += newCharLen;
|
||||
}
|
||||
return p8;
|
||||
return p9;
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -350,8 +354,10 @@ function getOriginalIndex(diffs, pos, len) {
|
|||
return [pos, len];
|
||||
}
|
||||
|
||||
// First char in the new string.
|
||||
const start = pos;
|
||||
const end = pos + len;
|
||||
// Last char in the new string.
|
||||
const end = pos + len - 1;
|
||||
let i = binarySearchFirstItem(diffs, x => x[0] >= start);
|
||||
if (diffs[i][0] > start) {
|
||||
--i;
|
||||
|
@ -362,7 +368,14 @@ function getOriginalIndex(diffs, pos, len) {
|
|||
--j;
|
||||
}
|
||||
|
||||
return [start + diffs[i][1], len + diffs[j][1] - diffs[i][1]];
|
||||
// First char in the old string.
|
||||
const oldStart = start + diffs[i][1];
|
||||
|
||||
// Last char in the old string.
|
||||
const oldEnd = end + diffs[j][1];
|
||||
const oldLen = oldEnd + 1 - oldStart;
|
||||
|
||||
return [oldStart, oldLen];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -378,6 +391,8 @@ function getOriginalIndex(diffs, pos, len) {
|
|||
* Provides search functionality to find a given string in a PDF document.
|
||||
*/
|
||||
class PDFFindController {
|
||||
#state = null;
|
||||
|
||||
#updateMatchesCountOnProgress = true;
|
||||
|
||||
#visitedPagesCount = 0;
|
||||
|
@ -390,6 +405,12 @@ class PDFFindController {
|
|||
this._eventBus = eventBus;
|
||||
this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress;
|
||||
|
||||
/**
|
||||
* Callback used to check if a `pageNumber` is currently visible.
|
||||
* @type {function}
|
||||
*/
|
||||
this.onIsPageVisible = null;
|
||||
|
||||
this.#reset();
|
||||
eventBus._on("find", this.#onFind.bind(this));
|
||||
eventBus._on("findbarclose", this.#onFindBarClose.bind(this));
|
||||
|
@ -412,7 +433,7 @@ class PDFFindController {
|
|||
}
|
||||
|
||||
get state() {
|
||||
return this._state;
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -439,10 +460,10 @@ class PDFFindController {
|
|||
const pdfDocument = this._pdfDocument;
|
||||
const { type } = state;
|
||||
|
||||
if (this._state === null || this.#shouldDirtyMatch(state)) {
|
||||
if (this.#state === null || this.#shouldDirtyMatch(state)) {
|
||||
this._dirtyMatch = true;
|
||||
}
|
||||
this._state = state;
|
||||
this.#state = state;
|
||||
if (type !== "highlightallchange") {
|
||||
this.#updateUIState(FindState.PENDING);
|
||||
}
|
||||
|
@ -481,7 +502,7 @@ class PDFFindController {
|
|||
|
||||
// When the findbar was previously closed, and `highlightAll` is set,
|
||||
// ensure that the matches on all active pages are highlighted again.
|
||||
if (findbarClosed && this._state.highlightAll) {
|
||||
if (findbarClosed && this.#state.highlightAll) {
|
||||
this.#updateAllPages();
|
||||
}
|
||||
} else if (type === "highlightallchange") {
|
||||
|
@ -499,6 +520,18 @@ class PDFFindController {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFFindControllerScrollMatchIntoViewParams
|
||||
* @property {HTMLElement} element
|
||||
* @property {number} selectedLeft
|
||||
* @property {number} pageIndex
|
||||
* @property {number} matchIndex
|
||||
*/
|
||||
|
||||
/**
|
||||
* Scroll the current match into view.
|
||||
* @param {PDFFindControllerScrollMatchIntoViewParams}
|
||||
*/
|
||||
scrollMatchIntoView({
|
||||
element = null,
|
||||
selectedLeft = 0,
|
||||
|
@ -528,7 +561,7 @@ class PDFFindController {
|
|||
this._pageMatches = [];
|
||||
this._pageMatchesLength = [];
|
||||
this.#visitedPagesCount = 0;
|
||||
this._state = null;
|
||||
this.#state = null;
|
||||
// Currently selected match.
|
||||
this._selected = {
|
||||
pageIdx: -1,
|
||||
|
@ -552,26 +585,47 @@ class PDFFindController {
|
|||
clearTimeout(this._findTimeout);
|
||||
this._findTimeout = null;
|
||||
|
||||
this._firstPageCapability = createPromiseCapability();
|
||||
this._firstPageCapability = Promise.withResolvers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {string} The (current) normalized search query.
|
||||
* @type {string|Array} The (current) normalized search query.
|
||||
*/
|
||||
get #query() {
|
||||
if (this._state.query !== this._rawQuery) {
|
||||
this._rawQuery = this._state.query;
|
||||
[this._normalizedQuery] = normalize(this._state.query);
|
||||
const { query } = this.#state;
|
||||
if (typeof query === "string") {
|
||||
if (query !== this._rawQuery) {
|
||||
this._rawQuery = query;
|
||||
[this._normalizedQuery] = normalize(query);
|
||||
}
|
||||
return this._normalizedQuery;
|
||||
}
|
||||
return this._normalizedQuery;
|
||||
// We don't bother caching the normalized search query in the Array-case,
|
||||
// since this code-path is *essentially* unused in the default viewer.
|
||||
return (query || []).filter(q => !!q).map(q => normalize(q)[0]);
|
||||
}
|
||||
|
||||
#shouldDirtyMatch(state) {
|
||||
// When the search query changes, regardless of the actual search command
|
||||
// used, always re-calculate matches to avoid errors (fixes bug 1030622).
|
||||
if (state.query !== this._state.query) {
|
||||
const newQuery = state.query,
|
||||
prevQuery = this.#state.query;
|
||||
const newType = typeof newQuery,
|
||||
prevType = typeof prevQuery;
|
||||
|
||||
if (newType !== prevType) {
|
||||
return true;
|
||||
}
|
||||
if (newType === "string") {
|
||||
if (newQuery !== prevQuery) {
|
||||
return true;
|
||||
}
|
||||
} else if (
|
||||
/* isArray && */ JSON.stringify(newQuery) !== JSON.stringify(prevQuery)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (state.type) {
|
||||
case "again":
|
||||
const pageNumber = this._selected.pageIdx + 1;
|
||||
|
@ -584,15 +638,12 @@ class PDFFindController {
|
|||
// there's a risk that consecutive 'findagain' operations could "skip"
|
||||
// over matches at the top/bottom of pages thus making them completely
|
||||
// inaccessible when there's multiple pages visible in the viewer.
|
||||
if (
|
||||
return (
|
||||
pageNumber >= 1 &&
|
||||
pageNumber <= linkService.pagesCount &&
|
||||
pageNumber !== linkService.page &&
|
||||
!linkService.isPageVisible(pageNumber)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
!(this.onIsPageVisible?.(pageNumber) ?? true)
|
||||
);
|
||||
case "highlightallchange":
|
||||
return false;
|
||||
}
|
||||
|
@ -629,39 +680,8 @@ class PDFFindController {
|
|||
return true;
|
||||
}
|
||||
|
||||
#calculateRegExpMatch(query, entireWord, pageIndex, pageContent) {
|
||||
const matches = (this._pageMatches[pageIndex] = []);
|
||||
const matchesLength = (this._pageMatchesLength[pageIndex] = []);
|
||||
if (!query) {
|
||||
// The query can be empty because some chars like diacritics could have
|
||||
// been stripped out.
|
||||
return;
|
||||
}
|
||||
const diffs = this._pageDiffs[pageIndex];
|
||||
let match;
|
||||
while ((match = query.exec(pageContent)) !== null) {
|
||||
if (
|
||||
entireWord &&
|
||||
!this.#isEntireWord(pageContent, match.index, match[0].length)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [matchPos, matchLen] = getOriginalIndex(
|
||||
diffs,
|
||||
match.index,
|
||||
match[0].length
|
||||
);
|
||||
|
||||
if (matchLen) {
|
||||
matches.push(matchPos);
|
||||
matchesLength.push(matchLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#convertToRegExpString(query, hasDiacritics) {
|
||||
const { matchDiacritics } = this._state;
|
||||
const { matchDiacritics } = this.#state;
|
||||
let isUnicode = false;
|
||||
query = query.replaceAll(
|
||||
SPECIAL_CHARS_REG_EXP,
|
||||
|
@ -731,47 +751,28 @@ class PDFFindController {
|
|||
}
|
||||
|
||||
#calculateMatch(pageIndex) {
|
||||
let query = this.#query;
|
||||
if (!query) {
|
||||
// Do nothing: the matches should be wiped out already.
|
||||
return;
|
||||
const query = this.#query;
|
||||
if (query.length === 0) {
|
||||
return; // Do nothing: the matches should be wiped out already.
|
||||
}
|
||||
|
||||
const { caseSensitive, entireWord, phraseSearch } = this._state;
|
||||
const pageContent = this._pageContents[pageIndex];
|
||||
const hasDiacritics = this._hasDiacritics[pageIndex];
|
||||
const matcherResult = this.match(query, pageContent, pageIndex);
|
||||
|
||||
let isUnicode = false;
|
||||
if (phraseSearch) {
|
||||
[isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics);
|
||||
} else {
|
||||
// Words are sorted in reverse order to be sure that "foobar" is matched
|
||||
// before "foo" in case the query is "foobar foo".
|
||||
const match = query.match(/\S+/g);
|
||||
if (match) {
|
||||
query = match
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(q => {
|
||||
const [isUnicodePart, queryPart] = this.#convertToRegExpString(
|
||||
q,
|
||||
hasDiacritics
|
||||
);
|
||||
isUnicode ||= isUnicodePart;
|
||||
return `(${queryPart})`;
|
||||
})
|
||||
.join("|");
|
||||
const matches = (this._pageMatches[pageIndex] = []);
|
||||
const matchesLength = (this._pageMatchesLength[pageIndex] = []);
|
||||
const diffs = this._pageDiffs[pageIndex];
|
||||
|
||||
matcherResult?.forEach(({ index, length }) => {
|
||||
const [matchPos, matchLen] = getOriginalIndex(diffs, index, length);
|
||||
if (matchLen) {
|
||||
matches.push(matchPos);
|
||||
matchesLength.push(matchLen);
|
||||
}
|
||||
}
|
||||
|
||||
const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`;
|
||||
query = query ? new RegExp(query, flags) : null;
|
||||
|
||||
this.#calculateRegExpMatch(query, entireWord, pageIndex, pageContent);
|
||||
});
|
||||
|
||||
// When `highlightAll` is set, ensure that the matches on previously
|
||||
// rendered (and still active) pages are correctly highlighted.
|
||||
if (this._state.highlightAll) {
|
||||
if (this.#state.highlightAll) {
|
||||
this.#updatePage(pageIndex);
|
||||
}
|
||||
if (this._resumePageIdx === pageIndex) {
|
||||
|
@ -780,7 +781,7 @@ class PDFFindController {
|
|||
}
|
||||
|
||||
// Update the match count.
|
||||
const pageMatchesCount = this._pageMatches[pageIndex].length;
|
||||
const pageMatchesCount = matches.length;
|
||||
this._matchesCountTotal += pageMatchesCount;
|
||||
if (this.#updateMatchesCountOnProgress) {
|
||||
if (pageMatchesCount > 0) {
|
||||
|
@ -793,23 +794,83 @@ class PDFFindController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} FindMatch
|
||||
* @property {number} index - The start of the matched text in the page's
|
||||
* string contents.
|
||||
* @property {number} length - The length of the matched text.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string | string[]} query - The search query.
|
||||
* @param {string} pageContent - The text content of the page to search in.
|
||||
* @param {number} pageIndex - The index of the page that is being processed.
|
||||
* @returns {FindMatch[] | undefined} An array of matches in the provided
|
||||
* page.
|
||||
*/
|
||||
match(query, pageContent, pageIndex) {
|
||||
const hasDiacritics = this._hasDiacritics[pageIndex];
|
||||
|
||||
let isUnicode = false;
|
||||
if (typeof query === "string") {
|
||||
[isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics);
|
||||
} else {
|
||||
// Words are sorted in reverse order to be sure that "foobar" is matched
|
||||
// before "foo" in case the query is "foobar foo".
|
||||
query = query
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(q => {
|
||||
const [isUnicodePart, queryPart] = this.#convertToRegExpString(
|
||||
q,
|
||||
hasDiacritics
|
||||
);
|
||||
isUnicode ||= isUnicodePart;
|
||||
return `(${queryPart})`;
|
||||
})
|
||||
.join("|");
|
||||
}
|
||||
if (!query) {
|
||||
// The query can be empty because some chars like diacritics could have
|
||||
// been stripped out.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { caseSensitive, entireWord } = this.#state;
|
||||
const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`;
|
||||
query = new RegExp(query, flags);
|
||||
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = query.exec(pageContent)) !== null) {
|
||||
if (
|
||||
entireWord &&
|
||||
!this.#isEntireWord(pageContent, match.index, match[0].length)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
matches.push({ index: match.index, length: match[0].length });
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
#extractText() {
|
||||
// Perform text extraction once if this method is called multiple times.
|
||||
if (this._extractTextPromises.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promise = Promise.resolve();
|
||||
let deferred = Promise.resolve();
|
||||
const textOptions = { disableNormalization: true };
|
||||
for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) {
|
||||
const extractTextCapability = createPromiseCapability();
|
||||
this._extractTextPromises[i] = extractTextCapability.promise;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
this._extractTextPromises[i] = promise;
|
||||
|
||||
promise = promise.then(() => {
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
deferred = deferred.then(() => {
|
||||
return this._pdfDocument
|
||||
.getPage(i + 1)
|
||||
.then(pdfPage => {
|
||||
return pdfPage.getTextContent();
|
||||
})
|
||||
.then(pdfPage => pdfPage.getTextContent(textOptions))
|
||||
.then(
|
||||
textContent => {
|
||||
const strBuf = [];
|
||||
|
@ -827,7 +888,7 @@ class PDFFindController {
|
|||
this._pageDiffs[i],
|
||||
this._hasDiacritics[i],
|
||||
] = normalize(strBuf.join(""));
|
||||
extractTextCapability.resolve();
|
||||
resolve();
|
||||
},
|
||||
reason => {
|
||||
console.error(
|
||||
|
@ -838,7 +899,7 @@ class PDFFindController {
|
|||
this._pageContents[i] = "";
|
||||
this._pageDiffs[i] = null;
|
||||
this._hasDiacritics[i] = false;
|
||||
extractTextCapability.resolve();
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -867,7 +928,7 @@ class PDFFindController {
|
|||
}
|
||||
|
||||
#nextMatch() {
|
||||
const previous = this._state.findPrevious;
|
||||
const previous = this.#state.findPrevious;
|
||||
const currentPageIndex = this._linkService.page - 1;
|
||||
const numPages = this._linkService.pagesCount;
|
||||
|
||||
|
@ -902,7 +963,8 @@ class PDFFindController {
|
|||
}
|
||||
|
||||
// If there's no query there's no point in searching.
|
||||
if (!this.#query) {
|
||||
const query = this.#query;
|
||||
if (query.length === 0) {
|
||||
this.#updateUIState(FindState.FOUND);
|
||||
return;
|
||||
}
|
||||
|
@ -939,7 +1001,7 @@ class PDFFindController {
|
|||
#matchesReady(matches) {
|
||||
const offset = this._offset;
|
||||
const numMatches = matches.length;
|
||||
const previous = this._state.findPrevious;
|
||||
const previous = this.#state.findPrevious;
|
||||
|
||||
if (numMatches) {
|
||||
// There were matches for the page, so initialize `matchIdx`.
|
||||
|
@ -1012,7 +1074,7 @@ class PDFFindController {
|
|||
}
|
||||
}
|
||||
|
||||
this.#updateUIState(state, this._state.findPrevious);
|
||||
this.#updateUIState(state, this.#state.findPrevious);
|
||||
if (this._selected.pageIdx !== -1) {
|
||||
// Ensure that the match will be scrolled into view.
|
||||
this._scrollMatches = true;
|
||||
|
@ -1066,7 +1128,7 @@ class PDFFindController {
|
|||
current += matchIdx + 1;
|
||||
}
|
||||
// When searching starts, this method may be called before the `pageMatches`
|
||||
// have been counted (in `_calculateMatch`). Ensure that the UI won't show
|
||||
// have been counted (in `#calculateMatch`). Ensure that the UI won't show
|
||||
// temporarily broken state when the active find result doesn't make sense.
|
||||
if (current < 1 || current > total) {
|
||||
current = total = 0;
|
||||
|
@ -1096,8 +1158,9 @@ class PDFFindController {
|
|||
source: this,
|
||||
state,
|
||||
previous,
|
||||
entireWord: this.#state?.entireWord ?? null,
|
||||
matchesCount: this.#requestMatchesCount(),
|
||||
rawQuery: this._state?.query ?? null,
|
||||
rawQuery: this.#state?.query ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,4 +112,46 @@ function getCharacterType(charCode) {
|
|||
return CharacterType.ALPHA_LETTER;
|
||||
}
|
||||
|
||||
export { CharacterType, getCharacterType };
|
||||
let NormalizeWithNFKC;
|
||||
function getNormalizeWithNFKC() {
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
NormalizeWithNFKC ||= ` ¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰꟲ-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`;
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
const ranges = [];
|
||||
const range = [];
|
||||
const diacriticsRegex = /^\p{M}$/u;
|
||||
// Some chars must be replaced by their NFKC counterpart during a search.
|
||||
for (let i = 0; i < 65536; i++) {
|
||||
const c = String.fromCharCode(i);
|
||||
if (c.normalize("NFKC") !== c && !diacriticsRegex.test(c)) {
|
||||
if (range.length !== 2) {
|
||||
range[0] = range[1] = i;
|
||||
continue;
|
||||
}
|
||||
if (range[1] + 1 !== i) {
|
||||
if (range[0] === range[1]) {
|
||||
ranges.push(String.fromCharCode(range[0]));
|
||||
} else {
|
||||
ranges.push(
|
||||
`${String.fromCharCode(range[0])}-${String.fromCharCode(
|
||||
range[1]
|
||||
)}`
|
||||
);
|
||||
}
|
||||
range[0] = range[1] = i;
|
||||
} else {
|
||||
range[1] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ranges.join("") !== NormalizeWithNFKC) {
|
||||
throw new Error(
|
||||
"getNormalizeWithNFKC - update the `NormalizeWithNFKC` string."
|
||||
);
|
||||
}
|
||||
}
|
||||
return NormalizeWithNFKC;
|
||||
}
|
||||
|
||||
export { CharacterType, getCharacterType, getNormalizeWithNFKC };
|
||||
|
|
|
@ -53,6 +53,8 @@ function getCurrentHash() {
|
|||
}
|
||||
|
||||
class PDFHistory {
|
||||
#eventAbortController = null;
|
||||
|
||||
/**
|
||||
* @param {PDFHistoryOptions} options
|
||||
*/
|
||||
|
@ -64,7 +66,6 @@ class PDFHistory {
|
|||
this._fingerprint = "";
|
||||
this.reset();
|
||||
|
||||
this._boundEvents = null;
|
||||
// Ensure that we don't miss a "pagesinit" event,
|
||||
// by registering the listener immediately.
|
||||
this.eventBus._on("pagesinit", () => {
|
||||
|
@ -102,7 +103,7 @@ class PDFHistory {
|
|||
this._updateUrl = updateUrl === true;
|
||||
|
||||
this._initialized = true;
|
||||
this._bindEvents();
|
||||
this.#bindEvents();
|
||||
const state = window.history.state;
|
||||
|
||||
this._popStateInProgress = false;
|
||||
|
@ -114,19 +115,19 @@ class PDFHistory {
|
|||
this._destination = null;
|
||||
this._position = null;
|
||||
|
||||
if (!this._isValidState(state, /* checkReload = */ true) || resetHistory) {
|
||||
const { hash, page, rotation } = this._parseCurrentHash(
|
||||
if (!this.#isValidState(state, /* checkReload = */ true) || resetHistory) {
|
||||
const { hash, page, rotation } = this.#parseCurrentHash(
|
||||
/* checkNameddest = */ true
|
||||
);
|
||||
|
||||
if (!hash || reInitialized || resetHistory) {
|
||||
// Ensure that the browser history is reset on PDF document load.
|
||||
this._pushOrReplaceState(null, /* forceReplace = */ true);
|
||||
this.#pushOrReplaceState(null, /* forceReplace = */ true);
|
||||
return;
|
||||
}
|
||||
// Ensure that the browser history is initialized correctly when
|
||||
// the document hash is present on PDF document load.
|
||||
this._pushOrReplaceState(
|
||||
this.#pushOrReplaceState(
|
||||
{ hash, page, rotation },
|
||||
/* forceReplace = */ true
|
||||
);
|
||||
|
@ -136,7 +137,7 @@ class PDFHistory {
|
|||
// The browser history contains a valid entry, ensure that the history is
|
||||
// initialized correctly on PDF document load.
|
||||
const destination = state.destination;
|
||||
this._updateInternalState(
|
||||
this.#updateInternalState(
|
||||
destination,
|
||||
state.uid,
|
||||
/* removeTemporary = */ true
|
||||
|
@ -166,10 +167,10 @@ class PDFHistory {
|
|||
*/
|
||||
reset() {
|
||||
if (this._initialized) {
|
||||
this._pageHide(); // Simulate a 'pagehide' event when resetting.
|
||||
this.#pageHide(); // Simulate a 'pagehide' event when resetting.
|
||||
|
||||
this._initialized = false;
|
||||
this._unbindEvents();
|
||||
this.#unbindEvents();
|
||||
}
|
||||
if (this._updateViewareaTimeout) {
|
||||
clearTimeout(this._updateViewareaTimeout);
|
||||
|
@ -199,7 +200,7 @@ class PDFHistory {
|
|||
`"${explicitDest}" is not a valid explicitDest parameter.`
|
||||
);
|
||||
return;
|
||||
} else if (!this._isValidPage(pageNumber)) {
|
||||
} else if (!this.#isValidPage(pageNumber)) {
|
||||
// Allow an unset `pageNumber` if and only if the history is still empty;
|
||||
// please refer to the `this._destination.page = null;` comment above.
|
||||
if (pageNumber !== null || this._destination) {
|
||||
|
@ -238,7 +239,7 @@ class PDFHistory {
|
|||
return;
|
||||
}
|
||||
|
||||
this._pushOrReplaceState(
|
||||
this.#pushOrReplaceState(
|
||||
{
|
||||
dest: explicitDest,
|
||||
hash,
|
||||
|
@ -269,7 +270,7 @@ class PDFHistory {
|
|||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
if (!this._isValidPage(pageNumber)) {
|
||||
if (!this.#isValidPage(pageNumber)) {
|
||||
console.error(
|
||||
`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`
|
||||
);
|
||||
|
@ -285,8 +286,8 @@ class PDFHistory {
|
|||
return;
|
||||
}
|
||||
|
||||
this._pushOrReplaceState({
|
||||
// Simulate an internal destination, for `this._tryPushCurrentPosition`:
|
||||
this.#pushOrReplaceState({
|
||||
// Simulate an internal destination, for `this.#tryPushCurrentPosition`:
|
||||
dest: null,
|
||||
hash: `page=${pageNumber}`,
|
||||
page: pageNumber,
|
||||
|
@ -312,7 +313,7 @@ class PDFHistory {
|
|||
if (!this._initialized || this._popStateInProgress) {
|
||||
return;
|
||||
}
|
||||
this._tryPushCurrentPosition();
|
||||
this.#tryPushCurrentPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -324,7 +325,7 @@ class PDFHistory {
|
|||
return;
|
||||
}
|
||||
const state = window.history.state;
|
||||
if (this._isValidState(state) && state.uid > 0) {
|
||||
if (this.#isValidState(state) && state.uid > 0) {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
@ -338,7 +339,7 @@ class PDFHistory {
|
|||
return;
|
||||
}
|
||||
const state = window.history.state;
|
||||
if (this._isValidState(state) && state.uid < this._maxUid) {
|
||||
if (this.#isValidState(state) && state.uid < this._maxUid) {
|
||||
window.history.forward();
|
||||
}
|
||||
}
|
||||
|
@ -362,10 +363,7 @@ class PDFHistory {
|
|||
return this._initialized ? this._initialRotation : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_pushOrReplaceState(destination, forceReplace = false) {
|
||||
#pushOrReplaceState(destination, forceReplace = false) {
|
||||
const shouldReplace = forceReplace || !this._destination;
|
||||
const newState = {
|
||||
fingerprint: this._fingerprint,
|
||||
|
@ -381,11 +379,11 @@ class PDFHistory {
|
|||
// history.state.chromecomState is managed by chromecom.js.
|
||||
newState.chromecomState = window.history.state.chromecomState;
|
||||
}
|
||||
this._updateInternalState(destination, newState.uid);
|
||||
this.#updateInternalState(destination, newState.uid);
|
||||
|
||||
let newUrl;
|
||||
if (this._updateUrl && destination?.hash) {
|
||||
const baseUrl = document.location.href.split("#")[0];
|
||||
const baseUrl = document.location.href.split("#", 1)[0];
|
||||
// Prevent errors in Firefox.
|
||||
if (!baseUrl.startsWith("file://")) {
|
||||
newUrl = `${baseUrl}#${destination.hash}`;
|
||||
|
@ -396,21 +394,9 @@ class PDFHistory {
|
|||
} else {
|
||||
window.history.pushState(newState, "", newUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("CHROME") &&
|
||||
top === window
|
||||
) {
|
||||
// eslint-disable-next-line no-undef
|
||||
chrome.runtime.sendMessage("showPageAction");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_tryPushCurrentPosition(temporary = false) {
|
||||
#tryPushCurrentPosition(temporary = false) {
|
||||
if (!this._position) {
|
||||
return;
|
||||
}
|
||||
|
@ -421,12 +407,12 @@ class PDFHistory {
|
|||
}
|
||||
|
||||
if (!this._destination) {
|
||||
this._pushOrReplaceState(position);
|
||||
this.#pushOrReplaceState(position);
|
||||
return;
|
||||
}
|
||||
if (this._destination.temporary) {
|
||||
// Always replace a previous *temporary* position.
|
||||
this._pushOrReplaceState(position, /* forceReplace = */ true);
|
||||
this.#pushOrReplaceState(position, /* forceReplace = */ true);
|
||||
return;
|
||||
}
|
||||
if (this._destination.hash === position.hash) {
|
||||
|
@ -460,22 +446,16 @@ class PDFHistory {
|
|||
// To avoid "flooding" the browser history, replace the current entry.
|
||||
forceReplace = true;
|
||||
}
|
||||
this._pushOrReplaceState(position, forceReplace);
|
||||
this.#pushOrReplaceState(position, forceReplace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isValidPage(val) {
|
||||
#isValidPage(val) {
|
||||
return (
|
||||
Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isValidState(state, checkReload = false) {
|
||||
#isValidState(state, checkReload = false) {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
@ -508,10 +488,7 @@ class PDFHistory {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateInternalState(destination, uid, removeTemporary = false) {
|
||||
#updateInternalState(destination, uid, removeTemporary = false) {
|
||||
if (this._updateViewareaTimeout) {
|
||||
// When updating `this._destination`, make sure that we always wait for
|
||||
// the next 'updateviewarea' event before (potentially) attempting to
|
||||
|
@ -531,26 +508,20 @@ class PDFHistory {
|
|||
this._numPositionUpdates = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_parseCurrentHash(checkNameddest = false) {
|
||||
#parseCurrentHash(checkNameddest = false) {
|
||||
const hash = unescape(getCurrentHash()).substring(1);
|
||||
const params = parseQueryString(hash);
|
||||
|
||||
const nameddest = params.get("nameddest") || "";
|
||||
let page = params.get("page") | 0;
|
||||
|
||||
if (!this._isValidPage(page) || (checkNameddest && nameddest.length > 0)) {
|
||||
if (!this.#isValidPage(page) || (checkNameddest && nameddest.length > 0)) {
|
||||
page = null;
|
||||
}
|
||||
return { hash, page, rotation: this.linkService.rotation };
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateViewarea({ location }) {
|
||||
#updateViewarea({ location }) {
|
||||
if (this._updateViewareaTimeout) {
|
||||
clearTimeout(this._updateViewareaTimeout);
|
||||
this._updateViewareaTimeout = null;
|
||||
|
@ -575,9 +546,9 @@ class PDFHistory {
|
|||
) {
|
||||
// If the current destination was set through the user changing the hash
|
||||
// of the document, we will usually not try to push the current position
|
||||
// to the browser history; see `this._tryPushCurrentPosition()`.
|
||||
// to the browser history; see `this.#tryPushCurrentPosition()`.
|
||||
//
|
||||
// To prevent `this._tryPushCurrentPosition()` from effectively being
|
||||
// To prevent `this.#tryPushCurrentPosition()` from effectively being
|
||||
// reduced to a no-op in this case, we will assume that the position
|
||||
// *did* in fact change if the 'updateviewarea' event was dispatched
|
||||
// more than `POSITION_UPDATED_THRESHOLD` times.
|
||||
|
@ -602,17 +573,14 @@ class PDFHistory {
|
|||
// the viewer has been idle for `UPDATE_VIEWAREA_TIMEOUT` milliseconds.
|
||||
this._updateViewareaTimeout = setTimeout(() => {
|
||||
if (!this._popStateInProgress) {
|
||||
this._tryPushCurrentPosition(/* temporary = */ true);
|
||||
this.#tryPushCurrentPosition(/* temporary = */ true);
|
||||
}
|
||||
this._updateViewareaTimeout = null;
|
||||
}, UPDATE_VIEWAREA_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_popState({ state }) {
|
||||
#popState({ state }) {
|
||||
const newHash = getCurrentHash(),
|
||||
hashChanged = this._currentHash !== newHash;
|
||||
this._currentHash = newHash;
|
||||
|
@ -621,20 +589,20 @@ class PDFHistory {
|
|||
(typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("CHROME") &&
|
||||
state?.chromecomState &&
|
||||
!this._isValidState(state)) ||
|
||||
!this.#isValidState(state)) ||
|
||||
!state
|
||||
) {
|
||||
// This case corresponds to the user changing the hash of the document.
|
||||
this._uid++;
|
||||
|
||||
const { hash, page, rotation } = this._parseCurrentHash();
|
||||
this._pushOrReplaceState(
|
||||
const { hash, page, rotation } = this.#parseCurrentHash();
|
||||
this.#pushOrReplaceState(
|
||||
{ hash, page, rotation },
|
||||
/* forceReplace = */ true
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!this._isValidState(state)) {
|
||||
if (!this.#isValidState(state)) {
|
||||
// This should only occur in viewers with support for opening more than
|
||||
// one PDF document, e.g. the GENERIC viewer.
|
||||
return;
|
||||
|
@ -666,7 +634,7 @@ class PDFHistory {
|
|||
|
||||
// Navigate to the new destination.
|
||||
const destination = state.destination;
|
||||
this._updateInternalState(
|
||||
this.#updateInternalState(
|
||||
destination,
|
||||
state.uid,
|
||||
/* removeTemporary = */ true
|
||||
|
@ -691,50 +659,34 @@ class PDFHistory {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_pageHide() {
|
||||
#pageHide() {
|
||||
// Attempt to push the `this._position` into the browser history when
|
||||
// navigating away from the document. This is *only* done if the history
|
||||
// is empty/temporary, since otherwise an existing browser history entry
|
||||
// will end up being overwritten (given that new entries cannot be pushed
|
||||
// into the browser history when the 'unload' event has already fired).
|
||||
if (!this._destination || this._destination.temporary) {
|
||||
this._tryPushCurrentPosition();
|
||||
this.#tryPushCurrentPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_bindEvents() {
|
||||
if (this._boundEvents) {
|
||||
#bindEvents() {
|
||||
if (this.#eventAbortController) {
|
||||
return; // The event listeners were already added.
|
||||
}
|
||||
this._boundEvents = {
|
||||
updateViewarea: this._updateViewarea.bind(this),
|
||||
popState: this._popState.bind(this),
|
||||
pageHide: this._pageHide.bind(this),
|
||||
};
|
||||
this.#eventAbortController = new AbortController();
|
||||
const { signal } = this.#eventAbortController;
|
||||
|
||||
this.eventBus._on("updateviewarea", this._boundEvents.updateViewarea);
|
||||
window.addEventListener("popstate", this._boundEvents.popState);
|
||||
window.addEventListener("pagehide", this._boundEvents.pageHide);
|
||||
this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), {
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("popstate", this.#popState.bind(this), { signal });
|
||||
window.addEventListener("pagehide", this.#pageHide.bind(this), { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_unbindEvents() {
|
||||
if (!this._boundEvents) {
|
||||
return; // The event listeners were already removed.
|
||||
}
|
||||
this.eventBus._off("updateviewarea", this._boundEvents.updateViewarea);
|
||||
window.removeEventListener("popstate", this._boundEvents.popState);
|
||||
window.removeEventListener("pagehide", this._boundEvents.pageHide);
|
||||
|
||||
this._boundEvents = null;
|
||||
#unbindEvents() {
|
||||
this.#eventAbortController?.abort();
|
||||
this.#eventAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,26 +13,30 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/optional_content_config.js").OptionalContentConfig} OptionalContentConfig */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
|
||||
|
||||
import { BaseTreeViewer } from "./base_tree_viewer.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFLayerViewerOptions
|
||||
* @property {HTMLDivElement} container - The viewer element.
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {IL10n} l10n - Localization service.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFLayerViewerRenderParameters
|
||||
* @property {OptionalContentConfig|null} optionalContentConfig - An
|
||||
* {OptionalContentConfig} instance.
|
||||
* @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
|
||||
* @property {PDFDocumentProxy} pdfDocument - A {PDFDocument} instance.
|
||||
*/
|
||||
|
||||
class PDFLayerViewer extends BaseTreeViewer {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.l10n = options.l10n;
|
||||
|
||||
this.eventBus._on("optionalcontentconfigchanged", evt => {
|
||||
this.#updateLayers(evt.promise);
|
||||
|
@ -46,11 +50,13 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
reset() {
|
||||
super.reset();
|
||||
this._optionalContentConfig = null;
|
||||
this._optionalContentHash = null;
|
||||
|
||||
this._optionalContentVisibility?.clear();
|
||||
this._optionalContentVisibility = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_dispatchEvent(layersCount) {
|
||||
this.eventBus.dispatch("layersloaded", {
|
||||
|
@ -60,12 +66,17 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_bindLink(element, { groupId, input }) {
|
||||
const setVisibility = () => {
|
||||
this._optionalContentConfig.setVisibility(groupId, input.checked);
|
||||
this._optionalContentHash = this._optionalContentConfig.getHash();
|
||||
const visible = input.checked;
|
||||
this._optionalContentConfig.setVisibility(groupId, visible);
|
||||
|
||||
const cached = this._optionalContentVisibility.get(groupId);
|
||||
if (cached) {
|
||||
cached.visible = visible;
|
||||
}
|
||||
|
||||
this.eventBus.dispatch("optionalcontentconfig", {
|
||||
source: this,
|
||||
|
@ -89,18 +100,22 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
/**
|
||||
* @private
|
||||
*/
|
||||
async _setNestedName(element, { name = null }) {
|
||||
_setNestedName(element, { name = null }) {
|
||||
if (typeof name === "string") {
|
||||
element.textContent = this._normalizeTextContent(name);
|
||||
return;
|
||||
}
|
||||
// NOTE
|
||||
element.textContent = window.siyuan.languages.additionalLayers
|
||||
// element.setAttribute("data-l10n-id", "pdfjs-additional-layers");
|
||||
element.style.fontStyle = "italic";
|
||||
// Trigger translation manually, since translation is paused when
|
||||
// the final layer-tree is appended to the DOM.
|
||||
this._l10n.translateOnce(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_addToggleButton(div, { name = null }) {
|
||||
super._addToggleButton(div, /* hidden = */ name === null);
|
||||
|
@ -131,7 +146,7 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
this._dispatchEvent(/* layersCount = */ 0);
|
||||
return;
|
||||
}
|
||||
this._optionalContentHash = optionalContentConfig.getHash();
|
||||
this._optionalContentVisibility = new Map();
|
||||
|
||||
const fragment = document.createDocumentFragment(),
|
||||
queue = [{ parent: fragment, groups }];
|
||||
|
@ -164,6 +179,11 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
input.type = "checkbox";
|
||||
input.checked = group.visible;
|
||||
|
||||
this._optionalContentVisibility.set(groupId, {
|
||||
input,
|
||||
visible: input.checked,
|
||||
});
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = this._normalizeTextContent(group.name);
|
||||
|
||||
|
@ -185,21 +205,26 @@ class PDFLayerViewer extends BaseTreeViewer {
|
|||
}
|
||||
const pdfDocument = this._pdfDocument;
|
||||
const optionalContentConfig = await (promise ||
|
||||
pdfDocument.getOptionalContentConfig());
|
||||
pdfDocument.getOptionalContentConfig({ intent: "display" }));
|
||||
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
return; // The document was closed while the optional content resolved.
|
||||
}
|
||||
if (promise) {
|
||||
if (optionalContentConfig.getHash() === this._optionalContentHash) {
|
||||
return; // The optional content didn't change, hence no need to reset the UI.
|
||||
// Ensure that the UI displays the correct state (e.g. with RBGroups).
|
||||
for (const [groupId, cached] of this._optionalContentVisibility) {
|
||||
const group = optionalContentConfig.getGroup(groupId);
|
||||
|
||||
if (group && cached.visible !== group.visible) {
|
||||
cached.input.checked = cached.visible = !cached.visible;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.eventBus.dispatch("optionalcontentconfig", {
|
||||
source: this,
|
||||
promise: Promise.resolve(optionalContentConfig),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.eventBus.dispatch("optionalcontentconfig", {
|
||||
source: this,
|
||||
promise: Promise.resolve(optionalContentConfig),
|
||||
});
|
||||
|
||||
// Reset the sidebarView to the new state.
|
||||
this.render({
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
|
||||
import { parseQueryString, removeNullCharacters } from "./ui_utils.js";
|
||||
import { parseQueryString } from "./ui_utils.js";
|
||||
|
||||
const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
|
||||
|
||||
|
@ -28,60 +28,6 @@ const LinkTarget = {
|
|||
TOP: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExternalLinkParameters
|
||||
* @property {string} url - An absolute URL.
|
||||
* @property {LinkTarget} [target] - The link target. The default value is
|
||||
* `LinkTarget.NONE`.
|
||||
* @property {string} [rel] - The link relationship. The default value is
|
||||
* `DEFAULT_LINK_REL`.
|
||||
* @property {boolean} [enabled] - Whether the link should be enabled. The
|
||||
* default value is true.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds various attributes (href, title, target, rel) to hyperlinks.
|
||||
* @param {HTMLAnchorElement} link - The link element.
|
||||
* @param {ExternalLinkParameters} params
|
||||
*/
|
||||
function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) {
|
||||
if (!url || typeof url !== "string") {
|
||||
throw new Error('A valid "url" parameter must provided.');
|
||||
}
|
||||
|
||||
const urlNullRemoved = removeNullCharacters(url);
|
||||
if (enabled) {
|
||||
link.href = link.title = urlNullRemoved;
|
||||
} else {
|
||||
link.href = "";
|
||||
link.title = `Disabled: ${urlNullRemoved}`;
|
||||
link.onclick = () => {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
let targetStr = ""; // LinkTarget.NONE
|
||||
switch (target) {
|
||||
case LinkTarget.NONE:
|
||||
break;
|
||||
case LinkTarget.SELF:
|
||||
targetStr = "_self";
|
||||
break;
|
||||
case LinkTarget.BLANK:
|
||||
targetStr = "_blank";
|
||||
break;
|
||||
case LinkTarget.PARENT:
|
||||
targetStr = "_parent";
|
||||
break;
|
||||
case LinkTarget.TOP:
|
||||
targetStr = "_top";
|
||||
break;
|
||||
}
|
||||
link.target = targetStr;
|
||||
|
||||
link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFLinkServiceOptions
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
|
@ -101,7 +47,7 @@ function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) {
|
|||
* @implements {IPDFLinkService}
|
||||
*/
|
||||
class PDFLinkService {
|
||||
#pagesRefCache = new Map();
|
||||
externalLinkEnabled = true;
|
||||
|
||||
/**
|
||||
* @param {PDFLinkServiceOptions} options
|
||||
|
@ -115,7 +61,6 @@ class PDFLinkService {
|
|||
this.eventBus = eventBus;
|
||||
this.externalLinkTarget = externalLinkTarget;
|
||||
this.externalLinkRel = externalLinkRel;
|
||||
this.externalLinkEnabled = true;
|
||||
this._ignoreDestinationZoom = ignoreDestinationZoom;
|
||||
|
||||
this.baseUrl = null;
|
||||
|
@ -127,7 +72,6 @@ class PDFLinkService {
|
|||
setDocument(pdfDocument, baseUrl = null) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.#pagesRefCache.clear();
|
||||
}
|
||||
|
||||
setViewer(pdfViewer) {
|
||||
|
@ -149,75 +93,88 @@ class PDFLinkService {
|
|||
* @type {number}
|
||||
*/
|
||||
get page() {
|
||||
return this.pdfViewer.currentPageNumber;
|
||||
return this.pdfDocument ? this.pdfViewer.currentPageNumber : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
set page(value) {
|
||||
this.pdfViewer.currentPageNumber = value;
|
||||
if (this.pdfDocument) {
|
||||
this.pdfViewer.currentPageNumber = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get rotation() {
|
||||
return this.pdfViewer.pagesRotation;
|
||||
return this.pdfDocument ? this.pdfViewer.pagesRotation : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
set rotation(value) {
|
||||
this.pdfViewer.pagesRotation = value;
|
||||
if (this.pdfDocument) {
|
||||
this.pdfViewer.pagesRotation = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isInPresentationMode() {
|
||||
return this.pdfViewer.isInPresentationMode;
|
||||
return this.pdfDocument ? this.pdfViewer.isInPresentationMode : false;
|
||||
}
|
||||
|
||||
#goToDestinationHelper(rawDest, namedDest = null, explicitDest) {
|
||||
/**
|
||||
* This method will, when available, also update the browser history.
|
||||
*
|
||||
* @param {string|Array} dest - The named, or explicit, PDF destination.
|
||||
*/
|
||||
async goToDestination(dest) {
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
let namedDest, explicitDest, pageNumber;
|
||||
if (typeof dest === "string") {
|
||||
namedDest = dest;
|
||||
explicitDest = await this.pdfDocument.getDestination(dest);
|
||||
} else {
|
||||
namedDest = null;
|
||||
explicitDest = await dest;
|
||||
}
|
||||
if (!Array.isArray(explicitDest)) {
|
||||
console.error(
|
||||
`goToDestination: "${explicitDest}" is not a valid destination array, for dest="${dest}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Dest array looks like that: <page-ref> </XYZ|/FitXXX> <args..>
|
||||
const destRef = explicitDest[0];
|
||||
let pageNumber;
|
||||
const [destRef] = explicitDest;
|
||||
|
||||
if (typeof destRef === "object" && destRef !== null) {
|
||||
pageNumber = this._cachedPageNumber(destRef);
|
||||
if (destRef && typeof destRef === "object") {
|
||||
pageNumber = this.pdfDocument.cachedPageNumber(destRef);
|
||||
|
||||
if (!pageNumber) {
|
||||
// Fetch the page reference if it's not yet available. This could
|
||||
// only occur during loading, before all pages have been resolved.
|
||||
this.pdfDocument
|
||||
.getPageIndex(destRef)
|
||||
.then(pageIndex => {
|
||||
this.cachePageRef(pageIndex + 1, destRef);
|
||||
this.#goToDestinationHelper(rawDest, namedDest, explicitDest);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(
|
||||
`PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +
|
||||
`a valid page reference, for dest="${rawDest}".`
|
||||
);
|
||||
});
|
||||
return;
|
||||
try {
|
||||
pageNumber = (await this.pdfDocument.getPageIndex(destRef)) + 1;
|
||||
} catch {
|
||||
console.error(
|
||||
`goToDestination: "${destRef}" is not a valid page reference, for dest="${dest}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (Number.isInteger(destRef)) {
|
||||
pageNumber = destRef + 1;
|
||||
} else {
|
||||
console.error(
|
||||
`PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +
|
||||
`a valid destination reference, for dest="${rawDest}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) {
|
||||
console.error(
|
||||
`PDFLinkService.#goToDestinationHelper: "${pageNumber}" is not ` +
|
||||
`a valid page number, for dest="${rawDest}".`
|
||||
`goToDestination: "${pageNumber}" is not a valid page number, for dest="${dest}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -236,33 +193,6 @@ class PDFLinkService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will, when available, also update the browser history.
|
||||
*
|
||||
* @param {string|Array} dest - The named, or explicit, PDF destination.
|
||||
*/
|
||||
async goToDestination(dest) {
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
let namedDest, explicitDest;
|
||||
if (typeof dest === "string") {
|
||||
namedDest = dest;
|
||||
explicitDest = await this.pdfDocument.getDestination(dest);
|
||||
} else {
|
||||
namedDest = null;
|
||||
explicitDest = await dest;
|
||||
}
|
||||
if (!Array.isArray(explicitDest)) {
|
||||
console.error(
|
||||
`PDFLinkService.goToDestination: "${explicitDest}" is not ` +
|
||||
`a valid destination array, for dest="${dest}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#goToDestinationHelper(dest, namedDest, explicitDest);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will, when available, also update the browser history.
|
||||
*
|
||||
|
@ -297,18 +227,46 @@ class PDFLinkService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the `addLinkAttributes` helper function.
|
||||
* Adds various attributes (href, title, target, rel) to hyperlinks.
|
||||
* @param {HTMLAnchorElement} link
|
||||
* @param {string} url
|
||||
* @param {boolean} [newWindow]
|
||||
*/
|
||||
addLinkAttributes(link, url, newWindow = false) {
|
||||
addLinkAttributes(link, {
|
||||
url,
|
||||
target: newWindow ? LinkTarget.BLANK : this.externalLinkTarget,
|
||||
rel: this.externalLinkRel,
|
||||
enabled: this.externalLinkEnabled,
|
||||
});
|
||||
if (!url || typeof url !== "string") {
|
||||
throw new Error('A valid "url" parameter must provided.');
|
||||
}
|
||||
const target = newWindow ? LinkTarget.BLANK : this.externalLinkTarget,
|
||||
rel = this.externalLinkRel;
|
||||
|
||||
if (this.externalLinkEnabled) {
|
||||
link.href = link.title = url;
|
||||
} else {
|
||||
link.href = "";
|
||||
link.title = `Disabled: ${url}`;
|
||||
link.onclick = () => false;
|
||||
}
|
||||
|
||||
let targetStr = ""; // LinkTarget.NONE
|
||||
switch (target) {
|
||||
case LinkTarget.NONE:
|
||||
break;
|
||||
case LinkTarget.SELF:
|
||||
targetStr = "_self";
|
||||
break;
|
||||
case LinkTarget.BLANK:
|
||||
targetStr = "_blank";
|
||||
break;
|
||||
case LinkTarget.PARENT:
|
||||
targetStr = "_parent";
|
||||
break;
|
||||
case LinkTarget.TOP:
|
||||
targetStr = "_top";
|
||||
break;
|
||||
}
|
||||
link.target = targetStr;
|
||||
|
||||
link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -350,10 +308,12 @@ class PDFLinkService {
|
|||
if (hash.includes("=")) {
|
||||
const params = parseQueryString(hash);
|
||||
if (params.has("search")) {
|
||||
const query = params.get("search").replaceAll('"', ""),
|
||||
phrase = params.get("phrase") === "true";
|
||||
|
||||
this.eventBus.dispatch("findfromurlhash", {
|
||||
source: this,
|
||||
query: params.get("search").replaceAll('"', ""),
|
||||
phraseSearch: params.get("phrase") === "true",
|
||||
query: phrase ? query : query.match(/\S+/g),
|
||||
});
|
||||
}
|
||||
// borrowing syntax from "Parameters for Opening PDF Files"
|
||||
|
@ -376,40 +336,38 @@ class PDFLinkService {
|
|||
zoomArgs.length > 2 ? zoomArgs[2] | 0 : null,
|
||||
zoomArgNumber ? zoomArgNumber / 100 : zoomArg,
|
||||
];
|
||||
} else {
|
||||
if (zoomArg === "Fit" || zoomArg === "FitB") {
|
||||
dest = [null, { name: zoomArg }];
|
||||
} else if (
|
||||
zoomArg === "FitH" ||
|
||||
zoomArg === "FitBH" ||
|
||||
zoomArg === "FitV" ||
|
||||
zoomArg === "FitBV"
|
||||
) {
|
||||
} else if (zoomArg === "Fit" || zoomArg === "FitB") {
|
||||
dest = [null, { name: zoomArg }];
|
||||
} else if (
|
||||
zoomArg === "FitH" ||
|
||||
zoomArg === "FitBH" ||
|
||||
zoomArg === "FitV" ||
|
||||
zoomArg === "FitBV"
|
||||
) {
|
||||
dest = [
|
||||
null,
|
||||
{ name: zoomArg },
|
||||
zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,
|
||||
];
|
||||
} else if (zoomArg === "FitR") {
|
||||
if (zoomArgs.length !== 5) {
|
||||
console.error(
|
||||
'PDFLinkService.setHash: Not enough parameters for "FitR".'
|
||||
);
|
||||
} else {
|
||||
dest = [
|
||||
null,
|
||||
{ name: zoomArg },
|
||||
zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,
|
||||
zoomArgs[1] | 0,
|
||||
zoomArgs[2] | 0,
|
||||
zoomArgs[3] | 0,
|
||||
zoomArgs[4] | 0,
|
||||
];
|
||||
} else if (zoomArg === "FitR") {
|
||||
if (zoomArgs.length !== 5) {
|
||||
console.error(
|
||||
'PDFLinkService.setHash: Not enough parameters for "FitR".'
|
||||
);
|
||||
} else {
|
||||
dest = [
|
||||
null,
|
||||
{ name: zoomArg },
|
||||
zoomArgs[1] | 0,
|
||||
zoomArgs[2] | 0,
|
||||
zoomArgs[3] | 0,
|
||||
zoomArgs[4] | 0,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (dest) {
|
||||
|
@ -432,38 +390,47 @@ class PDFLinkService {
|
|||
if (params.has("nameddest")) {
|
||||
this.goToDestination(params.get("nameddest"));
|
||||
}
|
||||
} else {
|
||||
// Named (or explicit) destination.
|
||||
dest = unescape(hash);
|
||||
try {
|
||||
dest = JSON.parse(dest);
|
||||
|
||||
if (!Array.isArray(dest)) {
|
||||
// Avoid incorrectly rejecting a valid named destination, such as
|
||||
// e.g. "4.3" or "true", because `JSON.parse` converted its type.
|
||||
dest = dest.toString();
|
||||
}
|
||||
} catch (ex) {}
|
||||
|
||||
if (
|
||||
typeof dest === "string" ||
|
||||
PDFLinkService.#isValidExplicitDestination(dest)
|
||||
) {
|
||||
this.goToDestination(dest);
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
`PDFLinkService.setHash: "${unescape(
|
||||
hash
|
||||
)}" is not a valid destination.`
|
||||
);
|
||||
// Support opening of PDF attachments in the Firefox PDF Viewer,
|
||||
// which uses a couple of non-standard hash parameters; refer to
|
||||
// `DownloadManager.openOrDownloadData` in the firefoxcom.js file.
|
||||
if (!params.has("filename") || !params.has("filedest")) {
|
||||
return;
|
||||
}
|
||||
hash = params.get("filedest");
|
||||
}
|
||||
|
||||
// Named (or explicit) destination.
|
||||
dest = unescape(hash);
|
||||
try {
|
||||
dest = JSON.parse(dest);
|
||||
|
||||
if (!Array.isArray(dest)) {
|
||||
// Avoid incorrectly rejecting a valid named destination, such as
|
||||
// e.g. "4.3" or "true", because `JSON.parse` converted its type.
|
||||
dest = dest.toString();
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) {
|
||||
this.goToDestination(dest);
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
`PDFLinkService.setHash: "${unescape(hash)}" is not a valid destination.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} action
|
||||
*/
|
||||
executeNamedAction(action) {
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
// See PDF reference, table 8.45 - Named action
|
||||
switch (action) {
|
||||
case "GoBack":
|
||||
|
@ -504,126 +471,61 @@ class PDFLinkService {
|
|||
* @param {Object} action
|
||||
*/
|
||||
async executeSetOCGState(action) {
|
||||
const pdfDocument = this.pdfDocument;
|
||||
const optionalContentConfig = await this.pdfViewer
|
||||
.optionalContentConfigPromise;
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
const pdfDocument = this.pdfDocument,
|
||||
optionalContentConfig = await this.pdfViewer.optionalContentConfigPromise;
|
||||
|
||||
if (pdfDocument !== this.pdfDocument) {
|
||||
return; // The document was closed while the optional content resolved.
|
||||
}
|
||||
let operator;
|
||||
|
||||
for (const elem of action.state) {
|
||||
switch (elem) {
|
||||
case "ON":
|
||||
case "OFF":
|
||||
case "Toggle":
|
||||
operator = elem;
|
||||
continue;
|
||||
}
|
||||
switch (operator) {
|
||||
case "ON":
|
||||
optionalContentConfig.setVisibility(elem, true);
|
||||
break;
|
||||
case "OFF":
|
||||
optionalContentConfig.setVisibility(elem, false);
|
||||
break;
|
||||
case "Toggle":
|
||||
const group = optionalContentConfig.getGroup(elem);
|
||||
if (group) {
|
||||
optionalContentConfig.setVisibility(elem, !group.visible);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
optionalContentConfig.setOCGState(action);
|
||||
|
||||
this.pdfViewer.optionalContentConfigPromise = Promise.resolve(
|
||||
optionalContentConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pageNum - page number.
|
||||
* @param {Object} pageRef - reference to the page.
|
||||
*/
|
||||
cachePageRef(pageNum, pageRef) {
|
||||
if (!pageRef) {
|
||||
return;
|
||||
}
|
||||
const refStr =
|
||||
pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
|
||||
this.#pagesRefCache.set(refStr, pageNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
_cachedPageNumber(pageRef) {
|
||||
if (!pageRef) {
|
||||
return null;
|
||||
}
|
||||
const refStr =
|
||||
pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
|
||||
return this.#pagesRefCache.get(refStr) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pageNumber
|
||||
*/
|
||||
isPageVisible(pageNumber) {
|
||||
return this.pdfViewer.isPageVisible(pageNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pageNumber
|
||||
*/
|
||||
isPageCached(pageNumber) {
|
||||
return this.pdfViewer.isPageCached(pageNumber);
|
||||
}
|
||||
|
||||
static #isValidExplicitDestination(dest) {
|
||||
if (!Array.isArray(dest)) {
|
||||
static #isValidExplicitDest(dest) {
|
||||
if (!Array.isArray(dest) || dest.length < 2) {
|
||||
return false;
|
||||
}
|
||||
const destLength = dest.length;
|
||||
if (destLength < 2) {
|
||||
return false;
|
||||
}
|
||||
const page = dest[0];
|
||||
const [page, zoom, ...args] = dest;
|
||||
if (
|
||||
!(
|
||||
typeof page === "object" &&
|
||||
Number.isInteger(page.num) &&
|
||||
Number.isInteger(page.gen)
|
||||
Number.isInteger(page?.num) &&
|
||||
Number.isInteger(page?.gen)
|
||||
) &&
|
||||
!(Number.isInteger(page) && page >= 0)
|
||||
!Number.isInteger(page)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const zoom = dest[1];
|
||||
if (!(typeof zoom === "object" && typeof zoom.name === "string")) {
|
||||
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
|
||||
return false;
|
||||
}
|
||||
const argsLen = args.length;
|
||||
let allowNull = true;
|
||||
switch (zoom.name) {
|
||||
case "XYZ":
|
||||
if (destLength !== 5) {
|
||||
if (argsLen < 2 || argsLen > 3) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "Fit":
|
||||
case "FitB":
|
||||
return destLength === 2;
|
||||
return argsLen === 0;
|
||||
case "FitH":
|
||||
case "FitBH":
|
||||
case "FitV":
|
||||
case "FitBV":
|
||||
if (destLength !== 3) {
|
||||
if (argsLen > 1) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "FitR":
|
||||
if (destLength !== 6) {
|
||||
if (argsLen !== 4) {
|
||||
return false;
|
||||
}
|
||||
allowNull = false;
|
||||
|
@ -631,9 +533,8 @@ class PDFLinkService {
|
|||
default:
|
||||
return false;
|
||||
}
|
||||
for (let i = 2; i < destLength; i++) {
|
||||
const param = dest[i];
|
||||
if (!(typeof param === "number" || (allowNull && param === null))) {
|
||||
for (const arg of args) {
|
||||
if (!(typeof arg === "number" || (allowNull && arg === null))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -644,118 +545,8 @@ class PDFLinkService {
|
|||
/**
|
||||
* @implements {IPDFLinkService}
|
||||
*/
|
||||
class SimpleLinkService {
|
||||
constructor() {
|
||||
this.externalLinkEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get pagesCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get page() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
set page(value) {}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get rotation() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
set rotation(value) {}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isInPresentationMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|Array} dest - The named, or explicit, PDF destination.
|
||||
*/
|
||||
async goToDestination(dest) {}
|
||||
|
||||
/**
|
||||
* @param {number|string} val - The page number, or page label.
|
||||
*/
|
||||
goToPage(val) {}
|
||||
|
||||
/**
|
||||
* @param {HTMLAnchorElement} link
|
||||
* @param {string} url
|
||||
* @param {boolean} [newWindow]
|
||||
*/
|
||||
addLinkAttributes(link, url, newWindow = false) {
|
||||
addLinkAttributes(link, { url, enabled: this.externalLinkEnabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dest - The PDF destination object.
|
||||
* @returns {string} The hyperlink to the PDF object.
|
||||
*/
|
||||
getDestinationHash(dest) {
|
||||
return "#";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hash - The PDF parameters/hash.
|
||||
* @returns {string} The hyperlink to the PDF object.
|
||||
*/
|
||||
getAnchorUrl(hash) {
|
||||
return "#";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
*/
|
||||
setHash(hash) {}
|
||||
|
||||
/**
|
||||
* @param {string} action
|
||||
*/
|
||||
executeNamedAction(action) {}
|
||||
|
||||
/**
|
||||
* @param {Object} action
|
||||
*/
|
||||
executeSetOCGState(action) {}
|
||||
|
||||
/**
|
||||
* @param {number} pageNum - page number.
|
||||
* @param {Object} pageRef - reference to the page.
|
||||
*/
|
||||
cachePageRef(pageNum, pageRef) {}
|
||||
|
||||
/**
|
||||
* @param {number} pageNumber
|
||||
*/
|
||||
isPageVisible(pageNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pageNumber
|
||||
*/
|
||||
isPageCached(pageNumber) {
|
||||
return true;
|
||||
}
|
||||
class SimpleLinkService extends PDFLinkService {
|
||||
setDocument(pdfDocument, baseUrl = null) {}
|
||||
}
|
||||
|
||||
export { LinkTarget, PDFLinkService, SimpleLinkService };
|
||||
|
|
|
@ -13,8 +13,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
|
||||
/** @typedef {import("./interfaces.js").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
|
||||
|
||||
import { BaseTreeViewer } from "./base_tree_viewer.js";
|
||||
import { createPromiseCapability } from "./pdfjs";
|
||||
import { SidebarView } from "./ui_utils.js";
|
||||
|
||||
/**
|
||||
|
@ -28,7 +34,7 @@ import { SidebarView } from "./ui_utils.js";
|
|||
/**
|
||||
* @typedef {Object} PDFOutlineViewerRenderParameters
|
||||
* @property {Array|null} outline - An array of outline objects.
|
||||
* @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
|
||||
* @property {PDFDocumentProxy} pdfDocument - A {PDFDocument} instance.
|
||||
*/
|
||||
|
||||
class PDFOutlineViewer extends BaseTreeViewer {
|
||||
|
@ -54,14 +60,9 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
|
||||
// If the capability is still pending, see the `_dispatchEvent`-method,
|
||||
// we know that the `currentOutlineItem`-button can be enabled here.
|
||||
if (
|
||||
this._currentOutlineItemCapability &&
|
||||
!this._currentOutlineItemCapability.settled
|
||||
) {
|
||||
this._currentOutlineItemCapability.resolve(
|
||||
/* enabled = */ this._isPagesLoaded
|
||||
);
|
||||
}
|
||||
this._currentOutlineItemCapability?.resolve(
|
||||
/* enabled = */ this._isPagesLoaded
|
||||
);
|
||||
});
|
||||
this.eventBus._on("sidebarviewchanged", evt => {
|
||||
this._sidebarView = evt.view;
|
||||
|
@ -76,20 +77,15 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
this._currentPageNumber = 1;
|
||||
this._isPagesLoaded = null;
|
||||
|
||||
if (
|
||||
this._currentOutlineItemCapability &&
|
||||
!this._currentOutlineItemCapability.settled
|
||||
) {
|
||||
this._currentOutlineItemCapability.resolve(/* enabled = */ false);
|
||||
}
|
||||
this._currentOutlineItemCapability?.resolve(/* enabled = */ false);
|
||||
this._currentOutlineItemCapability = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_dispatchEvent(outlineCount) {
|
||||
this._currentOutlineItemCapability = createPromiseCapability();
|
||||
this._currentOutlineItemCapability = Promise.withResolvers();
|
||||
if (
|
||||
outlineCount === 0 ||
|
||||
this._pdfDocument?.loadingParams.disableAutoFetch
|
||||
|
@ -109,7 +105,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_bindLink(
|
||||
element,
|
||||
|
@ -133,7 +129,6 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
element.href = linkService.getAnchorUrl("");
|
||||
element.onclick = () => {
|
||||
this.downloadManager.openOrDownloadData(
|
||||
element,
|
||||
attachment.content,
|
||||
attachment.filename
|
||||
);
|
||||
|
@ -174,7 +169,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
_addToggleButton(div, { count, items }) {
|
||||
let hidden = false;
|
||||
|
@ -308,7 +303,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
if (this._pageNumberToDestHashCapability) {
|
||||
return this._pageNumberToDestHashCapability.promise;
|
||||
}
|
||||
this._pageNumberToDestHashCapability = createPromiseCapability();
|
||||
this._pageNumberToDestHashCapability = Promise.withResolvers();
|
||||
|
||||
const pageNumberToDestHash = new Map(),
|
||||
pageNumberNesting = new Map();
|
||||
|
@ -330,21 +325,10 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||
if (Array.isArray(explicitDest)) {
|
||||
const [destRef] = explicitDest;
|
||||
|
||||
if (typeof destRef === "object" && destRef !== null) {
|
||||
pageNumber = this.linkService._cachedPageNumber(destRef);
|
||||
|
||||
if (!pageNumber) {
|
||||
try {
|
||||
pageNumber = (await pdfDocument.getPageIndex(destRef)) + 1;
|
||||
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
return null; // The document was closed while the data resolved.
|
||||
}
|
||||
this.linkService.cachePageRef(pageNumber, destRef);
|
||||
} catch (ex) {
|
||||
// Invalid page reference, ignore it and continue parsing.
|
||||
}
|
||||
}
|
||||
if (destRef && typeof destRef === "object") {
|
||||
// The page reference must be available, since the current method
|
||||
// won't be invoked until all pages have been loaded.
|
||||
pageNumber = pdfDocument.cachedPageNumber(destRef);
|
||||
} else if (Number.isInteger(destRef)) {
|
||||
pageNumber = destRef + 1;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -13,6 +13,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
/** @typedef {import("./pdf_viewer.js").PDFViewer} PDFViewer */
|
||||
|
||||
import {
|
||||
normalizeWheelEventDelta,
|
||||
PresentationModeState,
|
||||
|
@ -46,6 +49,10 @@ class PDFPresentationMode {
|
|||
|
||||
#args = null;
|
||||
|
||||
#fullscreenChangeAbortController = null;
|
||||
|
||||
#windowAbortController = null;
|
||||
|
||||
/**
|
||||
* @param {PDFPresentationModeOptions} options
|
||||
*/
|
||||
|
@ -101,7 +108,7 @@ class PDFPresentationMode {
|
|||
await promise;
|
||||
pdfViewer.focus(); // Fixes bug 1787456.
|
||||
return true;
|
||||
} catch (reason) {
|
||||
} catch {
|
||||
this.#removeFullscreenChangeListeners();
|
||||
this.#notifyStateChange(PresentationModeState.NORMAL);
|
||||
}
|
||||
|
@ -175,7 +182,9 @@ class PDFPresentationMode {
|
|||
this.pdfViewer.currentScaleValue = "page-fit";
|
||||
|
||||
if (this.#args.annotationEditorMode !== null) {
|
||||
this.pdfViewer.annotationEditorMode = AnnotationEditorType.NONE;
|
||||
this.pdfViewer.annotationEditorMode = {
|
||||
mode: AnnotationEditorType.NONE,
|
||||
};
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
@ -186,7 +195,7 @@ class PDFPresentationMode {
|
|||
// Text selection is disabled in Presentation Mode, thus it's not possible
|
||||
// for the user to deselect text that is selected (e.g. with "Select all")
|
||||
// when entering Presentation Mode, hence we remove any active selection.
|
||||
window.getSelection().removeAllRanges();
|
||||
document.getSelection().empty();
|
||||
}
|
||||
|
||||
#exit() {
|
||||
|
@ -207,7 +216,9 @@ class PDFPresentationMode {
|
|||
this.pdfViewer.currentPageNumber = pageNumber;
|
||||
|
||||
if (this.#args.annotationEditorMode !== null) {
|
||||
this.pdfViewer.annotationEditorMode = this.#args.annotationEditorMode;
|
||||
this.pdfViewer.annotationEditorMode = {
|
||||
mode: this.#args.annotationEditorMode,
|
||||
};
|
||||
}
|
||||
this.#args = null;
|
||||
}, 0);
|
||||
|
@ -339,59 +350,62 @@ class PDFPresentationMode {
|
|||
}
|
||||
|
||||
#addWindowListeners() {
|
||||
this.showControlsBind = this.#showControls.bind(this);
|
||||
this.mouseDownBind = this.#mouseDown.bind(this);
|
||||
this.mouseWheelBind = this.#mouseWheel.bind(this);
|
||||
this.resetMouseScrollStateBind = this.#resetMouseScrollState.bind(this);
|
||||
this.contextMenuBind = this.#contextMenu.bind(this);
|
||||
this.touchSwipeBind = this.#touchSwipe.bind(this);
|
||||
if (this.#windowAbortController) {
|
||||
return;
|
||||
}
|
||||
this.#windowAbortController = new AbortController();
|
||||
const { signal } = this.#windowAbortController;
|
||||
|
||||
window.addEventListener("mousemove", this.showControlsBind);
|
||||
window.addEventListener("mousedown", this.mouseDownBind);
|
||||
window.addEventListener("wheel", this.mouseWheelBind, { passive: false });
|
||||
window.addEventListener("keydown", this.resetMouseScrollStateBind);
|
||||
window.addEventListener("contextmenu", this.contextMenuBind);
|
||||
window.addEventListener("touchstart", this.touchSwipeBind);
|
||||
window.addEventListener("touchmove", this.touchSwipeBind);
|
||||
window.addEventListener("touchend", this.touchSwipeBind);
|
||||
const touchSwipeBind = this.#touchSwipe.bind(this);
|
||||
|
||||
window.addEventListener("mousemove", this.#showControls.bind(this), {
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("mousedown", this.#mouseDown.bind(this), {
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("wheel", this.#mouseWheel.bind(this), {
|
||||
passive: false,
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("keydown", this.#resetMouseScrollState.bind(this), {
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("contextmenu", this.#contextMenu.bind(this), {
|
||||
signal,
|
||||
});
|
||||
window.addEventListener("touchstart", touchSwipeBind, { signal });
|
||||
window.addEventListener("touchmove", touchSwipeBind, { signal });
|
||||
window.addEventListener("touchend", touchSwipeBind, { signal });
|
||||
}
|
||||
|
||||
#removeWindowListeners() {
|
||||
window.removeEventListener("mousemove", this.showControlsBind);
|
||||
window.removeEventListener("mousedown", this.mouseDownBind);
|
||||
window.removeEventListener("wheel", this.mouseWheelBind, {
|
||||
passive: false,
|
||||
});
|
||||
window.removeEventListener("keydown", this.resetMouseScrollStateBind);
|
||||
window.removeEventListener("contextmenu", this.contextMenuBind);
|
||||
window.removeEventListener("touchstart", this.touchSwipeBind);
|
||||
window.removeEventListener("touchmove", this.touchSwipeBind);
|
||||
window.removeEventListener("touchend", this.touchSwipeBind);
|
||||
|
||||
delete this.showControlsBind;
|
||||
delete this.mouseDownBind;
|
||||
delete this.mouseWheelBind;
|
||||
delete this.resetMouseScrollStateBind;
|
||||
delete this.contextMenuBind;
|
||||
delete this.touchSwipeBind;
|
||||
}
|
||||
|
||||
#fullscreenChange() {
|
||||
if (/* isFullscreen = */ document.fullscreenElement) {
|
||||
this.#enter();
|
||||
} else {
|
||||
this.#exit();
|
||||
}
|
||||
this.#windowAbortController?.abort();
|
||||
this.#windowAbortController = null;
|
||||
}
|
||||
|
||||
#addFullscreenChangeListeners() {
|
||||
this.fullscreenChangeBind = this.#fullscreenChange.bind(this);
|
||||
window.addEventListener("fullscreenchange", this.fullscreenChangeBind);
|
||||
if (this.#fullscreenChangeAbortController) {
|
||||
return;
|
||||
}
|
||||
this.#fullscreenChangeAbortController = new AbortController();
|
||||
|
||||
window.addEventListener(
|
||||
"fullscreenchange",
|
||||
() => {
|
||||
if (/* isFullscreen = */ document.fullscreenElement) {
|
||||
this.#enter();
|
||||
} else {
|
||||
this.#exit();
|
||||
}
|
||||
},
|
||||
{ signal: this.#fullscreenChangeAbortController.signal }
|
||||
);
|
||||
}
|
||||
|
||||
#removeFullscreenChangeListeners() {
|
||||
window.removeEventListener("fullscreenchange", this.fullscreenChangeBind);
|
||||
delete this.fullscreenChangeBind;
|
||||
this.#fullscreenChangeAbortController?.abort();
|
||||
this.#fullscreenChangeAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
396
app/src/asset/pdf/pdf_print_service.js
Normal file
396
app/src/asset/pdf/pdf_print_service.js
Normal file
|
@ -0,0 +1,396 @@
|
|||
/* Copyright 2016 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./interfaces.js").IPDFPrintServiceFactory} IPDFPrintServiceFactory */
|
||||
|
||||
import {
|
||||
AnnotationMode,
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
shadow,
|
||||
} from "./pdfjs";
|
||||
import { getXfaHtmlForPrinting } from "./print_utils.js";
|
||||
|
||||
let activeService = null;
|
||||
let dialog = null;
|
||||
let overlayManager = null;
|
||||
let viewerApp = { initialized: false };
|
||||
|
||||
// Renders the page to the canvas of the given print service, and returns
|
||||
// the suggested dimensions of the output page.
|
||||
function renderPage(
|
||||
activeServiceOnEntry,
|
||||
pdfDocument,
|
||||
pageNumber,
|
||||
size,
|
||||
printResolution,
|
||||
optionalContentConfigPromise,
|
||||
printAnnotationStoragePromise
|
||||
) {
|
||||
const scratchCanvas = activeService.scratchCanvas;
|
||||
|
||||
// The size of the canvas in pixels for printing.
|
||||
const PRINT_UNITS = printResolution / PixelsPerInch.PDF;
|
||||
scratchCanvas.width = Math.floor(size.width * PRINT_UNITS);
|
||||
scratchCanvas.height = Math.floor(size.height * PRINT_UNITS);
|
||||
|
||||
const ctx = scratchCanvas.getContext("2d");
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
ctx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height);
|
||||
ctx.restore();
|
||||
|
||||
return Promise.all([
|
||||
pdfDocument.getPage(pageNumber),
|
||||
printAnnotationStoragePromise,
|
||||
]).then(function ([pdfPage, printAnnotationStorage]) {
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
|
||||
viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
|
||||
intent: "print",
|
||||
annotationMode: AnnotationMode.ENABLE_STORAGE,
|
||||
optionalContentConfigPromise,
|
||||
printAnnotationStorage,
|
||||
};
|
||||
const renderTask = pdfPage.render(renderContext);
|
||||
|
||||
return renderTask.promise.catch(reason => {
|
||||
if (!(reason instanceof RenderingCancelledException)) {
|
||||
console.error(reason);
|
||||
}
|
||||
throw reason;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class PDFPrintService {
|
||||
constructor({
|
||||
pdfDocument,
|
||||
pagesOverview,
|
||||
printContainer,
|
||||
printResolution,
|
||||
printAnnotationStoragePromise = null,
|
||||
}) {
|
||||
this.pdfDocument = pdfDocument;
|
||||
this.pagesOverview = pagesOverview;
|
||||
this.printContainer = printContainer;
|
||||
this._printResolution = printResolution || 150;
|
||||
this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "print",
|
||||
});
|
||||
this._printAnnotationStoragePromise =
|
||||
printAnnotationStoragePromise || Promise.resolve();
|
||||
this.currentPage = -1;
|
||||
// The temporary canvas where renderPage paints one page at a time.
|
||||
this.scratchCanvas = document.createElement("canvas");
|
||||
}
|
||||
|
||||
layout() {
|
||||
this.throwIfInactive();
|
||||
|
||||
const body = document.querySelector("body");
|
||||
body.setAttribute("data-pdfjsprinting", true);
|
||||
|
||||
const { width, height } = this.pagesOverview[0];
|
||||
const hasEqualPageSizes = this.pagesOverview.every(
|
||||
size => size.width === width && size.height === height
|
||||
);
|
||||
if (!hasEqualPageSizes) {
|
||||
console.warn(
|
||||
"Not all pages have the same size. The printed result may be incorrect!"
|
||||
);
|
||||
}
|
||||
|
||||
// Insert a @page + size rule to make sure that the page size is correctly
|
||||
// set. Note that we assume that all pages have the same size, because
|
||||
// variable-size pages are not supported yet (e.g. in Chrome & Firefox).
|
||||
// TODO(robwu): Use named pages when size calculation bugs get resolved
|
||||
// (e.g. https://crbug.com/355116) AND when support for named pages is
|
||||
// added (http://www.w3.org/TR/css3-page/#using-named-pages).
|
||||
// In browsers where @page + size is not supported, the next stylesheet
|
||||
// will be ignored and the user has to select the correct paper size in
|
||||
// the UI if wanted.
|
||||
this.pageStyleSheet = document.createElement("style");
|
||||
this.pageStyleSheet.textContent = `@page { size: ${width}pt ${height}pt;}`;
|
||||
body.append(this.pageStyleSheet);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (activeService !== this) {
|
||||
// |activeService| cannot be replaced without calling destroy() first,
|
||||
// so if it differs then an external consumer has a stale reference to us.
|
||||
return;
|
||||
}
|
||||
this.printContainer.textContent = "";
|
||||
|
||||
const body = document.querySelector("body");
|
||||
body.removeAttribute("data-pdfjsprinting");
|
||||
|
||||
if (this.pageStyleSheet) {
|
||||
this.pageStyleSheet.remove();
|
||||
this.pageStyleSheet = null;
|
||||
}
|
||||
this.scratchCanvas.width = this.scratchCanvas.height = 0;
|
||||
this.scratchCanvas = null;
|
||||
activeService = null;
|
||||
ensureOverlay().then(function () {
|
||||
if (overlayManager.active === dialog) {
|
||||
overlayManager.close(dialog);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderPages() {
|
||||
if (this.pdfDocument.isPureXfa) {
|
||||
getXfaHtmlForPrinting(this.printContainer, this.pdfDocument);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const pageCount = this.pagesOverview.length;
|
||||
const renderNextPage = (resolve, reject) => {
|
||||
this.throwIfInactive();
|
||||
if (++this.currentPage >= pageCount) {
|
||||
renderProgress(pageCount, pageCount);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const index = this.currentPage;
|
||||
renderProgress(index, pageCount);
|
||||
renderPage(
|
||||
this,
|
||||
this.pdfDocument,
|
||||
/* pageNumber = */ index + 1,
|
||||
this.pagesOverview[index],
|
||||
this._printResolution,
|
||||
this._optionalContentConfigPromise,
|
||||
this._printAnnotationStoragePromise
|
||||
)
|
||||
.then(this.useRenderedPage.bind(this))
|
||||
.then(function () {
|
||||
renderNextPage(resolve, reject);
|
||||
}, reject);
|
||||
};
|
||||
return new Promise(renderNextPage);
|
||||
}
|
||||
|
||||
useRenderedPage() {
|
||||
this.throwIfInactive();
|
||||
const img = document.createElement("img");
|
||||
this.scratchCanvas.toBlob(blob => {
|
||||
img.src = URL.createObjectURL(blob);
|
||||
});
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "printedPage";
|
||||
wrapper.append(img);
|
||||
this.printContainer.append(wrapper);
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
|
||||
promise
|
||||
.catch(() => {
|
||||
// Avoid "Uncaught promise" messages in the console.
|
||||
})
|
||||
.then(() => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
performPrint() {
|
||||
this.throwIfInactive();
|
||||
return new Promise(resolve => {
|
||||
// Push window.print in the macrotask queue to avoid being affected by
|
||||
// the deprecation of running print() code in a microtask, see
|
||||
// https://github.com/mozilla/pdf.js/issues/7547.
|
||||
setTimeout(() => {
|
||||
if (!this.active) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
print.call(window);
|
||||
// Delay promise resolution in case print() was not synchronous.
|
||||
setTimeout(resolve, 20); // Tidy-up.
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this === activeService;
|
||||
}
|
||||
|
||||
throwIfInactive() {
|
||||
if (!this.active) {
|
||||
throw new Error("This print request was cancelled or completed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const print = window.print;
|
||||
window.print = function () {
|
||||
if (activeService) {
|
||||
console.warn("Ignored window.print() because of a pending print job.");
|
||||
return;
|
||||
}
|
||||
ensureOverlay().then(function () {
|
||||
if (activeService) {
|
||||
overlayManager.open(dialog);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
dispatchEvent("beforeprint");
|
||||
} finally {
|
||||
if (!activeService) {
|
||||
console.error("Expected print service to be initialized.");
|
||||
ensureOverlay().then(function () {
|
||||
if (overlayManager.active === dialog) {
|
||||
overlayManager.close(dialog);
|
||||
}
|
||||
});
|
||||
return; // eslint-disable-line no-unsafe-finally
|
||||
}
|
||||
const activeServiceOnEntry = activeService;
|
||||
activeService
|
||||
.renderPages()
|
||||
.then(function () {
|
||||
return activeServiceOnEntry.performPrint();
|
||||
})
|
||||
.catch(function () {
|
||||
// Ignore any error messages.
|
||||
})
|
||||
.then(function () {
|
||||
// aborts acts on the "active" print request, so we need to check
|
||||
// whether the print request (activeServiceOnEntry) is still active.
|
||||
// Without the check, an unrelated print request (created after aborting
|
||||
// this print request while the pages were being generated) would be
|
||||
// aborted.
|
||||
if (activeServiceOnEntry.active) {
|
||||
abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function dispatchEvent(eventType) {
|
||||
const event = new CustomEvent(eventType, {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
detail: "custom",
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function abort() {
|
||||
if (activeService) {
|
||||
activeService.destroy();
|
||||
dispatchEvent("afterprint");
|
||||
}
|
||||
}
|
||||
|
||||
function renderProgress(index, total) {
|
||||
if (typeof PDFJSDev === "undefined" && window.isGECKOVIEW) {
|
||||
return;
|
||||
}
|
||||
dialog ||= document.getElementById("printServiceDialog");
|
||||
const progress = Math.round((100 * index) / total);
|
||||
const progressBar = dialog.querySelector("progress");
|
||||
const progressPerc = dialog.querySelector(".relative-progress");
|
||||
progressBar.value = progress;
|
||||
progressPerc.setAttribute("data-l10n-args", JSON.stringify({ progress }));
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
function (event) {
|
||||
// Intercept Cmd/Ctrl + P in all browsers.
|
||||
// Also intercept Cmd/Ctrl + Shift + P in Chrome and Opera
|
||||
if (
|
||||
event.keyCode === /* P= */ 80 &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
!event.altKey &&
|
||||
(!event.shiftKey || window.chrome || window.opera)
|
||||
) {
|
||||
window.print();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if ("onbeforeprint" in window) {
|
||||
// Do not propagate before/afterprint events when they are not triggered
|
||||
// from within this polyfill. (FF / Chrome 63+).
|
||||
const stopPropagationIfNeeded = function (event) {
|
||||
if (event.detail !== "custom") {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
window.addEventListener("beforeprint", stopPropagationIfNeeded);
|
||||
window.addEventListener("afterprint", stopPropagationIfNeeded);
|
||||
}
|
||||
|
||||
let overlayPromise;
|
||||
function ensureOverlay() {
|
||||
if (typeof PDFJSDev === "undefined" && window.isGECKOVIEW) {
|
||||
return Promise.reject(
|
||||
new Error("ensureOverlay not implemented in GECKOVIEW development mode.")
|
||||
);
|
||||
}
|
||||
if (!overlayPromise) {
|
||||
overlayManager = viewerApp.overlayManager;
|
||||
if (!overlayManager) {
|
||||
throw new Error("The overlay manager has not yet been initialized.");
|
||||
}
|
||||
dialog ||= document.getElementById("printServiceDialog");
|
||||
|
||||
overlayPromise = overlayManager.register(
|
||||
dialog,
|
||||
/* canForceClose = */ true
|
||||
);
|
||||
|
||||
document.getElementById("printCancel").onclick = abort;
|
||||
dialog.addEventListener("close", abort);
|
||||
}
|
||||
return overlayPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {IPDFPrintServiceFactory}
|
||||
*/
|
||||
class PDFPrintServiceFactory {
|
||||
static initGlobals(app) {
|
||||
viewerApp = app;
|
||||
}
|
||||
|
||||
static get supportsPrinting() {
|
||||
return shadow(this, "supportsPrinting", true);
|
||||
}
|
||||
|
||||
static createPrintService(params) {
|
||||
if (activeService) {
|
||||
throw new Error("The print service is created and active.");
|
||||
}
|
||||
return (activeService = new PDFPrintService(params));
|
||||
}
|
||||
}
|
||||
|
||||
export { PDFPrintServiceFactory };
|
|
@ -36,6 +36,12 @@ class PDFRenderingQueue {
|
|||
this.idleTimeout = null;
|
||||
this.printing = false;
|
||||
this.isThumbnailViewEnabled = false;
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
Object.defineProperty(this, "hasViewer", {
|
||||
value: () => !!this.pdfViewer,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,13 +66,6 @@ class PDFRenderingQueue {
|
|||
return this.highestPriorityPage === view.renderingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasViewer() {
|
||||
return !!this.pdfViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} currentlyVisiblePages
|
||||
*/
|
||||
|
|
|
@ -16,68 +16,74 @@
|
|||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
|
||||
import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js";
|
||||
import { createPromiseCapability, shadow } from "./pdfjs";
|
||||
import { shadow } from "./pdfjs";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFScriptingManagerOptions
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {string} sandboxBundleSrc - The path and filename of the scripting
|
||||
* bundle.
|
||||
* @property {Object} [scriptingFactory] - The factory that is used when
|
||||
* @property {string} [sandboxBundleSrc] - The path and filename of the
|
||||
* scripting bundle.
|
||||
* @property {Object} [externalServices] - The factory that is used when
|
||||
* initializing scripting; must contain a `createScripting` method.
|
||||
* PLEASE NOTE: Primarily intended for the default viewer use-case.
|
||||
* @property {function} [docPropertiesLookup] - The function that is used to
|
||||
* lookup the necessary document properties.
|
||||
* @property {function} [docProperties] - The function that is used to lookup
|
||||
* the necessary document properties.
|
||||
*/
|
||||
|
||||
class PDFScriptingManager {
|
||||
#closeCapability = null;
|
||||
|
||||
#destroyCapability = null;
|
||||
|
||||
#docProperties = null;
|
||||
|
||||
#eventAbortController = null;
|
||||
|
||||
#eventBus = null;
|
||||
|
||||
#externalServices = null;
|
||||
|
||||
#pdfDocument = null;
|
||||
|
||||
#pdfViewer = null;
|
||||
|
||||
#ready = false;
|
||||
|
||||
#scripting = null;
|
||||
|
||||
#willPrintCapability = null;
|
||||
|
||||
/**
|
||||
* @param {PDFScriptingManagerOptions} options
|
||||
*/
|
||||
constructor({
|
||||
eventBus,
|
||||
sandboxBundleSrc = null,
|
||||
scriptingFactory = null,
|
||||
docPropertiesLookup = null,
|
||||
}) {
|
||||
this._pdfDocument = null;
|
||||
this._pdfViewer = null;
|
||||
this._closeCapability = null;
|
||||
this._destroyCapability = null;
|
||||
constructor({ eventBus, externalServices = null, docProperties = null }) {
|
||||
this.#eventBus = eventBus;
|
||||
this.#externalServices = externalServices;
|
||||
this.#docProperties = docProperties;
|
||||
|
||||
this._scripting = null;
|
||||
this._ready = false;
|
||||
|
||||
this._eventBus = eventBus;
|
||||
this._sandboxBundleSrc = sandboxBundleSrc;
|
||||
this._scriptingFactory = scriptingFactory;
|
||||
this._docPropertiesLookup = docPropertiesLookup;
|
||||
|
||||
// The default viewer already handles adding/removing of DOM events,
|
||||
// hence limit this to only the viewer components.
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("COMPONENTS") &&
|
||||
!this._scriptingFactory
|
||||
) {
|
||||
window.addEventListener("updatefromsandbox", event => {
|
||||
this._eventBus.dispatch("updatefromsandbox", {
|
||||
source: window,
|
||||
detail: event.detail,
|
||||
});
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||
Object.defineProperty(this, "sandboxTrip", {
|
||||
value: () =>
|
||||
setTimeout(
|
||||
() =>
|
||||
this.#scripting?.dispatchEventInSandbox({
|
||||
name: "sandboxtripbegin",
|
||||
}),
|
||||
0
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setViewer(pdfViewer) {
|
||||
this._pdfViewer = pdfViewer;
|
||||
this.#pdfViewer = pdfViewer;
|
||||
}
|
||||
|
||||
async setDocument(pdfDocument) {
|
||||
if (this._pdfDocument) {
|
||||
await this._destroyScripting();
|
||||
if (this.#pdfDocument) {
|
||||
await this.#destroyScripting();
|
||||
}
|
||||
this._pdfDocument = pdfDocument;
|
||||
this.#pdfDocument = pdfDocument;
|
||||
|
||||
if (!pdfDocument) {
|
||||
return;
|
||||
|
@ -90,69 +96,88 @@ class PDFScriptingManager {
|
|||
|
||||
if (!objects && !docActions) {
|
||||
// No FieldObjects or JavaScript actions were found in the document.
|
||||
await this._destroyScripting();
|
||||
await this.#destroyScripting();
|
||||
return;
|
||||
}
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
if (pdfDocument !== this.#pdfDocument) {
|
||||
return; // The document was closed while the data resolved.
|
||||
}
|
||||
try {
|
||||
this._scripting = this._createScripting();
|
||||
this.#scripting = this.#initScripting();
|
||||
} catch (error) {
|
||||
console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
|
||||
console.error(`setDocument: "${error.message}".`);
|
||||
|
||||
await this._destroyScripting();
|
||||
await this.#destroyScripting();
|
||||
return;
|
||||
}
|
||||
const eventBus = this.#eventBus;
|
||||
|
||||
this._internalEvents.set("updatefromsandbox", event => {
|
||||
if (event?.source !== window) {
|
||||
return;
|
||||
}
|
||||
this._updateFromSandbox(event.detail);
|
||||
});
|
||||
this._internalEvents.set("dispatcheventinsandbox", event => {
|
||||
this._scripting?.dispatchEventInSandbox(event.detail);
|
||||
});
|
||||
this.#eventAbortController = new AbortController();
|
||||
const { signal } = this.#eventAbortController;
|
||||
|
||||
this._internalEvents.set("pagechanging", ({ pageNumber, previous }) => {
|
||||
if (pageNumber === previous) {
|
||||
return; // The current page didn't change.
|
||||
}
|
||||
this._dispatchPageClose(previous);
|
||||
this._dispatchPageOpen(pageNumber);
|
||||
});
|
||||
this._internalEvents.set("pagerendered", ({ pageNumber }) => {
|
||||
if (!this._pageOpenPending.has(pageNumber)) {
|
||||
return; // No pending "PageOpen" event for the newly rendered page.
|
||||
}
|
||||
if (pageNumber !== this._pdfViewer.currentPageNumber) {
|
||||
return; // The newly rendered page is no longer the current one.
|
||||
}
|
||||
this._dispatchPageOpen(pageNumber);
|
||||
});
|
||||
this._internalEvents.set("pagesdestroy", async event => {
|
||||
await this._dispatchPageClose(this._pdfViewer.currentPageNumber);
|
||||
eventBus._on(
|
||||
"updatefromsandbox",
|
||||
event => {
|
||||
if (event?.source === window) {
|
||||
this.#updateFromSandbox(event.detail);
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
eventBus._on(
|
||||
"dispatcheventinsandbox",
|
||||
event => {
|
||||
this.#scripting?.dispatchEventInSandbox(event.detail);
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
await this._scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "WillClose",
|
||||
});
|
||||
eventBus._on(
|
||||
"pagechanging",
|
||||
({ pageNumber, previous }) => {
|
||||
if (pageNumber === previous) {
|
||||
return; // The current page didn't change.
|
||||
}
|
||||
this.#dispatchPageClose(previous);
|
||||
this.#dispatchPageOpen(pageNumber);
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
eventBus._on(
|
||||
"pagerendered",
|
||||
({ pageNumber }) => {
|
||||
if (!this._pageOpenPending.has(pageNumber)) {
|
||||
return; // No pending "PageOpen" event for the newly rendered page.
|
||||
}
|
||||
if (pageNumber !== this.#pdfViewer.currentPageNumber) {
|
||||
return; // The newly rendered page is no longer the current one.
|
||||
}
|
||||
this.#dispatchPageOpen(pageNumber);
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
eventBus._on(
|
||||
"pagesdestroy",
|
||||
async () => {
|
||||
await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber);
|
||||
|
||||
this._closeCapability?.resolve();
|
||||
});
|
||||
await this.#scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "WillClose",
|
||||
});
|
||||
|
||||
for (const [name, listener] of this._internalEvents) {
|
||||
this._eventBus._on(name, listener);
|
||||
}
|
||||
this.#closeCapability?.resolve();
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
try {
|
||||
const docProperties = await this._getDocProperties();
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
const docProperties = await this.#docProperties(pdfDocument);
|
||||
if (pdfDocument !== this.#pdfDocument) {
|
||||
return; // The document was closed while the properties resolved.
|
||||
}
|
||||
|
||||
await this._scripting.createSandbox({
|
||||
await this.#scripting.createSandbox({
|
||||
objects,
|
||||
calculationOrder,
|
||||
appInfo: {
|
||||
|
@ -165,72 +190,78 @@ class PDFScriptingManager {
|
|||
},
|
||||
});
|
||||
|
||||
this._eventBus.dispatch("sandboxcreated", { source: this });
|
||||
eventBus.dispatch("sandboxcreated", { source: this });
|
||||
} catch (error) {
|
||||
console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
|
||||
console.error(`setDocument: "${error.message}".`);
|
||||
|
||||
await this._destroyScripting();
|
||||
await this.#destroyScripting();
|
||||
return;
|
||||
}
|
||||
|
||||
await this._scripting?.dispatchEventInSandbox({
|
||||
await this.#scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "Open",
|
||||
});
|
||||
await this._dispatchPageOpen(
|
||||
this._pdfViewer.currentPageNumber,
|
||||
await this.#dispatchPageOpen(
|
||||
this.#pdfViewer.currentPageNumber,
|
||||
/* initialize = */ true
|
||||
);
|
||||
|
||||
// Defer this slightly, to ensure that scripting is *fully* initialized.
|
||||
Promise.resolve().then(() => {
|
||||
if (pdfDocument === this._pdfDocument) {
|
||||
this._ready = true;
|
||||
if (pdfDocument === this.#pdfDocument) {
|
||||
this.#ready = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchWillSave(detail) {
|
||||
return this._scripting?.dispatchEventInSandbox({
|
||||
async dispatchWillSave() {
|
||||
return this.#scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "WillSave",
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchDidSave(detail) {
|
||||
return this._scripting?.dispatchEventInSandbox({
|
||||
async dispatchDidSave() {
|
||||
return this.#scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "DidSave",
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchWillPrint(detail) {
|
||||
return this._scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "WillPrint",
|
||||
});
|
||||
async dispatchWillPrint() {
|
||||
if (!this.#scripting) {
|
||||
return;
|
||||
}
|
||||
await this.#willPrintCapability?.promise;
|
||||
this.#willPrintCapability = Promise.withResolvers();
|
||||
try {
|
||||
await this.#scripting.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "WillPrint",
|
||||
});
|
||||
} catch (ex) {
|
||||
this.#willPrintCapability.resolve();
|
||||
this.#willPrintCapability = null;
|
||||
throw ex;
|
||||
}
|
||||
|
||||
await this.#willPrintCapability.promise;
|
||||
}
|
||||
|
||||
async dispatchDidPrint(detail) {
|
||||
return this._scripting?.dispatchEventInSandbox({
|
||||
async dispatchDidPrint() {
|
||||
return this.#scripting?.dispatchEventInSandbox({
|
||||
id: "doc",
|
||||
name: "DidPrint",
|
||||
});
|
||||
}
|
||||
|
||||
get destroyPromise() {
|
||||
return this._destroyCapability?.promise || null;
|
||||
return this.#destroyCapability?.promise || null;
|
||||
}
|
||||
|
||||
get ready() {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
get _internalEvents() {
|
||||
return shadow(this, "_internalEvents", new Map());
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -247,17 +278,25 @@ class PDFScriptingManager {
|
|||
return shadow(this, "_visitedPages", new Map());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _updateFromSandbox(detail) {
|
||||
async #updateFromSandbox(detail) {
|
||||
const pdfViewer = this.#pdfViewer;
|
||||
// Ignore some events, see below, that don't make sense in PresentationMode.
|
||||
const isInPresentationMode =
|
||||
this._pdfViewer.isInPresentationMode ||
|
||||
this._pdfViewer.isChangingPresentationMode;
|
||||
pdfViewer.isInPresentationMode || pdfViewer.isChangingPresentationMode;
|
||||
|
||||
const { id, siblings, command, value } = detail;
|
||||
if (!id) {
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("TESTING") &&
|
||||
command === "sandboxTripEnd"
|
||||
) {
|
||||
window.setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent("sandboxtripend"));
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "clear":
|
||||
console.clear();
|
||||
|
@ -265,72 +304,62 @@ class PDFScriptingManager {
|
|||
case "error":
|
||||
console.error(value);
|
||||
break;
|
||||
case "layout": {
|
||||
// NOTE: Always ignore the pageLayout in GeckoView since there's
|
||||
// no UI available to change Scroll/Spread modes for the user.
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined"
|
||||
? window.isGECKOVIEW
|
||||
: PDFJSDev.test("GECKOVIEW")) ||
|
||||
isInPresentationMode
|
||||
) {
|
||||
return;
|
||||
case "layout":
|
||||
if (!isInPresentationMode) {
|
||||
const modes = apiPageLayoutToViewerModes(value);
|
||||
pdfViewer.spreadMode = modes.spreadMode;
|
||||
}
|
||||
const modes = apiPageLayoutToViewerModes(value);
|
||||
this._pdfViewer.spreadMode = modes.spreadMode;
|
||||
break;
|
||||
}
|
||||
case "page-num":
|
||||
this._pdfViewer.currentPageNumber = value + 1;
|
||||
pdfViewer.currentPageNumber = value + 1;
|
||||
break;
|
||||
case "print":
|
||||
await this._pdfViewer.pagesPromise;
|
||||
this._eventBus.dispatch("print", { source: this });
|
||||
await pdfViewer.pagesPromise;
|
||||
this.#eventBus.dispatch("print", { source: this });
|
||||
break;
|
||||
case "println":
|
||||
console.log(value);
|
||||
break;
|
||||
case "zoom":
|
||||
if (isInPresentationMode) {
|
||||
return;
|
||||
if (!isInPresentationMode) {
|
||||
pdfViewer.currentScaleValue = value;
|
||||
}
|
||||
this._pdfViewer.currentScaleValue = value;
|
||||
break;
|
||||
case "SaveAs":
|
||||
this._eventBus.dispatch("download", { source: this });
|
||||
this.#eventBus.dispatch("download", { source: this });
|
||||
break;
|
||||
case "FirstPage":
|
||||
this._pdfViewer.currentPageNumber = 1;
|
||||
pdfViewer.currentPageNumber = 1;
|
||||
break;
|
||||
case "LastPage":
|
||||
this._pdfViewer.currentPageNumber = this._pdfViewer.pagesCount;
|
||||
pdfViewer.currentPageNumber = pdfViewer.pagesCount;
|
||||
break;
|
||||
case "NextPage":
|
||||
this._pdfViewer.nextPage();
|
||||
pdfViewer.nextPage();
|
||||
break;
|
||||
case "PrevPage":
|
||||
this._pdfViewer.previousPage();
|
||||
pdfViewer.previousPage();
|
||||
break;
|
||||
case "ZoomViewIn":
|
||||
if (isInPresentationMode) {
|
||||
return;
|
||||
if (!isInPresentationMode) {
|
||||
pdfViewer.increaseScale();
|
||||
}
|
||||
this._pdfViewer.increaseScale();
|
||||
break;
|
||||
case "ZoomViewOut":
|
||||
if (isInPresentationMode) {
|
||||
return;
|
||||
if (!isInPresentationMode) {
|
||||
pdfViewer.decreaseScale();
|
||||
}
|
||||
this._pdfViewer.decreaseScale();
|
||||
break;
|
||||
case "WillPrintFinished":
|
||||
this.#willPrintCapability?.resolve();
|
||||
this.#willPrintCapability = null;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPresentationMode) {
|
||||
if (detail.focus) {
|
||||
return;
|
||||
}
|
||||
if (isInPresentationMode && detail.focus) {
|
||||
return;
|
||||
}
|
||||
delete detail.id;
|
||||
delete detail.siblings;
|
||||
|
@ -344,25 +373,22 @@ class PDFScriptingManager {
|
|||
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
|
||||
} else {
|
||||
// The element hasn't been rendered yet, use the AnnotationStorage.
|
||||
this._pdfDocument?.annotationStorage.setValue(elementId, detail);
|
||||
this.#pdfDocument?.annotationStorage.setValue(elementId, detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _dispatchPageOpen(pageNumber, initialize = false) {
|
||||
const pdfDocument = this._pdfDocument,
|
||||
async #dispatchPageOpen(pageNumber, initialize = false) {
|
||||
const pdfDocument = this.#pdfDocument,
|
||||
visitedPages = this._visitedPages;
|
||||
|
||||
if (initialize) {
|
||||
this._closeCapability = createPromiseCapability();
|
||||
this.#closeCapability = Promise.withResolvers();
|
||||
}
|
||||
if (!this._closeCapability) {
|
||||
if (!this.#closeCapability) {
|
||||
return; // Scripting isn't fully initialized yet.
|
||||
}
|
||||
const pageView = this._pdfViewer.getPageView(/* index = */ pageNumber - 1);
|
||||
const pageView = this.#pdfViewer.getPageView(/* index = */ pageNumber - 1);
|
||||
|
||||
if (pageView?.renderingState !== RenderingStates.FINISHED) {
|
||||
this._pageOpenPending.add(pageNumber);
|
||||
|
@ -375,11 +401,11 @@ class PDFScriptingManager {
|
|||
const actions = await (!visitedPages.has(pageNumber)
|
||||
? pageView.pdfPage?.getJSActions()
|
||||
: null);
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
if (pdfDocument !== this.#pdfDocument) {
|
||||
return; // The document was closed while the actions resolved.
|
||||
}
|
||||
|
||||
await this._scripting?.dispatchEventInSandbox({
|
||||
await this.#scripting?.dispatchEventInSandbox({
|
||||
id: "page",
|
||||
name: "PageOpen",
|
||||
pageNumber,
|
||||
|
@ -389,14 +415,11 @@ class PDFScriptingManager {
|
|||
visitedPages.set(pageNumber, actionsPromise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _dispatchPageClose(pageNumber) {
|
||||
const pdfDocument = this._pdfDocument,
|
||||
async #dispatchPageClose(pageNumber) {
|
||||
const pdfDocument = this.#pdfDocument,
|
||||
visitedPages = this._visitedPages;
|
||||
|
||||
if (!this._closeCapability) {
|
||||
if (!this.#closeCapability) {
|
||||
return; // Scripting isn't fully initialized yet.
|
||||
}
|
||||
if (this._pageOpenPending.has(pageNumber)) {
|
||||
|
@ -410,97 +433,64 @@ class PDFScriptingManager {
|
|||
|
||||
// Ensure that the "PageOpen" event is dispatched first.
|
||||
await actionsPromise;
|
||||
if (pdfDocument !== this._pdfDocument) {
|
||||
if (pdfDocument !== this.#pdfDocument) {
|
||||
return; // The document was closed while the actions resolved.
|
||||
}
|
||||
|
||||
await this._scripting?.dispatchEventInSandbox({
|
||||
await this.#scripting?.dispatchEventInSandbox({
|
||||
id: "page",
|
||||
name: "PageClose",
|
||||
pageNumber,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Object>} A promise that is resolved with an {Object}
|
||||
* containing the necessary document properties; please find the expected
|
||||
* format in `PDFViewerApplication._scriptingDocProperties`.
|
||||
* @private
|
||||
*/
|
||||
async _getDocProperties() {
|
||||
if (this._docPropertiesLookup) {
|
||||
return this._docPropertiesLookup(this._pdfDocument);
|
||||
}
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
|
||||
const { docPropertiesLookup } = require("./generic_scripting.js");
|
||||
#initScripting() {
|
||||
this.#destroyCapability = Promise.withResolvers();
|
||||
|
||||
return docPropertiesLookup(this._pdfDocument);
|
||||
if (this.#scripting) {
|
||||
throw new Error("#initScripting: Scripting already exists.");
|
||||
}
|
||||
throw new Error("_getDocProperties: Unable to lookup properties.");
|
||||
return this.#externalServices.createScripting();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createScripting() {
|
||||
this._destroyCapability = createPromiseCapability();
|
||||
async #destroyScripting() {
|
||||
if (!this.#scripting) {
|
||||
this.#pdfDocument = null;
|
||||
|
||||
if (this._scripting) {
|
||||
throw new Error("_createScripting: Scripting already exists.");
|
||||
}
|
||||
if (this._scriptingFactory) {
|
||||
return this._scriptingFactory.createScripting({
|
||||
sandboxBundleSrc: this._sandboxBundleSrc,
|
||||
});
|
||||
}
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
|
||||
const { GenericScripting } = require("./generic_scripting.js");
|
||||
|
||||
return new GenericScripting(this._sandboxBundleSrc);
|
||||
}
|
||||
throw new Error("_createScripting: Cannot create scripting.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _destroyScripting() {
|
||||
if (!this._scripting) {
|
||||
this._pdfDocument = null;
|
||||
|
||||
this._destroyCapability?.resolve();
|
||||
this.#destroyCapability?.resolve();
|
||||
return;
|
||||
}
|
||||
if (this._closeCapability) {
|
||||
if (this.#closeCapability) {
|
||||
await Promise.race([
|
||||
this._closeCapability.promise,
|
||||
this.#closeCapability.promise,
|
||||
new Promise(resolve => {
|
||||
// Avoid the scripting/sandbox-destruction hanging indefinitely.
|
||||
setTimeout(resolve, 1000);
|
||||
}),
|
||||
]).catch(reason => {
|
||||
]).catch(() => {
|
||||
// Ignore any errors, to ensure that the sandbox is always destroyed.
|
||||
});
|
||||
this._closeCapability = null;
|
||||
this.#closeCapability = null;
|
||||
}
|
||||
this._pdfDocument = null;
|
||||
this.#pdfDocument = null;
|
||||
|
||||
try {
|
||||
await this._scripting.destroySandbox();
|
||||
} catch (ex) {}
|
||||
await this.#scripting.destroySandbox();
|
||||
} catch {}
|
||||
|
||||
for (const [name, listener] of this._internalEvents) {
|
||||
this._eventBus._off(name, listener);
|
||||
}
|
||||
this._internalEvents.clear();
|
||||
this.#willPrintCapability?.reject(new Error("Scripting destroyed."));
|
||||
this.#willPrintCapability = null;
|
||||
|
||||
this.#eventAbortController?.abort();
|
||||
this.#eventAbortController = null;
|
||||
|
||||
this._pageOpenPending.clear();
|
||||
this._visitedPages.clear();
|
||||
|
||||
this._scripting = null;
|
||||
this._ready = false;
|
||||
this.#scripting = null;
|
||||
this.#ready = false;
|
||||
|
||||
this._destroyCapability?.resolve();
|
||||
this.#destroyCapability?.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,19 +13,25 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
/** @typedef {import("./interfaces.js").IL10n} IL10n */
|
||||
|
||||
import {
|
||||
docStyle,
|
||||
PresentationModeState,
|
||||
RenderingStates,
|
||||
SidebarView,
|
||||
toggleCheckedBtn,
|
||||
toggleExpandedBtn,
|
||||
} from "./ui_utils.js";
|
||||
|
||||
const SIDEBAR_WIDTH_VAR = "--sidebar-width";
|
||||
const SIDEBAR_MIN_WIDTH = 200; // pixels
|
||||
const SIDEBAR_RESIZING_CLASS = "sidebarResizing";
|
||||
const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFSidebarOptions
|
||||
* @property {PDFSidebarElements} elements - The DOM elements.
|
||||
* @property {PDFViewer} pdfViewer - The document viewer.
|
||||
* @property {PDFThumbnailViewer} pdfThumbnailViewer - The thumbnail viewer.
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {IL10n} l10n - The localization service.
|
||||
*/
|
||||
|
@ -38,6 +44,8 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
|
|||
* (in which the views are placed).
|
||||
* @property {HTMLButtonElement} toggleButton - The button used for
|
||||
* opening/closing the sidebar.
|
||||
* @property {HTMLDivElement} resizer - The DOM element that can be dragged in
|
||||
* order to adjust the width of the sidebar.
|
||||
* @property {HTMLButtonElement} thumbnailButton - The button used to show
|
||||
* the thumbnail view.
|
||||
* @property {HTMLButtonElement} outlineButton - The button used to show
|
||||
|
@ -54,17 +62,23 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
|
|||
* the attachments are placed.
|
||||
* @property {HTMLDivElement} layersView - The container in which
|
||||
* the layers are placed.
|
||||
* @property {HTMLDivElement} outlineOptionsContainer - The container in which
|
||||
* the outline view-specific option button(s) are placed.
|
||||
* @property {HTMLButtonElement} currentOutlineItemButton - The button used to
|
||||
* find the current outline item.
|
||||
*/
|
||||
|
||||
class PDFSidebar {
|
||||
#isRTL = false;
|
||||
|
||||
#mouseAC = null;
|
||||
|
||||
#outerContainerWidth = null;
|
||||
|
||||
#width = null;
|
||||
|
||||
/**
|
||||
* @param {PDFSidebarOptions} options
|
||||
*/
|
||||
constructor({ elements, pdfViewer, pdfThumbnailViewer, eventBus, l10n }) {
|
||||
constructor({ elements, eventBus, l10n }) {
|
||||
this.isOpen = false;
|
||||
this.active = SidebarView.THUMBS;
|
||||
this.isInitialViewSet = false;
|
||||
|
@ -75,13 +89,12 @@ class PDFSidebar {
|
|||
* the viewers (PDFViewer/PDFThumbnailViewer) are updated correctly.
|
||||
*/
|
||||
this.onToggled = null;
|
||||
|
||||
this.pdfViewer = pdfViewer;
|
||||
this.pdfThumbnailViewer = pdfThumbnailViewer;
|
||||
this.onUpdateThumbnails = null;
|
||||
|
||||
this.outerContainer = elements.outerContainer;
|
||||
this.sidebarContainer = elements.sidebarContainer;
|
||||
this.toggleButton = elements.toggleButton;
|
||||
this.resizer = elements.resizer;
|
||||
|
||||
this.thumbnailButton = elements.thumbnailButton;
|
||||
this.outlineButton = elements.outlineButton;
|
||||
|
@ -93,12 +106,12 @@ class PDFSidebar {
|
|||
this.attachmentsView = elements.attachmentsView;
|
||||
this.layersView = elements.layersView;
|
||||
|
||||
this._outlineOptionsContainer = elements.outlineOptionsContainer;
|
||||
this._currentOutlineItemButton = elements.currentOutlineItemButton;
|
||||
|
||||
this.eventBus = eventBus;
|
||||
this.l10n = l10n;
|
||||
|
||||
// NOTE
|
||||
this.#isRTL = false;
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
|
@ -155,7 +168,7 @@ class PDFSidebar {
|
|||
*/
|
||||
switchView(view, forceOpen = false) {
|
||||
const isViewChanged = view !== this.active;
|
||||
let shouldForceRendering = false;
|
||||
let forceRendering = false;
|
||||
|
||||
switch (view) {
|
||||
case SidebarView.NONE:
|
||||
|
@ -165,7 +178,7 @@ class PDFSidebar {
|
|||
return; // Closing will trigger rendering and dispatch the event.
|
||||
case SidebarView.THUMBS:
|
||||
if (this.isOpen && isViewChanged) {
|
||||
shouldForceRendering = true;
|
||||
forceRendering = true;
|
||||
}
|
||||
break;
|
||||
case SidebarView.OUTLINE:
|
||||
|
@ -191,39 +204,35 @@ class PDFSidebar {
|
|||
// in order to prevent setting it to an invalid state.
|
||||
this.active = view;
|
||||
|
||||
const isThumbs = view === SidebarView.THUMBS,
|
||||
isOutline = view === SidebarView.OUTLINE,
|
||||
isAttachments = view === SidebarView.ATTACHMENTS,
|
||||
isLayers = view === SidebarView.LAYERS;
|
||||
|
||||
// Update the CSS classes (and aria attributes), for all buttons...
|
||||
this.thumbnailButton.classList.toggle("toggled", isThumbs);
|
||||
this.outlineButton.classList.toggle("toggled", isOutline);
|
||||
this.attachmentsButton.classList.toggle("toggled", isAttachments);
|
||||
this.layersButton.classList.toggle("toggled", isLayers);
|
||||
|
||||
this.thumbnailButton.setAttribute("aria-checked", isThumbs);
|
||||
this.outlineButton.setAttribute("aria-checked", isOutline);
|
||||
this.attachmentsButton.setAttribute("aria-checked", isAttachments);
|
||||
this.layersButton.setAttribute("aria-checked", isLayers);
|
||||
// ... and for all views.
|
||||
// NOTE
|
||||
this.thumbnailView.classList.toggle("fn__hidden", !isThumbs);
|
||||
this.outlineView.classList.toggle("fn__hidden", !isOutline);
|
||||
this.attachmentsView.classList.toggle("fn__hidden", !isAttachments);
|
||||
this.layersView.classList.toggle("fn__hidden", !isLayers);
|
||||
|
||||
// Finally, update view-specific CSS classes.
|
||||
// NOTE
|
||||
this._outlineOptionsContainer.classList.toggle("fn__hidden", !isOutline);
|
||||
// Update the CSS classes (and aria attributes), for all buttons and views.
|
||||
toggleCheckedBtn(
|
||||
this.thumbnailButton,
|
||||
view === SidebarView.THUMBS,
|
||||
this.thumbnailView
|
||||
);
|
||||
toggleCheckedBtn(
|
||||
this.outlineButton,
|
||||
view === SidebarView.OUTLINE,
|
||||
this.outlineView
|
||||
);
|
||||
toggleCheckedBtn(
|
||||
this.attachmentsButton,
|
||||
view === SidebarView.ATTACHMENTS,
|
||||
this.attachmentsView
|
||||
);
|
||||
toggleCheckedBtn(
|
||||
this.layersButton,
|
||||
view === SidebarView.LAYERS,
|
||||
this.layersView
|
||||
);
|
||||
|
||||
if (forceOpen && !this.isOpen) {
|
||||
this.open();
|
||||
return; // Opening will trigger rendering and dispatch the event.
|
||||
}
|
||||
if (shouldForceRendering) {
|
||||
this.#updateThumbnailViewer();
|
||||
this.#forceRendering();
|
||||
if (forceRendering) {
|
||||
this.onUpdateThumbnails();
|
||||
this.onToggled();
|
||||
}
|
||||
if (isViewChanged) {
|
||||
this.#dispatchEvent();
|
||||
|
@ -235,46 +244,49 @@ class PDFSidebar {
|
|||
return;
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.toggleButton.classList.add("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "true");
|
||||
toggleExpandedBtn(this.toggleButton, true);
|
||||
|
||||
this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");
|
||||
|
||||
if (this.active === SidebarView.THUMBS) {
|
||||
this.#updateThumbnailViewer();
|
||||
this.onUpdateThumbnails();
|
||||
}
|
||||
this.#forceRendering();
|
||||
this.onToggled();
|
||||
this.#dispatchEvent();
|
||||
|
||||
this.#hideUINotification();
|
||||
}
|
||||
|
||||
close() {
|
||||
close(evt = null) {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.toggleButton.classList.remove("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "false");
|
||||
toggleExpandedBtn(this.toggleButton, false);
|
||||
|
||||
this.outerContainer.classList.add("sidebarMoving");
|
||||
this.outerContainer.classList.remove("sidebarOpen");
|
||||
|
||||
this.#forceRendering();
|
||||
this.onToggled();
|
||||
this.#dispatchEvent();
|
||||
|
||||
if (evt?.detail > 0) {
|
||||
// Remove focus from the toggleButton if it's clicked (see issue 17361).
|
||||
this.toggleButton.blur();
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
toggle(evt = null) {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
this.close(evt);
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
#dispatchEvent() {
|
||||
if (this.isInitialViewSet && !this.isInitialEventDispatched) {
|
||||
this.isInitialEventDispatched = true;
|
||||
if (this.isInitialViewSet) {
|
||||
this.isInitialEventDispatched ||= true;
|
||||
}
|
||||
|
||||
this.eventBus.dispatch("sidebarviewchanged", {
|
||||
|
@ -283,34 +295,13 @@ class PDFSidebar {
|
|||
});
|
||||
}
|
||||
|
||||
#forceRendering() {
|
||||
if (this.onToggled) {
|
||||
this.onToggled();
|
||||
} else {
|
||||
// Fallback
|
||||
this.pdfViewer.forceRendering();
|
||||
this.pdfThumbnailViewer.forceRendering();
|
||||
}
|
||||
}
|
||||
|
||||
#updateThumbnailViewer() {
|
||||
const { pdfViewer, pdfThumbnailViewer } = this;
|
||||
|
||||
// Use the rendered pages to set the corresponding thumbnail images.
|
||||
const pagesCount = pdfViewer.pagesCount;
|
||||
for (let pageIndex = 0; pageIndex < pagesCount; pageIndex++) {
|
||||
const pageView = pdfViewer.getPageView(pageIndex);
|
||||
if (pageView?.renderingState === RenderingStates.FINISHED) {
|
||||
const thumbnailView = pdfThumbnailViewer.getThumbnail(pageIndex);
|
||||
thumbnailView.setImage(pageView);
|
||||
}
|
||||
}
|
||||
pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);
|
||||
}
|
||||
|
||||
#showUINotification() {
|
||||
// NOTE
|
||||
this.toggleButton.title = window.siyuan.languages.toggleSidebarNotification2Title
|
||||
// this.toggleButton.setAttribute(
|
||||
// "data-l10n-id",
|
||||
// "pdfjs-toggle-sidebar-notification-button"
|
||||
// );
|
||||
|
||||
if (!this.isOpen) {
|
||||
// Only show the notification on the `toggleButton` if the sidebar is
|
||||
|
@ -329,18 +320,26 @@ class PDFSidebar {
|
|||
if (reset) {
|
||||
// NOTE
|
||||
this.toggleButton.title = window.siyuan.languages.toggleSidebarTitle
|
||||
// this.toggleButton.setAttribute(
|
||||
// "data-l10n-id",
|
||||
// "pdfjs-toggle-sidebar-button"
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
#addEventListeners() {
|
||||
const { eventBus, outerContainer } = this;
|
||||
|
||||
this.sidebarContainer.addEventListener("transitionend", evt => {
|
||||
if (evt.target === this.sidebarContainer) {
|
||||
this.outerContainer.classList.remove("sidebarMoving");
|
||||
outerContainer.classList.remove("sidebarMoving");
|
||||
// Ensure that rendering is triggered after opening/closing the sidebar.
|
||||
eventBus.dispatch("resize", { source: this });
|
||||
}
|
||||
});
|
||||
|
||||
this.toggleButton.addEventListener("click", () => {
|
||||
this.toggle();
|
||||
this.toggleButton.addEventListener("click", evt => {
|
||||
this.toggle(evt);
|
||||
});
|
||||
|
||||
// Buttons for switching views.
|
||||
|
@ -352,7 +351,7 @@ class PDFSidebar {
|
|||
this.switchView(SidebarView.OUTLINE);
|
||||
});
|
||||
this.outlineButton.addEventListener("dblclick", () => {
|
||||
this.eventBus.dispatch("toggleoutlinetree", { source: this });
|
||||
eventBus.dispatch("toggleoutlinetree", { source: this });
|
||||
});
|
||||
|
||||
this.attachmentsButton.addEventListener("click", () => {
|
||||
|
@ -363,12 +362,12 @@ class PDFSidebar {
|
|||
this.switchView(SidebarView.LAYERS);
|
||||
});
|
||||
this.layersButton.addEventListener("dblclick", () => {
|
||||
this.eventBus.dispatch("resetlayers", { source: this });
|
||||
eventBus.dispatch("resetlayers", { source: this });
|
||||
});
|
||||
|
||||
// Buttons for view-specific options.
|
||||
this._currentOutlineItemButton.addEventListener("click", () => {
|
||||
this.eventBus.dispatch("currentoutlineitem", { source: this });
|
||||
eventBus.dispatch("currentoutlineitem", { source: this });
|
||||
});
|
||||
|
||||
// Disable/enable views.
|
||||
|
@ -384,7 +383,7 @@ class PDFSidebar {
|
|||
}
|
||||
};
|
||||
|
||||
this.eventBus._on("outlineloaded", evt => {
|
||||
eventBus._on("outlineloaded", evt => {
|
||||
onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
|
||||
|
||||
evt.currentOutlineItemPromise.then(enabled => {
|
||||
|
@ -395,7 +394,7 @@ class PDFSidebar {
|
|||
});
|
||||
});
|
||||
|
||||
this.eventBus._on("attachmentsloaded", evt => {
|
||||
eventBus._on("attachmentsloaded", evt => {
|
||||
onTreeLoaded(
|
||||
evt.attachmentsCount,
|
||||
this.attachmentsButton,
|
||||
|
@ -403,19 +402,117 @@ class PDFSidebar {
|
|||
);
|
||||
});
|
||||
|
||||
this.eventBus._on("layersloaded", evt => {
|
||||
eventBus._on("layersloaded", evt => {
|
||||
onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
|
||||
});
|
||||
|
||||
// Update the thumbnailViewer, if visible, when exiting presentation mode.
|
||||
this.eventBus._on("presentationmodechanged", evt => {
|
||||
eventBus._on("presentationmodechanged", evt => {
|
||||
if (
|
||||
evt.state === PresentationModeState.NORMAL &&
|
||||
this.visibleView === SidebarView.THUMBS
|
||||
) {
|
||||
this.#updateThumbnailViewer();
|
||||
this.onUpdateThumbnails();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle resizing of the sidebar.
|
||||
this.resizer.addEventListener("mousedown", evt => {
|
||||
if (evt.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// Disable the `transition-duration` rules when sidebar resizing begins,
|
||||
// in order to improve responsiveness and to avoid visual glitches.
|
||||
outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
|
||||
|
||||
this.#mouseAC = new AbortController();
|
||||
const opts = { signal: this.#mouseAC.signal };
|
||||
|
||||
window.addEventListener("mousemove", this.#mouseMove.bind(this), opts);
|
||||
window.addEventListener("mouseup", this.#mouseUp.bind(this), opts);
|
||||
window.addEventListener("blur", this.#mouseUp.bind(this), opts);
|
||||
});
|
||||
|
||||
eventBus._on("resize", evt => {
|
||||
// When the *entire* viewer is resized, such that it becomes narrower,
|
||||
// ensure that the sidebar doesn't end up being too wide.
|
||||
if (evt.source !== window) {
|
||||
return;
|
||||
}
|
||||
// Always reset the cached width when the viewer is resized.
|
||||
this.#outerContainerWidth = null;
|
||||
|
||||
if (!this.#width) {
|
||||
// The sidebar hasn't been resized, hence no need to adjust its width.
|
||||
return;
|
||||
}
|
||||
// NOTE: If the sidebar is closed, we don't need to worry about
|
||||
// visual glitches nor ensure that rendering is triggered.
|
||||
if (!this.isOpen) {
|
||||
this.#updateWidth(this.#width);
|
||||
return;
|
||||
}
|
||||
outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
|
||||
const updated = this.#updateWidth(this.#width);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
|
||||
// Trigger rendering if the sidebar width changed, to avoid
|
||||
// depending on the order in which 'resize' events are handled.
|
||||
if (updated) {
|
||||
eventBus.dispatch("resize", { source: this });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get outerContainerWidth() {
|
||||
return (this.#outerContainerWidth ||= this.outerContainer.clientWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns {boolean} Indicating if the sidebar width was updated.
|
||||
*/
|
||||
#updateWidth(width = 0) {
|
||||
// Prevent the sidebar from becoming too narrow, or from occupying more
|
||||
// than half of the available viewer width.
|
||||
const maxWidth = Math.floor(this.outerContainerWidth / 2);
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
if (width < SIDEBAR_MIN_WIDTH) {
|
||||
width = SIDEBAR_MIN_WIDTH;
|
||||
}
|
||||
// Only update the UI when the sidebar width did in fact change.
|
||||
if (width === this.#width) {
|
||||
return false;
|
||||
}
|
||||
this.#width = width;
|
||||
|
||||
docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
|
||||
return true;
|
||||
}
|
||||
|
||||
#mouseMove(evt) {
|
||||
let width = evt.clientX;
|
||||
// For sidebar resizing to work correctly in RTL mode, invert the width.
|
||||
if (this.#isRTL) {
|
||||
width = this.outerContainerWidth - width;
|
||||
}
|
||||
this.#updateWidth(width);
|
||||
}
|
||||
|
||||
#mouseUp(evt) {
|
||||
// Re-enable the `transition-duration` rules when sidebar resizing ends...
|
||||
this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
|
||||
// ... and ensure that rendering will always be triggered.
|
||||
this.eventBus.dispatch("resize", { source: this });
|
||||
|
||||
this.#mouseAC?.abort();
|
||||
this.#mouseAC = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,23 +13,27 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
||||
|
||||
import { OutputScale, RenderingStates } from "./ui_utils.js";
|
||||
import { RenderingCancelledException } from "./pdfjs";
|
||||
import { OutputScale, RenderingCancelledException } from "./pdfjs";
|
||||
import { RenderingStates } from "./ui_utils.js";
|
||||
|
||||
const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
|
||||
const MAX_NUM_SCALING_STEPS = 3;
|
||||
const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px
|
||||
const THUMBNAIL_WIDTH = 98; // px
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFThumbnailViewOptions
|
||||
* @property {HTMLDivElement} container - The viewer element.
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {number} id - The thumbnail's unique ID (normally its number).
|
||||
* @property {PageViewport} defaultViewport - The page viewport.
|
||||
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
|
||||
|
@ -37,10 +41,11 @@ const THUMBNAIL_WIDTH = 98; // px
|
|||
* The default value is `null`.
|
||||
* @property {IPDFLinkService} linkService - The navigation/linking service.
|
||||
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
|
||||
* @property {IL10n} l10n - Localization service.
|
||||
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
||||
* with user defined ones in order to improve readability in high contrast
|
||||
* mode.
|
||||
* @property {boolean} [enableHWA] - Enables hardware acceleration for
|
||||
* rendering. The default value is `false`.
|
||||
*/
|
||||
|
||||
class TempImageFactory {
|
||||
|
@ -52,7 +57,7 @@ class TempImageFactory {
|
|||
tempCanvas.height = height;
|
||||
|
||||
// Since this is a temporary canvas, we need to fill it with a white
|
||||
// background ourselves. `_getPageDrawContext` uses CSS rules for this.
|
||||
// background ourselves. `#getPageDrawContext` uses CSS rules for this.
|
||||
const ctx = tempCanvas.getContext("2d", { alpha: false });
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgb(255, 255, 255)";
|
||||
|
@ -82,13 +87,14 @@ class PDFThumbnailView {
|
|||
*/
|
||||
constructor({
|
||||
container,
|
||||
eventBus,
|
||||
id,
|
||||
defaultViewport,
|
||||
optionalContentConfigPromise,
|
||||
linkService,
|
||||
renderingQueue,
|
||||
l10n,
|
||||
pageColors,
|
||||
enableHWA,
|
||||
}) {
|
||||
this.id = id;
|
||||
this.renderingId = "thumbnail" + id;
|
||||
|
@ -100,7 +106,9 @@ class PDFThumbnailView {
|
|||
this.pdfPageRotate = defaultViewport.rotation;
|
||||
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
|
||||
this.pageColors = pageColors || null;
|
||||
this.enableHWA = enableHWA || false;
|
||||
|
||||
this.eventBus = eventBus;
|
||||
this.linkService = linkService;
|
||||
this.renderingQueue = renderingQueue;
|
||||
|
||||
|
@ -108,20 +116,12 @@ class PDFThumbnailView {
|
|||
this.renderingState = RenderingStates.INITIAL;
|
||||
this.resume = null;
|
||||
|
||||
const pageWidth = this.viewport.width,
|
||||
pageHeight = this.viewport.height,
|
||||
pageRatio = pageWidth / pageHeight;
|
||||
|
||||
this.canvasWidth = THUMBNAIL_WIDTH;
|
||||
this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
|
||||
this.scale = this.canvasWidth / pageWidth;
|
||||
|
||||
this.l10n = l10n;
|
||||
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = linkService.getAnchorUrl("#page=" + id);
|
||||
// NOTE
|
||||
anchor.title = this._thumbPageTitle
|
||||
anchor.title = window.siyuan.languages.thumbPageTitle.replace('{{page}}', JSON.parse(this.#pageL10nArgs).page)
|
||||
anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
|
||||
anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
anchor.onclick = function () {
|
||||
linkService.goToPage(id);
|
||||
return false;
|
||||
|
@ -132,19 +132,30 @@ class PDFThumbnailView {
|
|||
div.className = "thumbnail";
|
||||
div.setAttribute("data-page-number", this.id);
|
||||
this.div = div;
|
||||
this.#updateDims();
|
||||
|
||||
const ring = document.createElement("div");
|
||||
ring.className = "thumbnailSelectionRing";
|
||||
const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
|
||||
ring.style.width = this.canvasWidth + borderAdjustment + "px";
|
||||
ring.style.height = this.canvasHeight + borderAdjustment + "px";
|
||||
this.ring = ring;
|
||||
const img = document.createElement("div");
|
||||
img.className = "thumbnailImage";
|
||||
this._placeholderImg = img;
|
||||
|
||||
div.append(ring);
|
||||
div.append(img);
|
||||
anchor.append(div);
|
||||
container.append(anchor);
|
||||
}
|
||||
|
||||
#updateDims() {
|
||||
const { width, height } = this.viewport;
|
||||
const ratio = width / height;
|
||||
|
||||
this.canvasWidth = THUMBNAIL_WIDTH;
|
||||
this.canvasHeight = (this.canvasWidth / ratio) | 0;
|
||||
this.scale = this.canvasWidth / width;
|
||||
|
||||
const { style } = this.div;
|
||||
style.setProperty("--thumbnail-width", `${this.canvasWidth}px`);
|
||||
style.setProperty("--thumbnail-height", `${this.canvasHeight}px`);
|
||||
}
|
||||
|
||||
setPdfPage(pdfPage) {
|
||||
this.pdfPage = pdfPage;
|
||||
this.pdfPageRotate = pdfPage.rotate;
|
||||
|
@ -157,27 +168,10 @@ class PDFThumbnailView {
|
|||
this.cancelRendering();
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
|
||||
const pageWidth = this.viewport.width,
|
||||
pageHeight = this.viewport.height,
|
||||
pageRatio = pageWidth / pageHeight;
|
||||
|
||||
this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
|
||||
this.scale = this.canvasWidth / pageWidth;
|
||||
|
||||
this.div.removeAttribute("data-loaded");
|
||||
const ring = this.ring;
|
||||
ring.textContent = ""; // Remove the thumbnail from the DOM.
|
||||
const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
|
||||
ring.style.width = this.canvasWidth + borderAdjustment + "px";
|
||||
ring.style.height = this.canvasHeight + borderAdjustment + "px";
|
||||
this.image?.replaceWith(this._placeholderImg);
|
||||
this.#updateDims();
|
||||
|
||||
if (this.canvas) {
|
||||
// Zeroing the width and height causes Firefox to release graphics
|
||||
// resources immediately, which can greatly reduce memory consumption.
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
delete this.canvas;
|
||||
}
|
||||
if (this.image) {
|
||||
this.image.removeAttribute("src");
|
||||
delete this.image;
|
||||
|
@ -208,14 +202,14 @@ class PDFThumbnailView {
|
|||
this.resume = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getPageDrawContext(upscaleFactor = 1) {
|
||||
#getPageDrawContext(upscaleFactor = 1, enableHWA = this.enableHWA) {
|
||||
// Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
|
||||
// until rendering/image conversion is complete, to avoid display issues.
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d", { alpha: false });
|
||||
const ctx = canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
willReadFrequently: !enableHWA,
|
||||
});
|
||||
const outputScale = new OutputScale();
|
||||
|
||||
canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0;
|
||||
|
@ -228,27 +222,21 @@ class PDFThumbnailView {
|
|||
return { ctx, canvas, transform };
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_convertCanvasToImage(canvas) {
|
||||
#convertCanvasToImage(canvas) {
|
||||
if (this.renderingState !== RenderingStates.FINISHED) {
|
||||
throw new Error("_convertCanvasToImage: Rendering has not finished.");
|
||||
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
||||
}
|
||||
const reducedCanvas = this._reduceImage(canvas);
|
||||
const reducedCanvas = this.#reduceImage(canvas);
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.className = "thumbnailImage";
|
||||
// NOTE
|
||||
image.setAttribute("aria-label", this._thumbPageCanvas);
|
||||
image.style.width = this.canvasWidth + "px";
|
||||
image.style.height = this.canvasHeight + "px";
|
||||
|
||||
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
||||
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
image.src = reducedCanvas.toDataURL();
|
||||
this.image = image;
|
||||
|
||||
this.div.setAttribute("data-loaded", true);
|
||||
this.ring.append(image);
|
||||
this._placeholderImg.replaceWith(image);
|
||||
|
||||
// Zeroing the width and height causes Firefox to release graphics
|
||||
// resources immediately, which can greatly reduce memory consumption.
|
||||
|
@ -256,46 +244,46 @@ class PDFThumbnailView {
|
|||
reducedCanvas.height = 0;
|
||||
}
|
||||
|
||||
draw() {
|
||||
async #finishRenderTask(renderTask, canvas, error = null) {
|
||||
// The renderTask may have been replaced by a new one, so only remove
|
||||
// the reference to the renderTask if it matches the one that is
|
||||
// triggering this callback.
|
||||
if (renderTask === this.renderTask) {
|
||||
this.renderTask = null;
|
||||
}
|
||||
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
return;
|
||||
}
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
this.#convertCanvasToImage(canvas);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async draw() {
|
||||
if (this.renderingState !== RenderingStates.INITIAL) {
|
||||
console.error("Must be in new state before drawing");
|
||||
return Promise.resolve();
|
||||
return undefined;
|
||||
}
|
||||
const { pdfPage } = this;
|
||||
|
||||
if (!pdfPage) {
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
return Promise.reject(new Error("pdfPage is not loaded"));
|
||||
throw new Error("pdfPage is not loaded");
|
||||
}
|
||||
|
||||
this.renderingState = RenderingStates.RUNNING;
|
||||
|
||||
const finishRenderTask = async (error = null) => {
|
||||
// The renderTask may have been replaced by a new one, so only remove
|
||||
// the reference to the renderTask if it matches the one that is
|
||||
// triggering this callback.
|
||||
if (renderTask === this.renderTask) {
|
||||
this.renderTask = null;
|
||||
}
|
||||
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
return;
|
||||
}
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
this._convertCanvasToImage(canvas);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Render the thumbnail at a larger size and downsize the canvas (similar
|
||||
// to `setImage`), to improve consistency between thumbnails created by
|
||||
// the `draw` and `setImage` methods (fixes issue 8233).
|
||||
// NOTE: To primarily avoid increasing memory usage too much, but also to
|
||||
// reduce downsizing overhead, we purposely limit the up-scaling factor.
|
||||
const { ctx, canvas, transform } =
|
||||
this._getPageDrawContext(DRAW_UPSCALE_FACTOR);
|
||||
this.#getPageDrawContext(DRAW_UPSCALE_FACTOR);
|
||||
const drawViewport = this.viewport.clone({
|
||||
scale: DRAW_UPSCALE_FACTOR * this.scale,
|
||||
});
|
||||
|
@ -322,12 +310,8 @@ class PDFThumbnailView {
|
|||
renderTask.onContinue = renderContinueCallback;
|
||||
|
||||
const resultPromise = renderTask.promise.then(
|
||||
function () {
|
||||
return finishRenderTask(null);
|
||||
},
|
||||
function (error) {
|
||||
return finishRenderTask(error);
|
||||
}
|
||||
() => this.#finishRenderTask(renderTask, canvas),
|
||||
error => this.#finishRenderTask(renderTask, canvas, error)
|
||||
);
|
||||
resultPromise.finally(() => {
|
||||
// Zeroing the width and height causes Firefox to release graphics
|
||||
|
@ -335,12 +319,11 @@ class PDFThumbnailView {
|
|||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
|
||||
// Only trigger cleanup, once rendering has finished, when the current
|
||||
// pageView is *not* cached on the `BaseViewer`-instance.
|
||||
const pageCached = this.linkService.isPageCached(this.id);
|
||||
if (!pageCached) {
|
||||
this.pdfPage?.cleanup();
|
||||
}
|
||||
this.eventBus.dispatch("thumbnailrendered", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
pdfPage: this.pdfPage,
|
||||
});
|
||||
});
|
||||
|
||||
return resultPromise;
|
||||
|
@ -362,14 +345,11 @@ class PDFThumbnailView {
|
|||
return;
|
||||
}
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
this._convertCanvasToImage(canvas);
|
||||
this.#convertCanvasToImage(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_reduceImage(img) {
|
||||
const { ctx, canvas } = this._getPageDrawContext();
|
||||
#reduceImage(img) {
|
||||
const { ctx, canvas } = this.#getPageDrawContext(1, true);
|
||||
|
||||
if (img.width <= 2 * canvas.width) {
|
||||
ctx.drawImage(
|
||||
|
@ -437,16 +417,8 @@ class PDFThumbnailView {
|
|||
return canvas;
|
||||
}
|
||||
|
||||
get _thumbPageTitle() {
|
||||
// NOTE
|
||||
return window.siyuan.languages.thumbPageTitle.replace('{{page}}',
|
||||
this.pageLabel ?? this.id)
|
||||
}
|
||||
|
||||
get _thumbPageCanvas() {
|
||||
// NOTE
|
||||
return window.siyuan.languages.thumbPage.replace('{{page}}',
|
||||
this.pageLabel ?? this.id)
|
||||
get #pageL10nArgs() {
|
||||
return JSON.stringify({ page: this.pageLabel ?? this.id });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -456,12 +428,13 @@ class PDFThumbnailView {
|
|||
this.pageLabel = typeof label === "string" ? label : null;
|
||||
|
||||
// NOTE
|
||||
this.anchor.title = this._thumbPageTitle
|
||||
this.anchor.title = window.siyuan.languages.thumbPageTitle.replace('{{page}}', JSON.parse(this.#pageL10nArgs).page)
|
||||
this.anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
|
||||
if (this.renderingState !== RenderingStates.FINISHED) {
|
||||
return;
|
||||
}
|
||||
this.image?.setAttribute('aria-label', this._thumbPageCanvas)
|
||||
this.image?.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
*/
|
||||
|
||||
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
|
||||
/** @typedef {import("./interfaces").IL10n} IL10n */
|
||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
||||
|
@ -35,12 +36,16 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
|
|||
* @typedef {Object} PDFThumbnailViewerOptions
|
||||
* @property {HTMLDivElement} container - The container for the thumbnail
|
||||
* elements.
|
||||
* @property {EventBus} eventBus - The application event bus.
|
||||
* @property {IPDFLinkService} linkService - The navigation/linking service.
|
||||
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
|
||||
* @property {IL10n} l10n - Localization service.
|
||||
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
||||
* with user defined ones in order to improve readability in high contrast
|
||||
* mode.
|
||||
* @property {AbortSignal} [abortSignal] - The AbortSignal for the window
|
||||
* events.
|
||||
* @property {boolean} [enableHWA] - Enables hardware acceleration for
|
||||
* rendering. The default value is `false`.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -50,38 +55,31 @@ class PDFThumbnailViewer {
|
|||
/**
|
||||
* @param {PDFThumbnailViewerOptions} options
|
||||
*/
|
||||
constructor({ container, linkService, renderingQueue, l10n, pageColors }) {
|
||||
constructor({
|
||||
container,
|
||||
eventBus,
|
||||
linkService,
|
||||
renderingQueue,
|
||||
pageColors,
|
||||
abortSignal,
|
||||
enableHWA,
|
||||
}) {
|
||||
this.container = container;
|
||||
this.eventBus = eventBus;
|
||||
this.linkService = linkService;
|
||||
this.renderingQueue = renderingQueue;
|
||||
this.l10n = l10n;
|
||||
this.pageColors = pageColors || null;
|
||||
this.enableHWA = enableHWA || false;
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
if (
|
||||
this.pageColors &&
|
||||
!(
|
||||
CSS.supports("color", this.pageColors.background) &&
|
||||
CSS.supports("color", this.pageColors.foreground)
|
||||
)
|
||||
) {
|
||||
if (this.pageColors.background || this.pageColors.foreground) {
|
||||
console.warn(
|
||||
"PDFThumbnailViewer: Ignoring `pageColors`-option, since the browser doesn't support the values used."
|
||||
);
|
||||
}
|
||||
this.pageColors = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this));
|
||||
this._resetView();
|
||||
this.scroll = watchScroll(
|
||||
this.container,
|
||||
this.#scrollUpdated.bind(this),
|
||||
abortSignal
|
||||
);
|
||||
this.#resetView();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_scrollUpdated() {
|
||||
#scrollUpdated() {
|
||||
this.renderingQueue.renderHighestPriority();
|
||||
}
|
||||
|
||||
|
@ -89,10 +87,7 @@ class PDFThumbnailViewer {
|
|||
return this._thumbnails[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getVisibleThumbs() {
|
||||
#getVisibleThumbs() {
|
||||
return getVisibleElements({
|
||||
scrollEl: this.container,
|
||||
views: this._thumbnails,
|
||||
|
@ -117,7 +112,7 @@ class PDFThumbnailViewer {
|
|||
// ... and add the highlight to the new thumbnail.
|
||||
thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
|
||||
}
|
||||
const { first, last, views } = this._getVisibleThumbs();
|
||||
const { first, last, views } = this.#getVisibleThumbs();
|
||||
|
||||
// If the thumbnail isn't currently visible, scroll it into view.
|
||||
if (views.length > 0) {
|
||||
|
@ -172,10 +167,7 @@ class PDFThumbnailViewer {
|
|||
TempImageFactory.destroyCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_resetView() {
|
||||
#resetView() {
|
||||
this._thumbnails = [];
|
||||
this._currentPageNumber = 1;
|
||||
this._pageLabels = null;
|
||||
|
@ -190,8 +182,8 @@ class PDFThumbnailViewer {
|
|||
*/
|
||||
setDocument(pdfDocument) {
|
||||
if (this.pdfDocument) {
|
||||
this._cancelRendering();
|
||||
this._resetView();
|
||||
this.#cancelRendering();
|
||||
this.#resetView();
|
||||
}
|
||||
|
||||
this.pdfDocument = pdfDocument;
|
||||
|
@ -199,7 +191,9 @@ class PDFThumbnailViewer {
|
|||
return;
|
||||
}
|
||||
const firstPagePromise = pdfDocument.getPage(1);
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
|
||||
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({
|
||||
intent: "display",
|
||||
});
|
||||
|
||||
firstPagePromise
|
||||
.then(firstPdfPage => {
|
||||
|
@ -209,13 +203,14 @@ class PDFThumbnailViewer {
|
|||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||
const thumbnail = new PDFThumbnailView({
|
||||
container: this.container,
|
||||
eventBus: this.eventBus,
|
||||
id: pageNum,
|
||||
defaultViewport: viewport.clone(),
|
||||
optionalContentConfigPromise,
|
||||
linkService: this.linkService,
|
||||
renderingQueue: this.renderingQueue,
|
||||
l10n: this.l10n,
|
||||
pageColors: this.pageColors,
|
||||
enableHWA: this.enableHWA,
|
||||
});
|
||||
this._thumbnails.push(thumbnail);
|
||||
}
|
||||
|
@ -233,10 +228,7 @@ class PDFThumbnailViewer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_cancelRendering() {
|
||||
#cancelRendering() {
|
||||
for (const thumbnail of this._thumbnails) {
|
||||
thumbnail.cancelRendering();
|
||||
}
|
||||
|
@ -295,7 +287,7 @@ class PDFThumbnailViewer {
|
|||
}
|
||||
|
||||
forceRendering() {
|
||||
const visibleThumbs = this._getVisibleThumbs();
|
||||
const visibleThumbs = this.#getVisibleThumbs();
|
||||
const scrollAhead = this.#getScrollAhead(visibleThumbs);
|
||||
const thumbView = this.renderingQueue.getHighestPriority(
|
||||
visibleThumbs,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -16,7 +16,4 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const {addScriptSync} = require('../../protyle/util/addScript')
|
||||
const {Constants} = require('../../constants')
|
||||
addScriptSync(`${Constants.PROTYLE_CDN}/js/pdf/pdf.js?v=3.5.141`, 'pdfjsScript')
|
||||
module.exports = window["pdfjs-dist/build/pdf"];
|
||||
module.exports = window.pdfjsLib;
|
||||
|
|
|
@ -22,17 +22,18 @@ import { AppOptions, OptionKind } from "./app_options.js";
|
|||
*/
|
||||
class BasePreferences {
|
||||
#defaults = Object.freeze(
|
||||
typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")
|
||||
? AppOptions.getAll(OptionKind.PREFERENCE)
|
||||
typeof PDFJSDev === "undefined"
|
||||
? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true)
|
||||
: PDFJSDev.eval("DEFAULT_PREFERENCES")
|
||||
);
|
||||
|
||||
#prefs = Object.create(null);
|
||||
|
||||
#initializedPromise = null;
|
||||
|
||||
constructor() {
|
||||
if (this.constructor === BasePreferences) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
|
||||
this.constructor === BasePreferences
|
||||
) {
|
||||
throw new Error("Cannot initialize BasePreferences.");
|
||||
}
|
||||
|
||||
|
@ -45,16 +46,26 @@ class BasePreferences {
|
|||
}
|
||||
|
||||
this.#initializedPromise = this._readFromStorage(this.#defaults).then(
|
||||
prefs => {
|
||||
for (const name in this.#defaults) {
|
||||
const prefValue = prefs?.[name];
|
||||
// Ignore preferences whose types don't match the default values.
|
||||
if (typeof prefValue === typeof this.#defaults[name]) {
|
||||
this.#prefs[name] = prefValue;
|
||||
}
|
||||
({ browserPrefs, prefs }) => {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
||||
AppOptions._checkDisablePreferences()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
AppOptions.setAll({ ...browserPrefs, ...prefs }, /* prefs = */ true);
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
window.addEventListener(
|
||||
"updatedPreference",
|
||||
async ({ detail: { name, value } }) => {
|
||||
await this.#initializedPromise;
|
||||
AppOptions.setAll({ [name]: value }, /* prefs = */ true);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,15 +94,13 @@ class BasePreferences {
|
|||
* have been reset.
|
||||
*/
|
||||
async reset() {
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
throw new Error("Please use `about:config` to change preferences.");
|
||||
}
|
||||
await this.#initializedPromise;
|
||||
const prefs = this.#prefs;
|
||||
AppOptions.setAll(this.#defaults, /* prefs = */ true);
|
||||
|
||||
this.#prefs = Object.create(null);
|
||||
return this._writeToStorage(this.#defaults).catch(reason => {
|
||||
// Revert all preference values, since writing to storage failed.
|
||||
this.#prefs = prefs;
|
||||
throw reason;
|
||||
});
|
||||
await this._writeToStorage(this.#defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,37 +112,13 @@ class BasePreferences {
|
|||
*/
|
||||
async set(name, value) {
|
||||
await this.#initializedPromise;
|
||||
const defaultValue = this.#defaults[name],
|
||||
prefs = this.#prefs;
|
||||
AppOptions.setAll({ [name]: value }, /* prefs = */ true);
|
||||
|
||||
if (defaultValue === undefined) {
|
||||
throw new Error(`Set preference: "${name}" is undefined.`);
|
||||
} else if (value === undefined) {
|
||||
throw new Error("Set preference: no value is specified.");
|
||||
}
|
||||
const valueType = typeof value,
|
||||
defaultType = typeof defaultValue;
|
||||
|
||||
if (valueType !== defaultType) {
|
||||
if (valueType === "number" && defaultType === "string") {
|
||||
value = value.toString();
|
||||
} else {
|
||||
throw new Error(
|
||||
`Set preference: "${value}" is a ${valueType}, expected a ${defaultType}.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (valueType === "number" && !Number.isInteger(value)) {
|
||||
throw new Error(`Set preference: "${value}" must be an integer.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.#prefs[name] = value;
|
||||
return this._writeToStorage(this.#prefs).catch(reason => {
|
||||
// Revert all preference values, since writing to storage failed.
|
||||
this.#prefs = prefs;
|
||||
throw reason;
|
||||
});
|
||||
await this._writeToStorage(
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? { [name]: AppOptions.get(name) }
|
||||
: AppOptions.getAll(OptionKind.PREFERENCE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,28 +128,15 @@ class BasePreferences {
|
|||
* containing the value of the preference.
|
||||
*/
|
||||
async get(name) {
|
||||
await this.#initializedPromise;
|
||||
const defaultValue = this.#defaults[name];
|
||||
|
||||
if (defaultValue === undefined) {
|
||||
throw new Error(`Get preference: "${name}" is undefined.`);
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
throw new Error("Not implemented: get");
|
||||
}
|
||||
return this.#prefs[name] ?? defaultValue;
|
||||
await this.#initializedPromise;
|
||||
return AppOptions.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the values of all preferences.
|
||||
* @returns {Promise} A promise that is resolved with an {Object} containing
|
||||
* the values of all preferences.
|
||||
*/
|
||||
async getAll() {
|
||||
await this.#initializedPromise;
|
||||
const obj = Object.create(null);
|
||||
|
||||
for (const name in this.#defaults) {
|
||||
obj[name] = this.#prefs[name] ?? this.#defaults[name];
|
||||
}
|
||||
return obj;
|
||||
get initializedPromise() {
|
||||
return this.#initializedPromise;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
43
app/src/asset/pdf/print_utils.js
Normal file
43
app/src/asset/pdf/print_utils.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* Copyright 2021 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getXfaPageViewport, PixelsPerInch } from "./pdfjs";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||
|
||||
function getXfaHtmlForPrinting(printContainer, pdfDocument) {
|
||||
const xfaHtml = pdfDocument.allXfaHtml;
|
||||
const linkService = new SimpleLinkService();
|
||||
const scale = Math.round(PixelsPerInch.PDF_TO_CSS_UNITS * 100) / 100;
|
||||
|
||||
for (const xfaPage of xfaHtml.children) {
|
||||
const page = document.createElement("div");
|
||||
page.className = "xfaPrintedPage";
|
||||
printContainer.append(page);
|
||||
|
||||
const builder = new XfaLayerBuilder({
|
||||
pdfPage: null,
|
||||
annotationStorage: pdfDocument.annotationStorage,
|
||||
linkService,
|
||||
xfaHtml: xfaPage,
|
||||
});
|
||||
const viewport = getXfaPageViewport(xfaPage, { scale });
|
||||
|
||||
builder.render(viewport, "print");
|
||||
page.append(builder.div);
|
||||
}
|
||||
}
|
||||
|
||||
export { getXfaHtmlForPrinting };
|
|
@ -13,7 +13,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CursorTool, ScrollMode, SpreadMode } from "./ui_utils.js";
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
|
||||
import {
|
||||
CursorTool,
|
||||
ScrollMode,
|
||||
SpreadMode,
|
||||
toggleCheckedBtn,
|
||||
toggleExpandedBtn,
|
||||
} from "./ui_utils.js";
|
||||
import { PagesCountLimit } from "./pdf_viewer.js";
|
||||
|
||||
/**
|
||||
|
@ -41,19 +49,22 @@ import { PagesCountLimit } from "./pdf_viewer.js";
|
|||
* select tool.
|
||||
* @property {HTMLButtonElement} cursorHandToolButton - Button to enable the
|
||||
* hand tool.
|
||||
* @property {HTMLButtonElement} imageAltTextSettingsButton - Button for opening
|
||||
* the image alt-text settings dialog.
|
||||
* @property {HTMLButtonElement} documentPropertiesButton - Button for opening
|
||||
* the document properties dialog.
|
||||
*/
|
||||
|
||||
class SecondaryToolbar {
|
||||
#opts;
|
||||
|
||||
/**
|
||||
* @param {SecondaryToolbarOptions} options
|
||||
* @param {EventBus} eventBus
|
||||
*/
|
||||
constructor(options, eventBus, externalServices) {
|
||||
this.toolbar = options.toolbar;
|
||||
this.toggleButton = options.toggleButton;
|
||||
this.buttons = [
|
||||
constructor(options, eventBus) {
|
||||
this.#opts = options;
|
||||
const buttons = [
|
||||
{
|
||||
element: options.presentationModeButton,
|
||||
eventName: "presentationmode",
|
||||
|
@ -128,37 +139,31 @@ class SecondaryToolbar {
|
|||
eventDetails: { mode: SpreadMode.EVEN },
|
||||
close: true,
|
||||
},
|
||||
{
|
||||
element: options.imageAltTextSettingsButton,
|
||||
eventName: "imagealttextsettings",
|
||||
close: true,
|
||||
},
|
||||
{
|
||||
element: options.documentPropertiesButton,
|
||||
eventName: "documentproperties",
|
||||
close: true,
|
||||
},
|
||||
];
|
||||
// NOTE
|
||||
// if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
// this.buttons.push({
|
||||
// element: options.openFileButton,
|
||||
// eventName: "openfile",
|
||||
// close: true,
|
||||
// });
|
||||
// }
|
||||
this.items = {
|
||||
firstPage: options.firstPageButton,
|
||||
lastPage: options.lastPageButton,
|
||||
pageRotateCw: options.pageRotateCwButton,
|
||||
pageRotateCcw: options.pageRotateCcwButton,
|
||||
};
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
buttons.push({
|
||||
element: options.openFileButton,
|
||||
eventName: "openfile",
|
||||
close: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.eventBus = eventBus;
|
||||
this.externalServices = externalServices;
|
||||
this.opened = false;
|
||||
|
||||
// Bind the event listeners for click, cursor tool, and scroll/spread mode
|
||||
// actions.
|
||||
this.#bindClickListeners();
|
||||
this.#bindCursorToolsListener(options);
|
||||
this.#bindScrollModeListener(options);
|
||||
this.#bindSpreadModeListener(options);
|
||||
this.#bindListeners(buttons);
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
@ -186,124 +191,104 @@ class SecondaryToolbar {
|
|||
this.#updateUIState();
|
||||
|
||||
// Reset the Scroll/Spread buttons too, since they're document specific.
|
||||
this.eventBus.dispatch("secondarytoolbarreset", { source: this });
|
||||
this.eventBus.dispatch("switchcursortool", { source: this, reset: true });
|
||||
this.#scrollModeChanged({ mode: ScrollMode.VERTICAL });
|
||||
this.#spreadModeChanged({ mode: SpreadMode.NONE });
|
||||
}
|
||||
|
||||
#updateUIState() {
|
||||
this.items.firstPage.disabled = this.pageNumber <= 1;
|
||||
this.items.lastPage.disabled = this.pageNumber >= this.pagesCount;
|
||||
this.items.pageRotateCw.disabled = this.pagesCount === 0;
|
||||
this.items.pageRotateCcw.disabled = this.pagesCount === 0;
|
||||
const {
|
||||
firstPageButton,
|
||||
lastPageButton,
|
||||
pageRotateCwButton,
|
||||
pageRotateCcwButton,
|
||||
} = this.#opts;
|
||||
|
||||
firstPageButton.disabled = this.pageNumber <= 1;
|
||||
lastPageButton.disabled = this.pageNumber >= this.pagesCount;
|
||||
pageRotateCwButton.disabled = this.pagesCount === 0;
|
||||
pageRotateCcwButton.disabled = this.pagesCount === 0;
|
||||
}
|
||||
|
||||
#bindClickListeners() {
|
||||
#bindListeners(buttons) {
|
||||
const { eventBus } = this;
|
||||
const { toggleButton } = this.#opts;
|
||||
// Button to toggle the visibility of the secondary toolbar.
|
||||
this.toggleButton.addEventListener("click", this.toggle.bind(this));
|
||||
toggleButton.addEventListener("click", this.toggle.bind(this));
|
||||
|
||||
// All items within the secondary toolbar.
|
||||
for (const { element, eventName, close, eventDetails } of this.buttons) {
|
||||
for (const { element, eventName, close, eventDetails } of buttons) {
|
||||
element.addEventListener("click", evt => {
|
||||
if (eventName !== null) {
|
||||
this.eventBus.dispatch(eventName, { source: this, ...eventDetails });
|
||||
eventBus.dispatch(eventName, { source: this, ...eventDetails });
|
||||
}
|
||||
if (close) {
|
||||
this.close();
|
||||
}
|
||||
this.externalServices.reportTelemetry({
|
||||
type: "buttons",
|
||||
data: { id: element.id },
|
||||
eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: {
|
||||
type: "buttons",
|
||||
data: { id: element.id },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
eventBus._on("cursortoolchanged", this.#cursorToolChanged.bind(this));
|
||||
eventBus._on("scrollmodechanged", this.#scrollModeChanged.bind(this));
|
||||
eventBus._on("spreadmodechanged", this.#spreadModeChanged.bind(this));
|
||||
}
|
||||
|
||||
#bindCursorToolsListener({ cursorSelectToolButton, cursorHandToolButton }) {
|
||||
this.eventBus._on("cursortoolchanged", function ({ tool }) {
|
||||
const isSelect = tool === CursorTool.SELECT,
|
||||
isHand = tool === CursorTool.HAND;
|
||||
#cursorToolChanged({ tool, disabled }) {
|
||||
const { cursorSelectToolButton, cursorHandToolButton } = this.#opts;
|
||||
|
||||
cursorSelectToolButton.classList.toggle("toggled", isSelect);
|
||||
cursorHandToolButton.classList.toggle("toggled", isHand);
|
||||
toggleCheckedBtn(cursorSelectToolButton, tool === CursorTool.SELECT);
|
||||
toggleCheckedBtn(cursorHandToolButton, tool === CursorTool.HAND);
|
||||
|
||||
cursorSelectToolButton.setAttribute("aria-checked", isSelect);
|
||||
cursorHandToolButton.setAttribute("aria-checked", isHand);
|
||||
});
|
||||
cursorSelectToolButton.disabled = disabled;
|
||||
cursorHandToolButton.disabled = disabled;
|
||||
}
|
||||
|
||||
#bindScrollModeListener({
|
||||
scrollPageButton,
|
||||
scrollVerticalButton,
|
||||
scrollHorizontalButton,
|
||||
scrollWrappedButton,
|
||||
spreadNoneButton,
|
||||
spreadOddButton,
|
||||
spreadEvenButton,
|
||||
}) {
|
||||
const scrollModeChanged = ({ mode }) => {
|
||||
const isPage = mode === ScrollMode.PAGE,
|
||||
isVertical = mode === ScrollMode.VERTICAL,
|
||||
isHorizontal = mode === ScrollMode.HORIZONTAL,
|
||||
isWrapped = mode === ScrollMode.WRAPPED;
|
||||
#scrollModeChanged({ mode }) {
|
||||
const {
|
||||
scrollPageButton,
|
||||
scrollVerticalButton,
|
||||
scrollHorizontalButton,
|
||||
scrollWrappedButton,
|
||||
spreadNoneButton,
|
||||
spreadOddButton,
|
||||
spreadEvenButton,
|
||||
} = this.#opts;
|
||||
|
||||
scrollPageButton.classList.toggle("toggled", isPage);
|
||||
scrollVerticalButton.classList.toggle("toggled", isVertical);
|
||||
scrollHorizontalButton.classList.toggle("toggled", isHorizontal);
|
||||
scrollWrappedButton.classList.toggle("toggled", isWrapped);
|
||||
toggleCheckedBtn(scrollPageButton, mode === ScrollMode.PAGE);
|
||||
toggleCheckedBtn(scrollVerticalButton, mode === ScrollMode.VERTICAL);
|
||||
toggleCheckedBtn(scrollHorizontalButton, mode === ScrollMode.HORIZONTAL);
|
||||
toggleCheckedBtn(scrollWrappedButton, mode === ScrollMode.WRAPPED);
|
||||
|
||||
scrollPageButton.setAttribute("aria-checked", isPage);
|
||||
scrollVerticalButton.setAttribute("aria-checked", isVertical);
|
||||
scrollHorizontalButton.setAttribute("aria-checked", isHorizontal);
|
||||
scrollWrappedButton.setAttribute("aria-checked", isWrapped);
|
||||
// Permanently *disable* the Scroll buttons when PAGE-scrolling is being
|
||||
// enforced for *very* long/large documents; please see the `BaseViewer`.
|
||||
const forceScrollModePage =
|
||||
this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE;
|
||||
scrollPageButton.disabled = forceScrollModePage;
|
||||
scrollVerticalButton.disabled = forceScrollModePage;
|
||||
scrollHorizontalButton.disabled = forceScrollModePage;
|
||||
scrollWrappedButton.disabled = forceScrollModePage;
|
||||
|
||||
// Permanently *disable* the Scroll buttons when PAGE-scrolling is being
|
||||
// enforced for *very* long/large documents; please see the `BaseViewer`.
|
||||
const forceScrollModePage =
|
||||
this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE;
|
||||
scrollPageButton.disabled = forceScrollModePage;
|
||||
scrollVerticalButton.disabled = forceScrollModePage;
|
||||
scrollHorizontalButton.disabled = forceScrollModePage;
|
||||
scrollWrappedButton.disabled = forceScrollModePage;
|
||||
|
||||
// Temporarily *disable* the Spread buttons when horizontal scrolling is
|
||||
// enabled, since the non-default Spread modes doesn't affect the layout.
|
||||
spreadNoneButton.disabled = isHorizontal;
|
||||
spreadOddButton.disabled = isHorizontal;
|
||||
spreadEvenButton.disabled = isHorizontal;
|
||||
};
|
||||
this.eventBus._on("scrollmodechanged", scrollModeChanged);
|
||||
|
||||
this.eventBus._on("secondarytoolbarreset", evt => {
|
||||
if (evt.source === this) {
|
||||
scrollModeChanged({ mode: ScrollMode.VERTICAL });
|
||||
}
|
||||
});
|
||||
// Temporarily *disable* the Spread buttons when horizontal scrolling is
|
||||
// enabled, since the non-default Spread modes doesn't affect the layout.
|
||||
const isHorizontal = mode === ScrollMode.HORIZONTAL;
|
||||
spreadNoneButton.disabled = isHorizontal;
|
||||
spreadOddButton.disabled = isHorizontal;
|
||||
spreadEvenButton.disabled = isHorizontal;
|
||||
}
|
||||
|
||||
#bindSpreadModeListener({
|
||||
spreadNoneButton,
|
||||
spreadOddButton,
|
||||
spreadEvenButton,
|
||||
}) {
|
||||
function spreadModeChanged({ mode }) {
|
||||
const isNone = mode === SpreadMode.NONE,
|
||||
isOdd = mode === SpreadMode.ODD,
|
||||
isEven = mode === SpreadMode.EVEN;
|
||||
#spreadModeChanged({ mode }) {
|
||||
const { spreadNoneButton, spreadOddButton, spreadEvenButton } = this.#opts;
|
||||
|
||||
spreadNoneButton.classList.toggle("toggled", isNone);
|
||||
spreadOddButton.classList.toggle("toggled", isOdd);
|
||||
spreadEvenButton.classList.toggle("toggled", isEven);
|
||||
|
||||
spreadNoneButton.setAttribute("aria-checked", isNone);
|
||||
spreadOddButton.setAttribute("aria-checked", isOdd);
|
||||
spreadEvenButton.setAttribute("aria-checked", isEven);
|
||||
}
|
||||
this.eventBus._on("spreadmodechanged", spreadModeChanged);
|
||||
|
||||
this.eventBus._on("secondarytoolbarreset", evt => {
|
||||
if (evt.source === this) {
|
||||
spreadModeChanged({ mode: SpreadMode.NONE });
|
||||
}
|
||||
});
|
||||
toggleCheckedBtn(spreadNoneButton, mode === SpreadMode.NONE);
|
||||
toggleCheckedBtn(spreadOddButton, mode === SpreadMode.ODD);
|
||||
toggleCheckedBtn(spreadEvenButton, mode === SpreadMode.EVEN);
|
||||
}
|
||||
|
||||
open() {
|
||||
|
@ -311,10 +296,9 @@ class SecondaryToolbar {
|
|||
return;
|
||||
}
|
||||
this.opened = true;
|
||||
this.toggleButton.classList.add("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "true");
|
||||
// NOTE
|
||||
this.toolbar.classList.remove("fn__hidden");
|
||||
|
||||
const { toggleButton, toolbar } = this.#opts;
|
||||
toggleExpandedBtn(toggleButton, true, toolbar);
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -322,10 +306,9 @@ class SecondaryToolbar {
|
|||
return;
|
||||
}
|
||||
this.opened = false;
|
||||
// NOTE
|
||||
this.toolbar.classList.add("fn__hidden");
|
||||
this.toggleButton.classList.remove("toggled");
|
||||
this.toggleButton.setAttribute("aria-expanded", "false");
|
||||
|
||||
const { toggleButton, toolbar } = this.#opts;
|
||||
toggleExpandedBtn(toggleButton, false, toolbar);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { removeNullCharacters } from "./ui_utils.js";
|
||||
|
||||
const PDF_ROLE_TO_HTML_ROLE = {
|
||||
// Document level structure types
|
||||
Document: null, // There's a "document" role, but it doesn't make sense here.
|
||||
|
@ -72,19 +74,46 @@ const PDF_ROLE_TO_HTML_ROLE = {
|
|||
const HEADING_PATTERN = /^H(\d+)$/;
|
||||
|
||||
class StructTreeLayerBuilder {
|
||||
#treeDom = undefined;
|
||||
#promise;
|
||||
|
||||
get renderingDone() {
|
||||
return this.#treeDom !== undefined;
|
||||
#treeDom = null;
|
||||
|
||||
#treePromise;
|
||||
|
||||
#elementAttributes = new Map();
|
||||
|
||||
#rawDims;
|
||||
|
||||
#elementsToAddToTextLayer = null;
|
||||
|
||||
constructor(pdfPage, rawDims) {
|
||||
this.#promise = pdfPage.getStructTree();
|
||||
this.#rawDims = rawDims;
|
||||
}
|
||||
|
||||
render(structTree) {
|
||||
if (this.#treeDom !== undefined) {
|
||||
return this.#treeDom;
|
||||
async render() {
|
||||
if (this.#treePromise) {
|
||||
return this.#treePromise;
|
||||
}
|
||||
const treeDom = this.#walk(structTree);
|
||||
treeDom?.classList.add("structTree");
|
||||
return (this.#treeDom = treeDom);
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
this.#treePromise = promise;
|
||||
|
||||
try {
|
||||
this.#treeDom = this.#walk(await this.#promise);
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
this.#promise = null;
|
||||
|
||||
this.#treeDom?.classList.add("structTree");
|
||||
resolve(this.#treeDom);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async getAriaAttributes(annotationId) {
|
||||
await this.render();
|
||||
return this.#elementAttributes.get(annotationId);
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
@ -100,17 +129,82 @@ class StructTreeLayerBuilder {
|
|||
}
|
||||
|
||||
#setAttributes(structElement, htmlElement) {
|
||||
if (structElement.alt !== undefined) {
|
||||
htmlElement.setAttribute("aria-label", structElement.alt);
|
||||
const { alt, id, lang } = structElement;
|
||||
if (alt !== undefined) {
|
||||
// Don't add the label in the struct tree layer but on the annotation
|
||||
// in the annotation layer.
|
||||
let added = false;
|
||||
const label = removeNullCharacters(alt);
|
||||
for (const child of structElement.children) {
|
||||
if (child.type === "annotation") {
|
||||
let attrs = this.#elementAttributes.get(child.id);
|
||||
if (!attrs) {
|
||||
attrs = new Map();
|
||||
this.#elementAttributes.set(child.id, attrs);
|
||||
}
|
||||
attrs.set("aria-label", label);
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
htmlElement.setAttribute("aria-label", label);
|
||||
}
|
||||
}
|
||||
if (structElement.id !== undefined) {
|
||||
htmlElement.setAttribute("aria-owns", structElement.id);
|
||||
if (id !== undefined) {
|
||||
htmlElement.setAttribute("aria-owns", id);
|
||||
}
|
||||
if (structElement.lang !== undefined) {
|
||||
htmlElement.setAttribute("lang", structElement.lang);
|
||||
if (lang !== undefined) {
|
||||
htmlElement.setAttribute(
|
||||
"lang",
|
||||
removeNullCharacters(lang, /* replaceInvisible = */ true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#addImageInTextLayer(node, element) {
|
||||
const { alt, bbox, children } = node;
|
||||
const child = children?.[0];
|
||||
if (!this.#rawDims || !alt || !bbox || child?.type !== "content") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { id } = child;
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We cannot add the created element to the text layer immediately, as the
|
||||
// text layer might not be ready yet. Instead, we store the element and add
|
||||
// it later in `addElementsToTextLayer`.
|
||||
|
||||
element.setAttribute("aria-owns", id);
|
||||
const img = document.createElement("span");
|
||||
(this.#elementsToAddToTextLayer ||= new Map()).set(id, img);
|
||||
img.setAttribute("role", "img");
|
||||
img.setAttribute("aria-label", removeNullCharacters(alt));
|
||||
|
||||
const { pageHeight, pageX, pageY } = this.#rawDims;
|
||||
const calc = "calc(var(--scale-factor)*";
|
||||
const { style } = img;
|
||||
style.width = `${calc}${bbox[2] - bbox[0]}px)`;
|
||||
style.height = `${calc}${bbox[3] - bbox[1]}px)`;
|
||||
style.left = `${calc}${bbox[0] - pageX}px)`;
|
||||
style.top = `${calc}${pageHeight - bbox[3] + pageY}px)`;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addElementsToTextLayer() {
|
||||
if (!this.#elementsToAddToTextLayer) {
|
||||
return;
|
||||
}
|
||||
for (const [id, img] of this.#elementsToAddToTextLayer) {
|
||||
document.getElementById(id)?.append(img);
|
||||
}
|
||||
this.#elementsToAddToTextLayer.clear();
|
||||
this.#elementsToAddToTextLayer = null;
|
||||
}
|
||||
|
||||
#walk(node) {
|
||||
if (!node) {
|
||||
return null;
|
||||
|
@ -126,6 +220,9 @@ class StructTreeLayerBuilder {
|
|||
} else if (PDF_ROLE_TO_HTML_ROLE[role]) {
|
||||
element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]);
|
||||
}
|
||||
if (role === "Figure" && this.#addImageInTextLayer(node, element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
this.#setAttributes(node, element);
|
||||
|
|
|
@ -179,17 +179,18 @@ class TextAccessibilityManager {
|
|||
* in order to correctly position this editor in the text flow.
|
||||
* @param {HTMLElement} element
|
||||
* @param {boolean} isRemovable
|
||||
* @returns {string|null} The id in the struct tree if any.
|
||||
*/
|
||||
addPointerInTextLayer(element, isRemovable) {
|
||||
const { id } = element;
|
||||
if (!id) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.#enabled) {
|
||||
// The text layer needs to be there, so we postpone the association.
|
||||
this.#waitingElements.set(element, isRemovable);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isRemovable) {
|
||||
|
@ -198,7 +199,7 @@ class TextAccessibilityManager {
|
|||
|
||||
const children = this.#textChildren;
|
||||
if (!children || children.length === 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = binarySearchFirstItem(
|
||||
|
@ -208,20 +209,25 @@ class TextAccessibilityManager {
|
|||
);
|
||||
|
||||
const nodeIndex = Math.max(0, index - 1);
|
||||
this.#addIdToAriaOwns(id, children[nodeIndex]);
|
||||
const child = children[nodeIndex];
|
||||
this.#addIdToAriaOwns(id, child);
|
||||
this.#textNodes.set(id, nodeIndex);
|
||||
|
||||
const parent = child.parentNode;
|
||||
return parent?.classList.contains("markedContent") ? parent.id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a div in the DOM in order to respect the visual order.
|
||||
* @param {HTMLDivElement} element
|
||||
* @returns {string|null} The id in the struct tree if any.
|
||||
*/
|
||||
moveElementInDOM(container, element, contentElement, isRemovable) {
|
||||
this.addPointerInTextLayer(contentElement, isRemovable);
|
||||
const id = this.addPointerInTextLayer(contentElement, isRemovable);
|
||||
|
||||
if (!container.hasChildNodes()) {
|
||||
container.append(element);
|
||||
return;
|
||||
return id;
|
||||
}
|
||||
|
||||
const children = Array.from(container.childNodes).filter(
|
||||
|
@ -229,7 +235,7 @@ class TextAccessibilityManager {
|
|||
);
|
||||
|
||||
if (children.length === 0) {
|
||||
return;
|
||||
return id;
|
||||
}
|
||||
|
||||
const elementToCompare = contentElement || element;
|
||||
|
@ -247,6 +253,8 @@ class TextAccessibilityManager {
|
|||
} else {
|
||||
children[index - 1].after(element);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
* either the text layer or XFA layer depending on the type of document.
|
||||
*/
|
||||
class TextHighlighter {
|
||||
#eventAbortController = null;
|
||||
|
||||
/**
|
||||
* @param {TextHighlighterOptions} options
|
||||
*/
|
||||
|
@ -37,7 +39,6 @@ class TextHighlighter {
|
|||
this.matches = [];
|
||||
this.eventBus = eventBus;
|
||||
this.pageIdx = pageIndex;
|
||||
this._onUpdateTextLayerMatches = null;
|
||||
this.textDivs = null;
|
||||
this.textContentItemsStr = null;
|
||||
this.enabled = false;
|
||||
|
@ -69,15 +70,18 @@ class TextHighlighter {
|
|||
throw new Error("TextHighlighter is already enabled.");
|
||||
}
|
||||
this.enabled = true;
|
||||
if (!this._onUpdateTextLayerMatches) {
|
||||
this._onUpdateTextLayerMatches = evt => {
|
||||
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
|
||||
this._updateMatches();
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.#eventAbortController) {
|
||||
this.#eventAbortController = new AbortController();
|
||||
|
||||
this.eventBus._on(
|
||||
"updatetextlayermatches",
|
||||
this._onUpdateTextLayerMatches
|
||||
evt => {
|
||||
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
|
||||
this._updateMatches();
|
||||
}
|
||||
},
|
||||
{ signal: this.#eventAbortController.signal }
|
||||
);
|
||||
}
|
||||
this._updateMatches();
|
||||
|
@ -88,13 +92,10 @@ class TextHighlighter {
|
|||
return;
|
||||
}
|
||||
this.enabled = false;
|
||||
if (this._onUpdateTextLayerMatches) {
|
||||
this.eventBus._off(
|
||||
"updatetextlayermatches",
|
||||
this._onUpdateTextLayerMatches
|
||||
);
|
||||
this._onUpdateTextLayerMatches = null;
|
||||
}
|
||||
|
||||
this.#eventAbortController?.abort();
|
||||
this.#eventAbortController = null;
|
||||
|
||||
this._updateMatches(/* reset = */ true);
|
||||
}
|
||||
|
||||
|
@ -208,9 +209,20 @@ class TextHighlighter {
|
|||
return;
|
||||
}
|
||||
|
||||
let lastDivIdx = -1;
|
||||
let lastOffset = -1;
|
||||
for (let i = i0; i < i1; i++) {
|
||||
const match = matches[i];
|
||||
const begin = match.begin;
|
||||
if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) {
|
||||
// It's possible to be in this situation if we searched for a 'f' and we
|
||||
// have a ligature 'ff' in the text. The 'ff' has to be highlighted two
|
||||
// times.
|
||||
continue;
|
||||
}
|
||||
lastDivIdx = begin.divIdx;
|
||||
lastOffset = begin.offset;
|
||||
|
||||
const end = match.end;
|
||||
const isSelected = isSelectedPage && i === selectedMatchIdx;
|
||||
const highlightSuffix = isSelected ? " selected" : "";
|
||||
|
|
|
@ -13,23 +13,24 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
/** @typedef {import("../src/display/api").TextContent} TextContent */
|
||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
|
||||
import { renderTextLayer, updateTextLayer } from "./pdfjs";
|
||||
import { normalizeUnicode, TextLayer } from "./pdfjs";
|
||||
import { removeNullCharacters } from "./ui_utils.js";
|
||||
import {getHighlight} from "../anno";
|
||||
|
||||
/**
|
||||
* @typedef {Object} TextLayerBuilderOptions
|
||||
* @property {TextHighlighter} highlighter - Optional object that will handle
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {TextHighlighter} [highlighter] - Optional object that will handle
|
||||
* highlighting text from the find controller.
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
||||
* OffscreenCanvas if needed.
|
||||
* @property {function} [onAppend]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -38,109 +39,89 @@ import {getHighlight} from "../anno";
|
|||
* contain text that matches the PDF text they are overlaying.
|
||||
*/
|
||||
class TextLayerBuilder {
|
||||
#rotation = 0;
|
||||
#enablePermissions = false;
|
||||
|
||||
#scale = 0;
|
||||
#onAppend = null;
|
||||
|
||||
#textContentSource = null;
|
||||
#renderingDone = false;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
static #textLayers = new Map();
|
||||
|
||||
static #selectionChangeAbortController = null;
|
||||
|
||||
constructor({
|
||||
pdfPage,
|
||||
highlighter = null,
|
||||
accessibilityManager = null,
|
||||
isOffscreenCanvasSupported = true,
|
||||
enablePermissions = false,
|
||||
onAppend = null,
|
||||
}) {
|
||||
this.textContentItemsStr = [];
|
||||
this.renderingDone = false;
|
||||
this.textDivs = [];
|
||||
this.textDivProperties = new WeakMap();
|
||||
this.textLayerRenderTask = null;
|
||||
this.pdfPage = pdfPage;
|
||||
this.highlighter = highlighter;
|
||||
this.accessibilityManager = accessibilityManager;
|
||||
this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
|
||||
this.#enablePermissions = enablePermissions === true;
|
||||
this.#onAppend = onAppend;
|
||||
|
||||
this.div = document.createElement("div");
|
||||
this.div.tabIndex = 0;
|
||||
this.div.className = "textLayer";
|
||||
this.hide();
|
||||
}
|
||||
|
||||
#finishRendering() {
|
||||
this.renderingDone = true;
|
||||
|
||||
const endOfContent = document.createElement("div");
|
||||
endOfContent.className = "endOfContent";
|
||||
this.div.append(endOfContent);
|
||||
|
||||
this.#bindMouse();
|
||||
}
|
||||
|
||||
get numTextDivs() {
|
||||
return this.textDivs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the text layer.
|
||||
* @param {PageViewport} viewport
|
||||
* @param {Object} [textContentParams]
|
||||
*/
|
||||
async render(viewport) {
|
||||
if (!this.#textContentSource) {
|
||||
throw new Error('No "textContentSource" parameter specified.');
|
||||
}
|
||||
|
||||
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
|
||||
const { rotation } = viewport;
|
||||
if (this.renderingDone) {
|
||||
const mustRotate = rotation !== this.#rotation;
|
||||
const mustRescale = scale !== this.#scale;
|
||||
if (mustRotate || mustRescale) {
|
||||
this.hide();
|
||||
updateTextLayer({
|
||||
container: this.div,
|
||||
viewport,
|
||||
textDivs: this.textDivs,
|
||||
textDivProperties: this.textDivProperties,
|
||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
||||
mustRescale,
|
||||
mustRotate,
|
||||
});
|
||||
this.#scale = scale;
|
||||
this.#rotation = rotation;
|
||||
}
|
||||
async render(viewport, textContentParams = null) {
|
||||
if (this.#renderingDone && this.#textLayer) {
|
||||
this.#textLayer.update({
|
||||
viewport,
|
||||
onBefore: this.hide.bind(this),
|
||||
});
|
||||
this.show();
|
||||
// NOTE
|
||||
if (this.div.lastElementChild.previousElementSibling &&
|
||||
this.div.lastElementChild.previousElementSibling.classList.contains("endOfContent")) {
|
||||
this.div.lastElementChild.innerHTML = ""
|
||||
}
|
||||
this.div.querySelector(".pdf__rects")?.remove()
|
||||
getHighlight(this.div)
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancel();
|
||||
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
|
||||
this.accessibilityManager?.setTextMapping(this.textDivs);
|
||||
|
||||
this.textLayerRenderTask = renderTextLayer({
|
||||
textContentSource: this.#textContentSource,
|
||||
this.#textLayer = new TextLayer({
|
||||
textContentSource: this.pdfPage.streamTextContent(
|
||||
textContentParams || {
|
||||
includeMarkedContent: true,
|
||||
disableNormalization: true,
|
||||
}
|
||||
),
|
||||
container: this.div,
|
||||
viewport,
|
||||
textDivs: this.textDivs,
|
||||
textDivProperties: this.textDivProperties,
|
||||
textContentItemsStr: this.textContentItemsStr,
|
||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
||||
});
|
||||
|
||||
await this.textLayerRenderTask.promise;
|
||||
this.#finishRendering();
|
||||
this.#scale = scale;
|
||||
this.#rotation = rotation;
|
||||
this.show();
|
||||
const { textDivs, textContentItemsStr } = this.#textLayer;
|
||||
this.highlighter?.setTextMapping(textDivs, textContentItemsStr);
|
||||
this.accessibilityManager?.setTextMapping(textDivs);
|
||||
|
||||
await this.#textLayer.render();
|
||||
this.#renderingDone = true;
|
||||
|
||||
const endOfContent = document.createElement("div");
|
||||
endOfContent.className = "endOfContent";
|
||||
this.div.append(endOfContent);
|
||||
|
||||
this.#bindMouse(endOfContent);
|
||||
// Ensure that the textLayer is appended to the DOM *before* handling
|
||||
// e.g. a pending search operation.
|
||||
this.#onAppend?.(this.div);
|
||||
this.highlighter?.enable();
|
||||
// NOTE
|
||||
getHighlight(this.div)
|
||||
this.accessibilityManager?.enable();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.div.hidden) {
|
||||
if (!this.div.hidden && this.#renderingDone) {
|
||||
// We turn off the highlighter in order to avoid to scroll into view an
|
||||
// element of the text layer which could be hidden.
|
||||
this.highlighter?.disable();
|
||||
|
@ -149,7 +130,7 @@ class TextLayerBuilder {
|
|||
}
|
||||
|
||||
show() {
|
||||
if (this.div.hidden && this.renderingDone) {
|
||||
if (this.div.hidden && this.#renderingDone) {
|
||||
this.div.hidden = false;
|
||||
this.highlighter?.enable();
|
||||
}
|
||||
|
@ -159,23 +140,12 @@ class TextLayerBuilder {
|
|||
* Cancel rendering of the text layer.
|
||||
*/
|
||||
cancel() {
|
||||
if (this.textLayerRenderTask) {
|
||||
this.textLayerRenderTask.cancel();
|
||||
this.textLayerRenderTask = null;
|
||||
}
|
||||
this.#textLayer?.cancel();
|
||||
this.#textLayer = null;
|
||||
|
||||
this.highlighter?.disable();
|
||||
this.accessibilityManager?.disable();
|
||||
this.textContentItemsStr.length = 0;
|
||||
this.textDivs.length = 0;
|
||||
this.textDivProperties = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReadableStream | TextContent} source
|
||||
*/
|
||||
setTextContentSource(source) {
|
||||
this.cancel();
|
||||
this.#textContentSource = source;
|
||||
TextLayerBuilder.#removeGlobalSelectionListener(this.div);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,44 +153,173 @@ class TextLayerBuilder {
|
|||
* clicked. This reduces flickering of the content if the mouse is slowly
|
||||
* dragged up or down.
|
||||
*/
|
||||
#bindMouse() {
|
||||
#bindMouse(end) {
|
||||
const { div } = this;
|
||||
|
||||
div.addEventListener("mousedown", evt => {
|
||||
const end = div.querySelector(".endOfContent");
|
||||
if (!end) {
|
||||
return;
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
// On non-Firefox browsers, the selection will feel better if the height
|
||||
// of the `endOfContent` div is adjusted to start at mouse click
|
||||
// location. This avoids flickering when the selection moves up.
|
||||
// However it does not work when selection is started on empty space.
|
||||
let adjustTop = evt.target !== div;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
adjustTop &&=
|
||||
getComputedStyle(end).getPropertyValue("-moz-user-select") !==
|
||||
"none";
|
||||
}
|
||||
if (adjustTop) {
|
||||
const divBounds = div.getBoundingClientRect();
|
||||
const r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
|
||||
end.style.top = (r * 100).toFixed(2) + "%";
|
||||
}
|
||||
}
|
||||
end.classList.add("active");
|
||||
div.addEventListener("mousedown", () => {
|
||||
div.classList.add("selecting");
|
||||
});
|
||||
|
||||
div.addEventListener("mouseup", () => {
|
||||
const end = div.querySelector(".endOfContent");
|
||||
if (!end) {
|
||||
return;
|
||||
div.addEventListener("copy", event => {
|
||||
if (!this.#enablePermissions) {
|
||||
const selection = document.getSelection();
|
||||
event.clipboardData.setData(
|
||||
"text/plain",
|
||||
removeNullCharacters(normalizeUnicode(selection.toString()))
|
||||
);
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
end.style.top = "";
|
||||
}
|
||||
end.classList.remove("active");
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
TextLayerBuilder.#textLayers.set(div, end);
|
||||
TextLayerBuilder.#enableGlobalSelectionListener();
|
||||
}
|
||||
|
||||
static #removeGlobalSelectionListener(textLayerDiv) {
|
||||
this.#textLayers.delete(textLayerDiv);
|
||||
|
||||
if (this.#textLayers.size === 0) {
|
||||
this.#selectionChangeAbortController?.abort();
|
||||
this.#selectionChangeAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
static #enableGlobalSelectionListener() {
|
||||
if (this.#selectionChangeAbortController) {
|
||||
// document-level event listeners already installed
|
||||
return;
|
||||
}
|
||||
this.#selectionChangeAbortController = new AbortController();
|
||||
const { signal } = this.#selectionChangeAbortController;
|
||||
|
||||
const reset = (end, textLayer) => {
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
textLayer.append(end);
|
||||
end.style.width = "";
|
||||
end.style.height = "";
|
||||
}
|
||||
textLayer.classList.remove("selecting");
|
||||
};
|
||||
|
||||
let isPointerDown = false;
|
||||
document.addEventListener(
|
||||
"pointerdown",
|
||||
() => {
|
||||
isPointerDown = true;
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
"pointerup",
|
||||
() => {
|
||||
isPointerDown = false;
|
||||
this.#textLayers.forEach(reset);
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
window.addEventListener(
|
||||
"blur",
|
||||
() => {
|
||||
isPointerDown = false;
|
||||
this.#textLayers.forEach(reset);
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
"keyup",
|
||||
() => {
|
||||
if (!isPointerDown) {
|
||||
this.#textLayers.forEach(reset);
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
|
||||
// eslint-disable-next-line no-var
|
||||
var isFirefox, prevRange;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"selectionchange",
|
||||
() => {
|
||||
const selection = document.getSelection();
|
||||
if (selection.rangeCount === 0) {
|
||||
this.#textLayers.forEach(reset);
|
||||
return;
|
||||
}
|
||||
|
||||
// Even though the spec says that .rangeCount should be 0 or 1, Firefox
|
||||
// creates multiple ranges when selecting across multiple pages.
|
||||
// Make sure to collect all the .textLayer elements where the selection
|
||||
// is happening.
|
||||
const activeTextLayers = new Set();
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
const range = selection.getRangeAt(i);
|
||||
for (const textLayerDiv of this.#textLayers.keys()) {
|
||||
if (
|
||||
!activeTextLayers.has(textLayerDiv) &&
|
||||
range.intersectsNode(textLayerDiv)
|
||||
) {
|
||||
activeTextLayers.add(textLayerDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [textLayerDiv, endDiv] of this.#textLayers) {
|
||||
if (activeTextLayers.has(textLayerDiv)) {
|
||||
textLayerDiv.classList.add("selecting");
|
||||
} else {
|
||||
reset(endDiv, textLayerDiv);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
|
||||
return;
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
|
||||
isFirefox ??=
|
||||
getComputedStyle(
|
||||
this.#textLayers.values().next().value
|
||||
).getPropertyValue("-moz-user-select") === "none";
|
||||
|
||||
if (isFirefox) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// In non-Firefox browsers, when hovering over an empty space (thus,
|
||||
// on .endOfContent), the selection will expand to cover all the
|
||||
// text between the current selection and .endOfContent. By moving
|
||||
// .endOfContent to right after (or before, depending on which side
|
||||
// of the selection the user is moving), we limit the selection jump
|
||||
// to at most cover the enteirety of the <span> where the selection
|
||||
// is being modified.
|
||||
const range = selection.getRangeAt(0);
|
||||
const modifyStart =
|
||||
prevRange &&
|
||||
(range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 ||
|
||||
range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0);
|
||||
let anchor = modifyStart ? range.startContainer : range.endContainer;
|
||||
if (anchor.nodeType === Node.TEXT_NODE) {
|
||||
anchor = anchor.parentNode;
|
||||
}
|
||||
|
||||
const parentTextLayer = anchor.parentElement.closest(".textLayer");
|
||||
const endDiv = this.#textLayers.get(parentTextLayer);
|
||||
if (endDiv) {
|
||||
endDiv.style.width = parentTextLayer.style.width;
|
||||
endDiv.style.height = parentTextLayer.style.height;
|
||||
anchor.parentElement.insertBefore(
|
||||
endDiv,
|
||||
modifyStart ? anchor : anchor.nextSibling
|
||||
);
|
||||
}
|
||||
|
||||
prevRange = range.cloneRange();
|
||||
},
|
||||
{ signal }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,17 +13,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
|
||||
import { AnnotationEditorType, ColorPicker, noContextMenu } from "./pdfjs";
|
||||
import {
|
||||
animationStarted,
|
||||
DEFAULT_SCALE,
|
||||
DEFAULT_SCALE_VALUE,
|
||||
MAX_SCALE,
|
||||
MIN_SCALE,
|
||||
noContextMenuHandler,
|
||||
toggleExpandedBtn,
|
||||
} from "./ui_utils.js";
|
||||
import { AnnotationEditorType } from "./pdfjs";
|
||||
|
||||
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
|
||||
|
||||
/**
|
||||
* @typedef {Object} ToolbarOptions
|
||||
|
@ -39,26 +38,27 @@ const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
|
|||
* @property {HTMLButtonElement} next - Button to go to the next page.
|
||||
* @property {HTMLButtonElement} zoomIn - Button to zoom in the pages.
|
||||
* @property {HTMLButtonElement} zoomOut - Button to zoom out the pages.
|
||||
* @property {HTMLButtonElement} viewFind - Button to open find bar.
|
||||
* @property {HTMLButtonElement} openFile - Button to open a new document.
|
||||
* @property {HTMLButtonElement} editorFreeTextButton - Button to switch to
|
||||
* FreeText editing.
|
||||
* @property {HTMLButtonElement} download - Button to download the document.
|
||||
*/
|
||||
|
||||
class Toolbar {
|
||||
#wasLocalized = false;
|
||||
#opts;
|
||||
|
||||
/**
|
||||
* @param {ToolbarOptions} options
|
||||
* @param {EventBus} eventBus
|
||||
* @param {IL10n} l10n - Localization service.
|
||||
* @param {number} toolbarDensity - The toolbar density value.
|
||||
* The possible values are:
|
||||
* - 0 (default) - The regular toolbar size.
|
||||
* - 1 (compact) - The small toolbar size.
|
||||
* - 2 (touch) - The large toolbar size.
|
||||
*/
|
||||
constructor(options, eventBus, l10n) {
|
||||
this.toolbar = options.container;
|
||||
constructor(options, eventBus, toolbarDensity = 0) {
|
||||
this.#opts = options;
|
||||
this.eventBus = eventBus;
|
||||
this.l10n = l10n;
|
||||
this.buttons = [
|
||||
const buttons = [
|
||||
{ element: options.previous, eventName: "previouspage" },
|
||||
{ element: options.next, eventName: "nextpage" },
|
||||
{ element: options.zoomIn, eventName: "zoomin" },
|
||||
|
@ -77,6 +77,18 @@ class Toolbar {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorHighlightButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: {
|
||||
get mode() {
|
||||
const { classList } = options.editorHighlightButton;
|
||||
return classList.contains("toggled")
|
||||
? AnnotationEditorType.NONE
|
||||
: AnnotationEditorType.HIGHLIGHT;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorInkButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
|
@ -89,27 +101,50 @@ class Toolbar {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorStampButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: {
|
||||
get mode() {
|
||||
const { classList } = options.editorStampButton;
|
||||
return classList.contains("toggled")
|
||||
? AnnotationEditorType.NONE
|
||||
: AnnotationEditorType.STAMP;
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
type: "editing",
|
||||
data: { action: "pdfjs.image.icon_click" },
|
||||
},
|
||||
},
|
||||
];
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.buttons.push({ element: options.openFile, eventName: "openfile" });
|
||||
}
|
||||
this.items = {
|
||||
numPages: options.numPages,
|
||||
pageNumber: options.pageNumber,
|
||||
scaleSelect: options.scaleSelect,
|
||||
customScaleOption: options.customScaleOption,
|
||||
previous: options.previous,
|
||||
next: options.next,
|
||||
zoomIn: options.zoomIn,
|
||||
zoomOut: options.zoomOut,
|
||||
};
|
||||
|
||||
// Bind the event listeners for click and various other actions.
|
||||
this.#bindListeners(options);
|
||||
this.#bindListeners(buttons);
|
||||
|
||||
this.#updateToolbarDensity({ value: toolbarDensity });
|
||||
this.reset();
|
||||
}
|
||||
|
||||
#updateToolbarDensity({ value }) {
|
||||
let name = "normal";
|
||||
switch (value) {
|
||||
case 1:
|
||||
name = "compact";
|
||||
break;
|
||||
case 2:
|
||||
name = "touch";
|
||||
break;
|
||||
}
|
||||
document.documentElement.setAttribute("data-toolbar-density", name);
|
||||
}
|
||||
|
||||
#setAnnotationEditorUIManager(uiManager, parentContainer) {
|
||||
const colorPicker = new ColorPicker({ uiManager });
|
||||
uiManager.setMainHighlightColorPicker(colorPicker);
|
||||
parentContainer.append(colorPicker.renderMainDropdown());
|
||||
}
|
||||
|
||||
setPageNumber(pageNumber, pageLabel) {
|
||||
this.pageNumber = pageNumber;
|
||||
this.pageLabel = pageLabel;
|
||||
|
@ -139,18 +174,35 @@ class Toolbar {
|
|||
this.updateLoadingIndicatorState();
|
||||
|
||||
// Reset the Editor buttons too, since they're document specific.
|
||||
this.eventBus.dispatch("toolbarreset", { source: this });
|
||||
this.#editorModeChanged({ mode: AnnotationEditorType.DISABLE });
|
||||
}
|
||||
|
||||
#bindListeners(options) {
|
||||
const { pageNumber, scaleSelect } = this.items;
|
||||
#bindListeners(buttons) {
|
||||
const { eventBus } = this;
|
||||
const {
|
||||
editorHighlightColorPicker,
|
||||
editorHighlightButton,
|
||||
pageNumber,
|
||||
scaleSelect,
|
||||
} = this.#opts;
|
||||
const self = this;
|
||||
|
||||
// The buttons within the toolbar.
|
||||
for (const { element, eventName, eventDetails } of this.buttons) {
|
||||
for (const { element, eventName, eventDetails, telemetry } of buttons) {
|
||||
element.addEventListener("click", evt => {
|
||||
if (eventName !== null) {
|
||||
this.eventBus.dispatch(eventName, { source: this, ...eventDetails });
|
||||
eventBus.dispatch(eventName, {
|
||||
source: this,
|
||||
...eventDetails,
|
||||
// evt.detail is the number of clicks.
|
||||
isFromKeyboard: evt.detail === 0,
|
||||
});
|
||||
}
|
||||
if (telemetry) {
|
||||
eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: telemetry,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -159,7 +211,7 @@ class Toolbar {
|
|||
this.select();
|
||||
});
|
||||
pageNumber.addEventListener("change", function () {
|
||||
self.eventBus.dispatch("pagenumberchanged", {
|
||||
eventBus.dispatch("pagenumberchanged", {
|
||||
source: self,
|
||||
value: this.value,
|
||||
});
|
||||
|
@ -169,15 +221,14 @@ class Toolbar {
|
|||
if (this.value === "custom") {
|
||||
return;
|
||||
}
|
||||
self.eventBus.dispatch("scalechanged", {
|
||||
eventBus.dispatch("scalechanged", {
|
||||
source: self,
|
||||
value: this.value,
|
||||
});
|
||||
});
|
||||
// Here we depend on browsers dispatching the "click" event *after* the
|
||||
// "change" event, when the <select>-element changes.
|
||||
scaleSelect.addEventListener("click", function (evt) {
|
||||
const target = evt.target;
|
||||
scaleSelect.addEventListener("click", function ({ target }) {
|
||||
// Remove focus when an <option>-element was *clicked*, to improve the UX
|
||||
// for mouse users (fixes bug 1300525 and issue 4923).
|
||||
if (
|
||||
|
@ -188,94 +239,118 @@ class Toolbar {
|
|||
}
|
||||
});
|
||||
// Suppress context menus for some controls.
|
||||
scaleSelect.oncontextmenu = noContextMenuHandler;
|
||||
scaleSelect.oncontextmenu = noContextMenu;
|
||||
|
||||
this.eventBus._on("localized", () => {
|
||||
this.#wasLocalized = true;
|
||||
this.#adjustScaleWidth();
|
||||
this.#updateUIState(true);
|
||||
eventBus._on(
|
||||
"annotationeditormodechanged",
|
||||
this.#editorModeChanged.bind(this)
|
||||
);
|
||||
eventBus._on("showannotationeditorui", ({ mode }) => {
|
||||
switch (mode) {
|
||||
case AnnotationEditorType.HIGHLIGHT:
|
||||
editorHighlightButton.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this));
|
||||
|
||||
// NOTE
|
||||
// this.#bindEditorToolsListener(options);
|
||||
if (editorHighlightColorPicker) {
|
||||
eventBus._on(
|
||||
"annotationeditoruimanager",
|
||||
({ uiManager }) => {
|
||||
this.#setAnnotationEditorUIManager(
|
||||
uiManager,
|
||||
editorHighlightColorPicker
|
||||
);
|
||||
},
|
||||
// Once the color picker has been added, we don't want to add it again.
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#bindEditorToolsListener({
|
||||
editorFreeTextButton,
|
||||
editorFreeTextParamsToolbar,
|
||||
editorInkButton,
|
||||
editorInkParamsToolbar,
|
||||
}) {
|
||||
const editorModeChanged = (evt, disableButtons = false) => {
|
||||
const editorButtons = [
|
||||
{
|
||||
mode: AnnotationEditorType.FREETEXT,
|
||||
button: editorFreeTextButton,
|
||||
toolbar: editorFreeTextParamsToolbar,
|
||||
},
|
||||
{
|
||||
mode: AnnotationEditorType.INK,
|
||||
button: editorInkButton,
|
||||
toolbar: editorInkParamsToolbar,
|
||||
},
|
||||
];
|
||||
#editorModeChanged({ mode }) {
|
||||
const {
|
||||
editorFreeTextButton,
|
||||
editorFreeTextParamsToolbar,
|
||||
editorHighlightButton,
|
||||
editorHighlightParamsToolbar,
|
||||
editorInkButton,
|
||||
editorInkParamsToolbar,
|
||||
editorStampButton,
|
||||
editorStampParamsToolbar,
|
||||
} = this.#opts;
|
||||
|
||||
for (const { mode, button, toolbar } of editorButtons) {
|
||||
const checked = mode === evt.mode;
|
||||
button.classList.toggle("toggled", checked);
|
||||
button.setAttribute("aria-checked", checked);
|
||||
button.disabled = disableButtons;
|
||||
// NOTE
|
||||
toolbar?.classList.toggle("fn__hidden", !checked);
|
||||
}
|
||||
};
|
||||
this.eventBus._on("annotationeditormodechanged", editorModeChanged);
|
||||
toggleExpandedBtn(
|
||||
editorFreeTextButton,
|
||||
mode === AnnotationEditorType.FREETEXT,
|
||||
editorFreeTextParamsToolbar
|
||||
);
|
||||
toggleExpandedBtn(
|
||||
editorHighlightButton,
|
||||
mode === AnnotationEditorType.HIGHLIGHT,
|
||||
editorHighlightParamsToolbar
|
||||
);
|
||||
toggleExpandedBtn(
|
||||
editorInkButton,
|
||||
mode === AnnotationEditorType.INK,
|
||||
editorInkParamsToolbar
|
||||
);
|
||||
toggleExpandedBtn(
|
||||
editorStampButton,
|
||||
mode === AnnotationEditorType.STAMP,
|
||||
editorStampParamsToolbar
|
||||
);
|
||||
|
||||
this.eventBus._on("toolbarreset", evt => {
|
||||
if (evt.source === this) {
|
||||
editorModeChanged(
|
||||
{ mode: AnnotationEditorType.NONE },
|
||||
/* disableButtons = */ true
|
||||
);
|
||||
}
|
||||
});
|
||||
const isDisable = mode === AnnotationEditorType.DISABLE;
|
||||
editorFreeTextButton.disabled = isDisable;
|
||||
editorHighlightButton.disabled = isDisable;
|
||||
editorInkButton.disabled = isDisable;
|
||||
editorStampButton.disabled = isDisable;
|
||||
}
|
||||
|
||||
#updateUIState(resetNumPages = false) {
|
||||
if (!this.#wasLocalized) {
|
||||
// Don't update the UI state until we localize the toolbar.
|
||||
return;
|
||||
}
|
||||
const { pageNumber, pagesCount, pageScaleValue, pageScale, items } = this;
|
||||
const { pageNumber, pagesCount, pageScaleValue, pageScale } = this;
|
||||
const opts = this.#opts;
|
||||
|
||||
if (resetNumPages) {
|
||||
if (this.hasPageLabels) {
|
||||
items.pageNumber.type = "text";
|
||||
opts.pageNumber.type = "text";
|
||||
|
||||
opts.numPages.setAttribute("data-l10n-id", "pdfjs-page-of-pages");
|
||||
} else {
|
||||
items.pageNumber.type = "number";
|
||||
opts.pageNumber.type = "number";
|
||||
// NOTE
|
||||
items.numPages.textContent = "/ " + pagesCount;
|
||||
opts.numPages.textContent = "/ " + pagesCount;
|
||||
// opts.numPages.setAttribute("data-l10n-id", "pdfjs-of-pages");
|
||||
// opts.numPages.setAttribute(
|
||||
// "data-l10n-args",
|
||||
// JSON.stringify({ pagesCount })
|
||||
// );
|
||||
}
|
||||
items.pageNumber.max = pagesCount;
|
||||
opts.pageNumber.max = pagesCount;
|
||||
}
|
||||
|
||||
if (this.hasPageLabels) {
|
||||
items.pageNumber.value = this.pageLabel;
|
||||
opts.pageNumber.value = this.pageLabel;
|
||||
// NOTE
|
||||
items.numPages.textContent = `(${pageNumber} / ${pagesCount})`
|
||||
opts.numPages.textContent = `(${pageNumber} / ${pagesCount})`
|
||||
// opts.numPages.setAttribute(
|
||||
// "data-l10n-args",
|
||||
// JSON.stringify({ pageNumber, pagesCount })
|
||||
// );
|
||||
} else {
|
||||
items.pageNumber.value = pageNumber;
|
||||
opts.pageNumber.value = pageNumber;
|
||||
}
|
||||
|
||||
items.previous.disabled = pageNumber <= 1;
|
||||
items.next.disabled = pageNumber >= pagesCount;
|
||||
opts.previous.disabled = pageNumber <= 1;
|
||||
opts.next.disabled = pageNumber >= pagesCount;
|
||||
|
||||
items.zoomOut.disabled = pageScale <= MIN_SCALE;
|
||||
items.zoomIn.disabled = pageScale >= MAX_SCALE;
|
||||
opts.zoomOut.disabled = pageScale <= MIN_SCALE;
|
||||
opts.zoomIn.disabled = pageScale >= MAX_SCALE;
|
||||
|
||||
// NOTE
|
||||
let predefinedValueFound = false;
|
||||
for (const option of items.scaleSelect.options) {
|
||||
for (const option of opts.scaleSelect.options) {
|
||||
if (option.value !== pageScaleValue) {
|
||||
option.selected = false;
|
||||
continue;
|
||||
|
@ -284,62 +359,21 @@ class Toolbar {
|
|||
predefinedValueFound = true;
|
||||
}
|
||||
if (!predefinedValueFound) {
|
||||
items.customScaleOption.textContent = `${Math.round(pageScale * 10000) / 100}%`;
|
||||
items.customScaleOption.selected = true;
|
||||
opts.customScaleOption.selected = true;
|
||||
// NOTE
|
||||
opts.customScaleOption.textContent = `${Math.round(pageScale * 10000) / 100}%`;
|
||||
// opts.customScaleOption.setAttribute(
|
||||
// "data-l10n-args",
|
||||
// JSON.stringify({
|
||||
// scale: Math.round(pageScale * 10000) / 100,
|
||||
// })
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingIndicatorState(loading = false) {
|
||||
const { pageNumber } = this.items;
|
||||
|
||||
pageNumber.classList.toggle(PAGE_NUMBER_LOADING_INDICATOR, loading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the width of the zoom dropdown DOM element if, and only if, it's
|
||||
* too narrow to fit the *longest* of the localized strings.
|
||||
*/
|
||||
async #adjustScaleWidth() {
|
||||
const { items, l10n } = this;
|
||||
|
||||
// NOTE
|
||||
const predefinedValuesPromise = [
|
||||
window.siyuan.languages.pageScaleAuto,
|
||||
window.siyuan.languages.pageScaleActual,
|
||||
window.siyuan.languages.pageScaleFit,
|
||||
window.siyuan.languages.pageScaleWidth];
|
||||
|
||||
await animationStarted;
|
||||
|
||||
const style = getComputedStyle(items.scaleSelect);
|
||||
const scaleSelectWidth = parseFloat(
|
||||
style.getPropertyValue("--scale-select-width")
|
||||
);
|
||||
|
||||
// The temporary canvas is used to measure text length in the DOM.
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d", { alpha: false });
|
||||
ctx.font = `${style.fontSize} ${style.fontFamily}`;
|
||||
|
||||
let maxWidth = 0;
|
||||
for (const predefinedValue of await predefinedValuesPromise) {
|
||||
const { width } = ctx.measureText(predefinedValue);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
}
|
||||
// Account for the icon width, and ensure that there's always some spacing
|
||||
// between the text and the icon.
|
||||
maxWidth += 0.3 * scaleSelectWidth;
|
||||
|
||||
if (maxWidth > scaleSelectWidth) {
|
||||
const container = items.scaleSelect.parentNode;
|
||||
container.style.setProperty("--scale-select-width", `${maxWidth}px`);
|
||||
}
|
||||
// Zeroing the width and height cause Firefox to release graphics resources
|
||||
// immediately, which can greatly reduce memory consumption.
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
const { pageNumber } = this.#opts;
|
||||
pageNumber.classList.toggle("loading", loading);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,17 +46,10 @@ const SidebarView = {
|
|||
LAYERS: 4,
|
||||
};
|
||||
|
||||
const RendererType =
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || GENERIC")
|
||||
? {
|
||||
CANVAS: "canvas",
|
||||
SVG: "svg",
|
||||
}
|
||||
: null;
|
||||
|
||||
const TextLayerMode = {
|
||||
DISABLE: 0,
|
||||
ENABLE: 1,
|
||||
ENABLE_PERMISSIONS: 2,
|
||||
};
|
||||
|
||||
const ScrollMode = {
|
||||
|
@ -83,37 +76,13 @@ const CursorTool = {
|
|||
// Used by `PDFViewerApplication`, and by the API unit-tests.
|
||||
const AutoPrintRegExp = /\bprint\s*\(/;
|
||||
|
||||
/**
|
||||
* Scale factors for the canvas, necessary with HiDPI displays.
|
||||
*/
|
||||
class OutputScale {
|
||||
constructor() {
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
/**
|
||||
* @type {number} Horizontal scale.
|
||||
*/
|
||||
this.sx = pixelRatio;
|
||||
|
||||
/**
|
||||
* @type {number} Vertical scale.
|
||||
*/
|
||||
this.sy = pixelRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean} Returns `true` when scaling is required, `false` otherwise.
|
||||
*/
|
||||
get scaled() {
|
||||
return this.sx !== 1 || this.sy !== 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls specified element into view of its parent.
|
||||
* @param {Object} element - The element to be visible.
|
||||
* @param {Object} spot - An object with optional top and left properties,
|
||||
* @param {HTMLElement} element - The element to be visible.
|
||||
* @param {Object} [spot] - An object with optional top and left properties,
|
||||
* specifying the offset from the top left edge.
|
||||
* @param {number} [spot.left]
|
||||
* @param {number} [spot.top]
|
||||
* @param {boolean} [scrollMatches] - When scrolling search results into view,
|
||||
* ignore elements that either: Contains marked content identifiers,
|
||||
* or have the CSS-rule `overflow: hidden;` set. The default value is `false`.
|
||||
|
@ -160,7 +129,7 @@ function scrollIntoView(element, spot, scrollMatches = false) {
|
|||
* Helper function to start monitoring the scroll event and converting them into
|
||||
* PDF.js friendly one: with scroll debounce and scroll direction.
|
||||
*/
|
||||
function watchScroll(viewAreaElement, callback) {
|
||||
function watchScroll(viewAreaElement, callback, abortSignal = undefined) {
|
||||
const debounceScroll = function (evt) {
|
||||
if (rAF) {
|
||||
return;
|
||||
|
@ -194,13 +163,21 @@ function watchScroll(viewAreaElement, callback) {
|
|||
};
|
||||
|
||||
let rAF = null;
|
||||
viewAreaElement.addEventListener("scroll", debounceScroll, true);
|
||||
viewAreaElement.addEventListener("scroll", debounceScroll, {
|
||||
useCapture: true,
|
||||
signal: abortSignal,
|
||||
});
|
||||
abortSignal?.addEventListener(
|
||||
"abort",
|
||||
() => window.cancelAnimationFrame(rAF),
|
||||
{ once: true }
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to parse query string (e.g. ?param1=value¶m2=...).
|
||||
* @param {string}
|
||||
* @param {string} query
|
||||
* @returns {Map}
|
||||
*/
|
||||
function parseQueryString(query) {
|
||||
|
@ -211,19 +188,18 @@ function parseQueryString(query) {
|
|||
return params;
|
||||
}
|
||||
|
||||
const InvisibleCharactersRegExp = /[\x01-\x1F]/g;
|
||||
const InvisibleCharsRegExp = /[\x00-\x1F]/g;
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {boolean} [replaceInvisible]
|
||||
*/
|
||||
function removeNullCharacters(str, replaceInvisible = false) {
|
||||
if (typeof str !== "string") {
|
||||
console.error(`The argument must be a string.`);
|
||||
if (!InvisibleCharsRegExp.test(str)) {
|
||||
return str;
|
||||
}
|
||||
if (replaceInvisible) {
|
||||
str = str.replaceAll(InvisibleCharactersRegExp, " ");
|
||||
return str.replaceAll(InvisibleCharsRegExp, m => (m === "\x00" ? "" : " "));
|
||||
}
|
||||
return str.replaceAll("\x00", "");
|
||||
}
|
||||
|
@ -266,6 +242,7 @@ function binarySearchFirstItem(items, condition, start = 0) {
|
|||
* @param {number} x - Positive float number.
|
||||
* @returns {Array} Estimated fraction: the first array item is a numerator,
|
||||
* the second one is a denominator.
|
||||
* They are both natural numbers.
|
||||
*/
|
||||
function approximateFraction(x) {
|
||||
// Fast paths for int numbers or their inversions.
|
||||
|
@ -312,9 +289,12 @@ function approximateFraction(x) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function roundToDivide(x, div) {
|
||||
const r = x % div;
|
||||
return r === 0 ? x : Math.round(x - r + div);
|
||||
/**
|
||||
* @param {number} x - A positive number to round to a multiple of `div`.
|
||||
* @param {number} div - A natural number.
|
||||
*/
|
||||
function floorToDivide(x, div) {
|
||||
return x - (x % div);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -466,7 +446,7 @@ function backtrackBeforeAllVisibleElements(index, views, top) {
|
|||
* rendering canvas. Earlier and later refer to index in `views`, not page
|
||||
* layout.)
|
||||
*
|
||||
* @param {GetVisibleElementsParameters}
|
||||
* @param {GetVisibleElementsParameters} params
|
||||
* @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
|
||||
*/
|
||||
function getVisibleElements({
|
||||
|
@ -609,13 +589,6 @@ function getVisibleElements({
|
|||
return { first, last, views: visible, ids };
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to suppress context menu.
|
||||
*/
|
||||
function noContextMenuHandler(evt) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
function normalizeWheelEventDirection(evt) {
|
||||
let delta = Math.hypot(evt.deltaX, evt.deltaY);
|
||||
const angle = Math.atan2(evt.deltaY, evt.deltaX);
|
||||
|
@ -741,7 +714,7 @@ class ProgressBar {
|
|||
}
|
||||
|
||||
setDisableAutoFetch(delay = /* ms = */ 5000) {
|
||||
if (isNaN(this.#percent)) {
|
||||
if (this.#percent === 100 || isNaN(this.#percent)) {
|
||||
return;
|
||||
}
|
||||
if (this.#disableAutoFetchTimeout) {
|
||||
|
@ -762,7 +735,6 @@ class ProgressBar {
|
|||
this.#visible = false;
|
||||
// NOTE
|
||||
this.#classList.add("fn__hidden");
|
||||
docStyle.setProperty("--progressBar-percent", "0");
|
||||
}
|
||||
|
||||
show() {
|
||||
|
@ -799,7 +771,7 @@ function getActiveOrFocusedElement() {
|
|||
|
||||
/**
|
||||
* Converts API PageLayout values to the format used by `BaseViewer`.
|
||||
* @param {string} mode - The API PageLayout value.
|
||||
* @param {string} layout - The API PageLayout value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function apiPageLayoutToViewerModes(layout) {
|
||||
|
@ -852,6 +824,39 @@ function apiPageModeToSidebarView(mode) {
|
|||
return SidebarView.NONE; // Default value.
|
||||
}
|
||||
|
||||
function toggleCheckedBtn(button, toggle, view = null) {
|
||||
button.classList.toggle("toggled", toggle);
|
||||
button.setAttribute("aria-checked", toggle);
|
||||
|
||||
view?.classList.toggle("fn__hidden", !toggle);
|
||||
}
|
||||
|
||||
function toggleExpandedBtn(button, toggle, view = null) {
|
||||
button.classList.toggle("toggled", toggle);
|
||||
button.setAttribute("aria-expanded", toggle);
|
||||
|
||||
view?.classList.toggle("fn__hidden", !toggle);
|
||||
}
|
||||
|
||||
// In Firefox, the css calc function uses f32 precision but the Chrome or Safari
|
||||
// are using f64 one. So in order to have the same rendering in all browsers, we
|
||||
// need to use the right precision in order to have correct dimensions.
|
||||
const calcRound =
|
||||
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
|
||||
? Math.fround
|
||||
: (function () {
|
||||
if (
|
||||
typeof PDFJSDev !== "undefined" &&
|
||||
PDFJSDev.test("LIB") &&
|
||||
typeof document === "undefined"
|
||||
) {
|
||||
return x => x;
|
||||
}
|
||||
const e = document.createElement("div");
|
||||
e.style.width = "round(down, calc(1.6666666666666665 * 792px), 1px)";
|
||||
return e.style.width === "calc(1320px)" ? Math.fround : x => x;
|
||||
})();
|
||||
|
||||
export {
|
||||
animationStarted,
|
||||
apiPageLayoutToViewerModes,
|
||||
|
@ -860,11 +865,13 @@ export {
|
|||
AutoPrintRegExp,
|
||||
backtrackBeforeAllVisibleElements, // only exported for testing
|
||||
binarySearchFirstItem,
|
||||
calcRound,
|
||||
CursorTool,
|
||||
DEFAULT_SCALE,
|
||||
DEFAULT_SCALE_DELTA,
|
||||
DEFAULT_SCALE_VALUE,
|
||||
docStyle,
|
||||
floorToDivide,
|
||||
getActiveOrFocusedElement,
|
||||
getPageSizeInches,
|
||||
getVisibleElements,
|
||||
|
@ -875,23 +882,21 @@ export {
|
|||
MAX_AUTO_SCALE,
|
||||
MAX_SCALE,
|
||||
MIN_SCALE,
|
||||
noContextMenuHandler,
|
||||
normalizeWheelEventDelta,
|
||||
normalizeWheelEventDirection,
|
||||
OutputScale,
|
||||
parseQueryString,
|
||||
PresentationModeState,
|
||||
ProgressBar,
|
||||
removeNullCharacters,
|
||||
RendererType,
|
||||
RenderingStates,
|
||||
roundToDivide,
|
||||
SCROLLBAR_PADDING,
|
||||
scrollIntoView,
|
||||
ScrollMode,
|
||||
SidebarView,
|
||||
SpreadMode,
|
||||
TextLayerMode,
|
||||
toggleCheckedBtn,
|
||||
toggleExpandedBtn,
|
||||
UNKNOWN_SCALE,
|
||||
VERTICAL_PADDING,
|
||||
watchScroll,
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
*/
|
||||
|
||||
import {setStorageVal} from "../../protyle/util/compatibility";
|
||||
|
||||
const DEFAULT_VIEW_HISTORY_CACHE_SIZE = 20;
|
||||
|
||||
/**
|
||||
|
@ -66,6 +67,7 @@ class ViewHistory {
|
|||
// NOTE
|
||||
window.siyuan.storage["pdfjs.history"] = databaseStr
|
||||
setStorageVal("pdfjs.history", databaseStr)
|
||||
// localStorage.setItem("pdfjs.history", databaseStr);
|
||||
}
|
||||
|
||||
async _readFromStorage() {
|
||||
|
@ -74,6 +76,7 @@ class ViewHistory {
|
|||
}
|
||||
// NOTE
|
||||
return window.siyuan.storage["pdfjs.history"];
|
||||
// return localStorage.getItem("pdfjs.history");
|
||||
}
|
||||
|
||||
async set(name, val) {
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RenderingStates, ScrollMode, SpreadMode } from "./ui_utils.js";
|
||||
import {RenderingStates, ScrollMode, SpreadMode, TextLayerMode} from "./ui_utils.js";
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { LinkTarget } from "./pdf_link_service.js";
|
||||
import { PDFViewerApplication } from "./app.js";
|
||||
import { initAnno } from "../anno";
|
||||
import {Constants} from "../../constants";
|
||||
import {initAnno} from "../anno";
|
||||
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const pdfjsVersion =
|
||||
|
@ -36,39 +38,46 @@ const AppConstants =
|
|||
// window.PDFViewerApplicationOptions = AppOptions;
|
||||
|
||||
function getViewerConfiguration(element) {
|
||||
// NOTE
|
||||
return {
|
||||
appContainer: element,
|
||||
principalContainer: element.querySelector("#mainContainer"),
|
||||
mainContainer: element.querySelector("#viewerContainer"),
|
||||
viewerContainer: element.querySelector("#viewer"),
|
||||
toolbar: {
|
||||
// NOTE
|
||||
rectAnno: element.querySelector("#rectAnno"),
|
||||
container: element.querySelector("#toolbarViewer"),
|
||||
container: element.querySelector("#toolbarContainer"),
|
||||
numPages: element.querySelector("#numPages"),
|
||||
pageNumber: element.querySelector("#pageNumber"),
|
||||
scaleSelect: element.querySelector("#scaleSelect"),
|
||||
customScaleOption: element.querySelector("#customScaleOption"),
|
||||
previous: element.querySelector("#previous"),
|
||||
next: element.querySelector("#next"),
|
||||
zoomIn: element.querySelector("#zoomIn"),
|
||||
zoomOut: element.querySelector("#zoomOut"),
|
||||
viewFind: element.querySelector("#viewFind"),
|
||||
openFile:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
|
||||
? element.querySelector("#openFile")
|
||||
: null,
|
||||
print: element.querySelector("#print"),
|
||||
editorFreeTextButton: element.querySelector("#editorFreeText"),
|
||||
zoomIn: element.querySelector("#zoomInButton"),
|
||||
zoomOut: element.querySelector("#zoomOutButton"),
|
||||
print: element.querySelector("#printButton"),
|
||||
editorFreeTextButton: element.querySelector("#editorFreeTextButton"),
|
||||
editorFreeTextParamsToolbar: element.querySelector(
|
||||
"#editorFreeTextParamsToolbar"
|
||||
),
|
||||
editorInkButton: element.querySelector("#editorInk"),
|
||||
editorHighlightButton: element.querySelector("#editorHighlightButton"),
|
||||
editorHighlightParamsToolbar: element.querySelector(
|
||||
"#editorHighlightParamsToolbar"
|
||||
),
|
||||
editorHighlightColorPicker: element.querySelector(
|
||||
"#editorHighlightColorPicker"
|
||||
),
|
||||
editorInkButton: element.querySelector("#editorInkButton"),
|
||||
editorInkParamsToolbar: element.querySelector("#editorInkParamsToolbar"),
|
||||
download: element.querySelector("#download"),
|
||||
editorStampButton: element.querySelector("#editorStampButton"),
|
||||
editorStampParamsToolbar: element.querySelector(
|
||||
"#editorStampParamsToolbar"
|
||||
),
|
||||
download: element.querySelector("#downloadButton"),
|
||||
},
|
||||
secondaryToolbar: {
|
||||
toolbar: element.querySelector("#secondaryToolbar"),
|
||||
toggleButton: element.querySelector("#secondaryToolbarToggle"),
|
||||
toggleButton: element.querySelector("#secondaryToolbarToggleButton"),
|
||||
presentationModeButton: element.querySelector("#presentationMode"),
|
||||
openFileButton:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
|
||||
|
@ -90,13 +99,20 @@ function getViewerConfiguration(element) {
|
|||
spreadNoneButton: element.querySelector("#spreadNone"),
|
||||
spreadOddButton: element.querySelector("#spreadOdd"),
|
||||
spreadEvenButton: element.querySelector("#spreadEven"),
|
||||
imageAltTextSettingsButton: element.querySelector(
|
||||
"#imageAltTextSettings"
|
||||
),
|
||||
imageAltTextSettingsSeparator: element.querySelector(
|
||||
"#imageAltTextSettingsSeparator"
|
||||
),
|
||||
documentPropertiesButton: element.querySelector("#documentProperties"),
|
||||
},
|
||||
sidebar: {
|
||||
// Divs (and sidebar button)
|
||||
outerContainer: element.querySelector("#outerContainer"),
|
||||
sidebarContainer: element.querySelector("#sidebarContainer"),
|
||||
toggleButton: element.querySelector("#sidebarToggle"),
|
||||
toggleButton: element.querySelector("#sidebarToggleButton"),
|
||||
resizer: element.querySelector("#sidebarResizer"),
|
||||
// Buttons
|
||||
thumbnailButton: element.querySelector("#viewThumbnail"),
|
||||
outlineButton: element.querySelector("#viewOutline"),
|
||||
|
@ -108,16 +124,11 @@ function getViewerConfiguration(element) {
|
|||
attachmentsView: element.querySelector("#attachmentsView"),
|
||||
layersView: element.querySelector("#layersView"),
|
||||
// View-specific options
|
||||
outlineOptionsContainer: element.querySelector("#outlineOptionsContainer"),
|
||||
currentOutlineItemButton: element.querySelector("#currentOutlineItem"),
|
||||
},
|
||||
sidebarResizer: {
|
||||
outerContainer: element.querySelector("#outerContainer"),
|
||||
resizer: element.querySelector("#sidebarResizer"),
|
||||
},
|
||||
findBar: {
|
||||
bar: element.querySelector("#findbar"),
|
||||
toggleButton: element.querySelector("#viewFind"),
|
||||
toggleButton: element.querySelector("#viewFindButton"),
|
||||
findField: element.querySelector("#findInput"),
|
||||
highlightAllCheckbox: element.querySelector("#findHighlightAll"),
|
||||
caseSensitiveCheckbox: element.querySelector("#findMatchCase"),
|
||||
|
@ -125,8 +136,8 @@ function getViewerConfiguration(element) {
|
|||
entireWordCheckbox: element.querySelector("#findEntireWord"),
|
||||
findMsg: element.querySelector("#findMsg"),
|
||||
findResultsCount: element.querySelector("#findResultsCount"),
|
||||
findPreviousButton: element.querySelector("#findPrevious"),
|
||||
findNextButton: element.querySelector("#findNext"),
|
||||
findPreviousButton: element.querySelector("#findPreviousButton"),
|
||||
findNextButton: element.querySelector("#findNextButton"),
|
||||
},
|
||||
passwordOverlay: {
|
||||
dialog: element.querySelector("#passwordDialog"),
|
||||
|
@ -155,35 +166,91 @@ function getViewerConfiguration(element) {
|
|||
linearized: element.querySelector("#linearizedField"),
|
||||
},
|
||||
},
|
||||
altTextDialog: {
|
||||
dialog: element.querySelector("#altTextDialog"),
|
||||
optionDescription: element.querySelector("#descriptionButton"),
|
||||
optionDecorative: element.querySelector("#decorativeButton"),
|
||||
textarea: element.querySelector("#descriptionTextarea"),
|
||||
cancelButton: element.querySelector("#altTextCancel"),
|
||||
saveButton: element.querySelector("#altTextSave"),
|
||||
},
|
||||
newAltTextDialog: {
|
||||
dialog: element.querySelector("#newAltTextDialog"),
|
||||
title: element.querySelector("#newAltTextTitle"),
|
||||
descriptionContainer: element.querySelector(
|
||||
"#newAltTextDescriptionContainer"
|
||||
),
|
||||
textarea: element.querySelector("#newAltTextDescriptionTextarea"),
|
||||
disclaimer: element.querySelector("#newAltTextDisclaimer"),
|
||||
learnMore: element.querySelector("#newAltTextLearnMore"),
|
||||
imagePreview: element.querySelector("#newAltTextImagePreview"),
|
||||
createAutomatically: element.querySelector(
|
||||
"#newAltTextCreateAutomatically"
|
||||
),
|
||||
createAutomaticallyButton: element.querySelector(
|
||||
"#newAltTextCreateAutomaticallyButton"
|
||||
),
|
||||
downloadModel: element.querySelector("#newAltTextDownloadModel"),
|
||||
downloadModelDescription: element.querySelector(
|
||||
"#newAltTextDownloadModelDescription"
|
||||
),
|
||||
error: element.querySelector("#newAltTextError"),
|
||||
errorCloseButton: element.querySelector("#newAltTextCloseButton"),
|
||||
cancelButton: element.querySelector("#newAltTextCancel"),
|
||||
notNowButton: element.querySelector("#newAltTextNotNow"),
|
||||
saveButton: element.querySelector("#newAltTextSave"),
|
||||
},
|
||||
altTextSettingsDialog: {
|
||||
dialog: element.querySelector("#altTextSettingsDialog"),
|
||||
createModelButton: element.querySelector("#createModelButton"),
|
||||
aiModelSettings: element.querySelector("#aiModelSettings"),
|
||||
learnMore: element.querySelector("#altTextSettingsLearnMore"),
|
||||
deleteModelButton: element.querySelector("#deleteModelButton"),
|
||||
downloadModelButton: element.querySelector("#downloadModelButton"),
|
||||
showAltTextDialogButton: element.querySelector(
|
||||
"#showAltTextDialogButton"
|
||||
),
|
||||
altTextSettingsCloseButton: element.querySelector(
|
||||
"#altTextSettingsCloseButton"
|
||||
),
|
||||
closeButton: element.querySelector("#altTextSettingsCloseButton"),
|
||||
},
|
||||
annotationEditorParams: {
|
||||
editorFreeTextFontSize: element.querySelector("#editorFreeTextFontSize"),
|
||||
editorFreeTextColor: element.querySelector("#editorFreeTextColor"),
|
||||
editorInkColor: element.querySelector("#editorInkColor"),
|
||||
editorInkThickness: element.querySelector("#editorInkThickness"),
|
||||
editorInkOpacity: element.querySelector("#editorInkOpacity"),
|
||||
editorStampAddImage: element.querySelector("#editorStampAddImage"),
|
||||
editorFreeHighlightThickness: element.querySelector(
|
||||
"#editorFreeHighlightThickness"
|
||||
),
|
||||
editorHighlightShowAll: element.querySelector("#editorHighlightShowAll"),
|
||||
},
|
||||
printContainer: element.querySelector("#printContainer"),
|
||||
openFileInput:
|
||||
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
|
||||
? element.querySelector("#fileInput")
|
||||
: null,
|
||||
debuggerScriptPath: "./debugger.js",
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE
|
||||
function webViewerLoad(file, element, pdfPage, annoId) {
|
||||
AppOptions.set("workerSrc", `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.min.mjs?v=4.7.85`);
|
||||
AppOptions.set("defaultUrl", file);
|
||||
AppOptions.set("cMapUrl", 'cmaps/');
|
||||
AppOptions.set("standardFontDataUrl", 'standard_fonts/');
|
||||
const pdf = new PDFViewerApplication(pdfPage)
|
||||
pdf.annoId = annoId
|
||||
const config = getViewerConfiguration(element);
|
||||
|
||||
config.file = file;
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
|
||||
// Give custom implementations of the default viewer a simpler way to
|
||||
// set various `AppOptions`, by dispatching an event once all viewer
|
||||
// files are loaded but *before* the viewer initialization has run.
|
||||
const event = document.createEvent("CustomEvent");
|
||||
event.initCustomEvent("webviewerloaded", true, true, {
|
||||
source: window,
|
||||
const event = new CustomEvent("webviewerloaded", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: {
|
||||
source: window,
|
||||
},
|
||||
});
|
||||
try {
|
||||
// Attempt to dispatch the event at the embedding `document`,
|
||||
|
@ -196,11 +263,9 @@ function webViewerLoad(file, element, pdfPage, annoId) {
|
|||
console.error(`webviewerloaded: ${ex}`);
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
} else {
|
||||
config.file = file
|
||||
}
|
||||
pdf.run(config)
|
||||
initAnno(element, pdf, config);
|
||||
pdf.run(config);
|
||||
initAnno(element, pdf);
|
||||
return pdf
|
||||
}
|
||||
|
||||
|
@ -220,5 +285,8 @@ document.blockUnblockOnload?.(true);
|
|||
|
||||
// NOTE
|
||||
export {
|
||||
webViewerLoad,
|
||||
// PDFViewerApplication,
|
||||
// AppConstants as PDFViewerApplicationConstants,
|
||||
// AppOptions as PDFViewerApplicationOptions,
|
||||
webViewerLoad
|
||||
};
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
||||
|
||||
|
@ -22,7 +24,6 @@ import { XfaLayer } from "./pdfjs";
|
|||
|
||||
/**
|
||||
* @typedef {Object} XfaLayerBuilderOptions
|
||||
* @property {HTMLDivElement} pageDiv
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {AnnotationStorage} [annotationStorage]
|
||||
* @property {IPDFLinkService} linkService
|
||||
|
@ -34,13 +35,11 @@ class XfaLayerBuilder {
|
|||
* @param {XfaLayerBuilderOptions} options
|
||||
*/
|
||||
constructor({
|
||||
pageDiv,
|
||||
pdfPage,
|
||||
annotationStorage = null,
|
||||
linkService,
|
||||
xfaHtml = null,
|
||||
}) {
|
||||
this.pageDiv = pageDiv;
|
||||
this.pdfPage = pdfPage;
|
||||
this.annotationStorage = annotationStorage;
|
||||
this.linkService = linkService;
|
||||
|
@ -69,9 +68,8 @@ class XfaLayerBuilder {
|
|||
};
|
||||
|
||||
// Create an xfa layer div and render the form
|
||||
const div = document.createElement("div");
|
||||
this.pageDiv.append(div);
|
||||
parameters.div = div;
|
||||
this.div = document.createElement("div");
|
||||
parameters.div = this.div;
|
||||
|
||||
return XfaLayer.render(parameters);
|
||||
}
|
||||
|
@ -96,7 +94,6 @@ class XfaLayerBuilder {
|
|||
}
|
||||
// Create an xfa layer div and render the form
|
||||
this.div = document.createElement("div");
|
||||
this.pageDiv.append(this.div);
|
||||
parameters.div = this.div;
|
||||
|
||||
return XfaLayer.render(parameters);
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
& > div {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
&:first-child {
|
||||
border-bottom-left-radius: var(--b3-border-radius);
|
||||
|
@ -136,7 +137,7 @@
|
|||
|
||||
#sidebarContainer {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
top: 30px;
|
||||
bottom: 0;
|
||||
width: var(--b3-pdf-sidebar-width);
|
||||
visibility: hidden;
|
||||
|
@ -174,7 +175,7 @@
|
|||
}
|
||||
|
||||
#sidebarContent {
|
||||
top: 32px;
|
||||
top: 30px;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
@ -187,7 +188,7 @@
|
|||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
@ -213,14 +214,14 @@
|
|||
#toolbarSidebar,
|
||||
#toolbarViewer {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
padding-left: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#toolbarViewer #numPages {
|
||||
line-height: 32px;
|
||||
line-height: 30px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
|
@ -316,9 +317,9 @@
|
|||
}
|
||||
|
||||
.findbar {
|
||||
left: 64px;
|
||||
left: 40px;
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
top: 30px;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -334,10 +335,11 @@
|
|||
.secondaryToolbar {
|
||||
right: 4px;
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
top: 30px;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
z-index: 3;
|
||||
// 需高于 pdf 中的链接
|
||||
z-index: 38;
|
||||
|
||||
.b3-menu__items {
|
||||
max-height: 60vh;
|
||||
|
@ -361,12 +363,11 @@
|
|||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 32px;
|
||||
padding: 5px;
|
||||
user-select: none;
|
||||
width: 20px;
|
||||
width: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
color: var(--b3-theme-on-surface);
|
||||
overflow: initial;
|
||||
|
@ -377,8 +378,8 @@
|
|||
}
|
||||
|
||||
& > svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
|
@ -405,13 +406,11 @@
|
|||
.dropdownToolbarButton {
|
||||
--scale-select-width: 140px;
|
||||
width: var(--scale-select-width);
|
||||
margin: 4px 8px 0 0;
|
||||
margin: 2px 8px 2px 0;
|
||||
|
||||
select {
|
||||
width: inherit;
|
||||
min-width: auto;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
@ -440,7 +439,7 @@
|
|||
text-align: right;
|
||||
width: 48px;
|
||||
padding: 1px 8px;
|
||||
margin: 4px 0;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.toolbarField.pageNumber::-webkit-inner-spin-button,
|
||||
|
@ -459,10 +458,10 @@
|
|||
#outlineView,
|
||||
#attachmentsView,
|
||||
#layersView {
|
||||
width: calc(100% - 16px);
|
||||
width: calc(100% - 60px);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 8px;
|
||||
padding: 10px 30px;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: absolute;
|
||||
|
@ -474,7 +473,18 @@
|
|||
}
|
||||
|
||||
#thumbnailView .thumbnail {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 5px;
|
||||
padding: 1px;
|
||||
border: 7px solid transparent;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--b3-list-hover);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--b3-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnailImage {
|
||||
|
@ -740,7 +750,7 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
|
|||
background: none repeat scroll 0 0 rgba(255, 255, 255, 1);
|
||||
border: 1px solid rgba(102, 102, 102, 1);
|
||||
position: fixed;
|
||||
top: 32px;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 10px;
|
||||
|
@ -840,6 +850,10 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
|
|||
color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
.editToolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rect-to-annotation {
|
||||
cursor: crosshair !important;
|
||||
user-select: none;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self'"/>-->
|
||||
<style id="editorAttr" type="text/css"></style>
|
||||
<script src="../../protyle/js/pdf/pdf.min.mjs?v=4.7.85" type="module"></script>
|
||||
</head>
|
||||
<body class="fn__flex-column">
|
||||
<div id="loading" class="b3-dialog b3-dialog--open">
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials">
|
||||
<link rel="apple-touch-icon" href="../../icon.png">
|
||||
<style id="editorAttr" type="text/css"></style>
|
||||
<script src="../../protyle/js/pdf/pdf.min.mjs?v=4.7.85" type="module"></script>
|
||||
</head>
|
||||
<body class="fn__flex-column">
|
||||
<div id="loading" class="b3-dialog b3-dialog--open">
|
||||
|
|
|
@ -73,8 +73,8 @@ export const globalClick = (event: MouseEvent & { target: HTMLElement }) => {
|
|||
}
|
||||
|
||||
// 点击空白,pdf 搜索、更多消失
|
||||
if (hasClosestByAttribute(event.target, "id", "secondaryToolbarToggle") ||
|
||||
hasClosestByAttribute(event.target, "id", "viewFind") ||
|
||||
if (hasClosestByAttribute(event.target, "id", "secondaryToolbarToggleButton") ||
|
||||
hasClosestByAttribute(event.target, "id", "viewFindButton") ||
|
||||
hasClosestByAttribute(event.target, "id", "findbar")) {
|
||||
return;
|
||||
}
|
||||
|
|
22
app/stage/protyle/js/pdf/pdf.js
vendored
22
app/stage/protyle/js/pdf/pdf.js
vendored
File diff suppressed because one or more lines are too long
21
app/stage/protyle/js/pdf/pdf.min.mjs
vendored
Normal file
21
app/stage/protyle/js/pdf/pdf.min.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
21
app/stage/protyle/js/pdf/pdf.sandbox.min.mjs
vendored
Normal file
21
app/stage/protyle/js/pdf/pdf.sandbox.min.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
22
app/stage/protyle/js/pdf/pdf.worker.js
vendored
22
app/stage/protyle/js/pdf/pdf.worker.js
vendored
File diff suppressed because one or more lines are too long
21
app/stage/protyle/js/pdf/pdf.worker.min.mjs
vendored
Normal file
21
app/stage/protyle/js/pdf/pdf.worker.min.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue