This commit is contained in:
Vanessa 2022-10-01 17:17:46 +08:00
parent ae8ce006ba
commit da71f8c4aa
44 changed files with 4505 additions and 3740 deletions

View file

@ -28,7 +28,7 @@ const rectAnno = (config: any, pdf: any, element: HTMLElement) => {
// 右键 // 右键
return; return;
} }
const canvasRect = pdf.pdfViewer._getCurrentVisiblePage().first.view.canvas.getBoundingClientRect(); const canvasRect = pdf.pdfViewer._getVisiblePages().first.view.canvas.getBoundingClientRect();
const containerRet = config.mainContainer.getBoundingClientRect(); const containerRet = config.mainContainer.getBoundingClientRect();
const mostLeft = canvasRect.left; const mostLeft = canvasRect.left;
const mostRight = canvasRect.right; const mostRight = canvasRect.right;

View file

@ -322,7 +322,7 @@ export class Asset extends Model {
</div> </div>
</div> </div>
<div id="overlayContainer" class="fn__hidden"> <div id="overlayContainer" class="fn__hidden">
<div id="passwordOverlay" class="container fn__hidden"> <div id="passwordDialog" class="container fn__hidden">
<div class="dialog"> <div class="dialog">
<div class="row"> <div class="row">
<p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p> <p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p>
@ -336,7 +336,7 @@ export class Asset extends Model {
</div> </div>
</div> </div>
</div> </div>
<div id="documentPropertiesOverlay" class="container fn__hidden"> <div id="documentPropertiesDialog" class="container fn__hidden">
<div class="dialog b3-menu"> <div class="dialog b3-menu">
<div class="row"> <div class="row">
<span>${window.siyuan.languages.fileName}</span> <p id="fileNameField">-</p> <span>${window.siyuan.languages.fileName}</span> <p id="fileNameField">-</p>
@ -427,6 +427,13 @@ export class Asset extends Model {
<span class="b3-menu__label">${window.siyuan.languages.remove}</span> <span class="b3-menu__label">${window.siyuan.languages.remove}</span>
</button> </button>
</div> </div>
<div class="fn__none">
<input id="editorFreeTextFontSize">
<input id="editorFreeTextColor">
<input id="editorInkColor">
<input id="editorInkThickness">
<input id="editorInkOpacity">
</div>
</div> <!-- outerContainer --> </div> <!-- outerContainer -->
<div id="printContainer"></div>`; <div id="printContainer"></div>`;
// 初始化完成后需等待页签是否显示设置完成,才可以判断 pdf 是否能进行渲染 // 初始化完成后需等待页签是否显示设置完成,才可以判断 pdf 是否能进行渲染

View file

@ -13,6 +13,15 @@
* limitations under the License. * 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("./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 */
import { AnnotationLayer } from "./pdfjs"; import { AnnotationLayer } from "./pdfjs";
import { NullL10n } from "./l10n_utils.js"; import { NullL10n } from "./l10n_utils.js";
@ -33,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
* [fieldObjectsPromise] * [fieldObjectsPromise]
* @property {Object} [mouseState] * @property {Object} [mouseState]
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} accessibilityManager
*/ */
class AnnotationLayerBuilder { class AnnotationLayerBuilder {
@ -53,6 +63,7 @@ class AnnotationLayerBuilder {
fieldObjectsPromise = null, fieldObjectsPromise = null,
mouseState = null, mouseState = null,
annotationCanvasMap = null, annotationCanvasMap = null,
accessibilityManager = null,
}) { }) {
this.pageDiv = pageDiv; this.pageDiv = pageDiv;
this.pdfPage = pdfPage; this.pdfPage = pdfPage;
@ -67,6 +78,7 @@ class AnnotationLayerBuilder {
this._fieldObjectsPromise = fieldObjectsPromise; this._fieldObjectsPromise = fieldObjectsPromise;
this._mouseState = mouseState; this._mouseState = mouseState;
this._annotationCanvasMap = annotationCanvasMap; this._annotationCanvasMap = annotationCanvasMap;
this._accessibilityManager = accessibilityManager;
this.div = null; this.div = null;
this._cancelled = false; this._cancelled = false;
@ -105,6 +117,7 @@ class AnnotationLayerBuilder {
fieldObjects, fieldObjects,
mouseState: this._mouseState, mouseState: this._mouseState,
annotationCanvasMap: this._annotationCanvasMap, annotationCanvasMap: this._annotationCanvasMap,
accessibilityManager: this._accessibilityManager,
}; };
if (this.div) { if (this.div) {
@ -116,7 +129,7 @@ class AnnotationLayerBuilder {
// if there is at least one annotation. // if there is at least one annotation.
this.div = document.createElement("div"); this.div = document.createElement("div");
this.div.className = "annotationLayer"; this.div.className = "annotationLayer";
this.pageDiv.appendChild(this.div); this.pageDiv.append(this.div);
parameters.div = this.div; parameters.div = this.div;
AnnotationLayer.render(parameters); AnnotationLayer.render(parameters);

File diff suppressed because it is too large Load diff

View file

@ -17,28 +17,21 @@ import { Constants } from '../../constants'
const compatibilityParams = Object.create(null) const compatibilityParams = Object.create(null)
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) { if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
const userAgent = if (
(typeof navigator !== 'undefined' && navigator.userAgent) || '' typeof PDFJSDev !== 'undefined' &&
const platform = PDFJSDev.test('LIB') &&
(typeof navigator !== 'undefined' && navigator.platform) || '' typeof navigator === 'undefined'
const maxTouchPoints = ) {
(typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1 globalThis.navigator = Object.create(null)
}
const userAgent = navigator.userAgent || ''
const platform = navigator.platform || ''
const maxTouchPoints = navigator.maxTouchPoints || 1
const isAndroid = /Android/.test(userAgent) const isAndroid = /Android/.test(userAgent)
const isIOS = const isIOS =
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
(platform === 'MacIntel' && maxTouchPoints > 1) (platform === 'MacIntel' && maxTouchPoints > 1);
const isIOSChrome = /CriOS/.test(userAgent);
// Disables URL.createObjectURL() usage in some environments.
// Support: Chrome on iOS
(function checkOnBlobSupport () {
// Sometimes Chrome on iOS loses data created with createObjectURL(),
// see issue 8081.
if (isIOSChrome) {
compatibilityParams.disableCreateObjectURL = true
}
})();
// Limit canvas size to 5 mega-pixels on mobile. // Limit canvas size to 5 mega-pixels on mobile.
// Support: Android, iOS // Support: Android, iOS
@ -62,9 +55,14 @@ const OptionKind = {
* primitive types and cannot rely on any imported types. * primitive types and cannot rely on any imported types.
*/ */
const defaultOptions = { const defaultOptions = {
annotationEditorMode: {
/** @type {number} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
annotationMode: { annotationMode: {
/** @type {number} */ /** @type {number} */
value: 2, // https://github.com/siyuan-note/siyuan/issues/2975 DISABLE: 0, ENABLE: 1, ENABLE_FORMS: 2 (default), ENABLE_STORAGE: 3 value: 2,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
cursorToolOnLoad: { cursorToolOnLoad: {
@ -72,11 +70,6 @@ const defaultOptions = {
value: 0, value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
defaultUrl: {
/** @type {string} */
value: 'compressed.tracemonkey-pldi-09.pdf',
kind: OptionKind.VIEWER,
},
defaultZoomValue: { defaultZoomValue: {
/** @type {string} */ /** @type {string} */
value: '', value: '',
@ -135,9 +128,23 @@ const defaultOptions = {
maxCanvasPixels: { maxCanvasPixels: {
/** @type {number} */ /** @type {number} */
value: 16777216, value: 16777216,
compatibility: compatibilityParams.maxCanvasPixels,
kind: OptionKind.VIEWER, kind: OptionKind.VIEWER,
}, },
forcePageColors: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pageColorsBackground: {
/** @type {string} */
value: 'Canvas',
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pageColorsForeground: {
/** @type {string} */
value: 'CanvasText',
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pdfBugEnabled: { pdfBugEnabled: {
/** @type {boolean} */ /** @type {boolean} */
value: typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION'), value: typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION'),
@ -148,11 +155,6 @@ const defaultOptions = {
value: 150, value: 150,
kind: OptionKind.VIEWER, kind: OptionKind.VIEWER,
}, },
renderer: {
/** @type {string} */
value: 'canvas',
kind: OptionKind.VIEWER,
},
sidebarViewOnLoad: { sidebarViewOnLoad: {
/** @type {number} */ /** @type {number} */
value: -1, value: -1,
@ -196,7 +198,10 @@ const defaultOptions = {
}, },
cMapUrl: { cMapUrl: {
/** @type {string} */ /** @type {string} */
value: 'cmaps/', value:
typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')
? '../external/bcmaps/'
: 'cmaps/', // NOTE
kind: OptionKind.API, kind: OptionKind.API,
}, },
disableAutoFetch: { disableAutoFetch: {
@ -270,7 +275,8 @@ const defaultOptions = {
}, },
workerSrc: { workerSrc: {
/** @type {string} */ /** @type {string} */
value: `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.js?v=2.14.102`, // NOTE
value: `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.js?v=3.0.150`,
kind: OptionKind.WORKER, kind: OptionKind.WORKER,
}, },
} }
@ -278,6 +284,11 @@ if (
typeof PDFJSDev === 'undefined' || typeof PDFJSDev === 'undefined' ||
PDFJSDev.test('!PRODUCTION || GENERIC') PDFJSDev.test('!PRODUCTION || GENERIC')
) { ) {
defaultOptions.defaultUrl = {
/** @type {string} */
value: 'compressed.tracemonkey-pldi-09.pdf',
kind: OptionKind.VIEWER,
}
defaultOptions.disablePreferences = { defaultOptions.disablePreferences = {
/** @type {boolean} */ /** @type {boolean} */
value: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('TESTING'), value: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('TESTING'),
@ -285,9 +296,14 @@ if (
} }
defaultOptions.locale = { defaultOptions.locale = {
/** @type {string} */ /** @type {string} */
value: typeof navigator !== 'undefined' ? navigator.language : 'en-US', value: navigator.language || 'en-US',
kind: OptionKind.VIEWER, kind: OptionKind.VIEWER,
} }
defaultOptions.renderer = {
/** @type {string} */
value: 'canvas',
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}
defaultOptions.sandboxBundleSrc = { defaultOptions.sandboxBundleSrc = {
/** @type {string} */ /** @type {string} */
value: value:
@ -296,9 +312,12 @@ if (
: '../build/pdf.sandbox.js', : '../build/pdf.sandbox.js',
kind: OptionKind.VIEWER, kind: OptionKind.VIEWER,
} }
defaultOptions.renderer.kind += OptionKind.PREFERENCE
} else if (PDFJSDev.test('CHROME')) { } else if (PDFJSDev.test('CHROME')) {
defaultOptions.defaultUrl = {
/** @type {string} */
value: '',
kind: OptionKind.VIEWER,
}
defaultOptions.disableTelemetry = { defaultOptions.disableTelemetry = {
/** @type {boolean} */ /** @type {boolean} */
value: false, value: false,
@ -325,7 +344,7 @@ class AppOptions {
} }
const defaultOption = defaultOptions[name] const defaultOption = defaultOptions[name]
if (defaultOption !== undefined) { if (defaultOption !== undefined) {
return defaultOption.compatibility ?? defaultOption.value return compatibilityParams[name] ?? defaultOption.value
} }
return undefined return undefined
} }
@ -357,7 +376,7 @@ class AppOptions {
options[name] = options[name] =
userOption !== undefined userOption !== undefined
? userOption ? userOption
: defaultOption.compatibility ?? defaultOption.value : compatibilityParams[name] ?? defaultOption.value
} }
return options return options
} }

View file

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { removeNullCharacters } from "./ui_utils"; import { removeNullCharacters } from "./ui_utils.js";
const TREEITEM_OFFSET_TOP = -100; // px const TREEITEM_OFFSET_TOP = -100; // px
const TREEITEM_SELECTED_CLASS = "selected"; const TREEITEM_SELECTED_CLASS = "selected";
@ -74,6 +74,7 @@ class BaseTreeViewer {
*/ */
_addToggleButton(div, hidden = false) { _addToggleButton(div, hidden = false) {
const toggler = document.createElement("div"); const toggler = document.createElement("div");
// NOTE
toggler.innerHTML = `<svg><use xlink:href="#iconDown"></use></svg>` toggler.innerHTML = `<svg><use xlink:href="#iconDown"></use></svg>`
toggler.className = "treeItemToggler"; toggler.className = "treeItemToggler";
if (hidden) { if (hidden) {
@ -88,7 +89,7 @@ class BaseTreeViewer {
this._toggleTreeItem(div, shouldShowAll); this._toggleTreeItem(div, shouldShowAll);
} }
}; };
div.insertBefore(toggler, div.firstChild); div.prepend(toggler);
} }
/** /**
@ -123,7 +124,7 @@ class BaseTreeViewer {
this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden"); this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden");
} }
this.container.appendChild(fragment); this.container.append(fragment);
this._dispatchEvent(count); this._dispatchEvent(count);
} }
@ -168,7 +169,7 @@ class BaseTreeViewer {
this.container.scrollTo( this.container.scrollTo(
treeItem.offsetLeft, treeItem.offsetLeft,
treeItem.offsetTop + TREEITEM_OFFSET_TOP + treeItem.offsetParent.offsetTop treeItem.offsetTop + TREEITEM_OFFSET_TOP
); );
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
import { createValidAbsoluteUrl, isPdfFile } from "./pdfjs"; import { createValidAbsoluteUrl, isPdfFile } from "./pdfjs";
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) {
@ -36,7 +38,7 @@ function download(blobUrl, filename) {
} }
// <a> must be in the document for recent Firefox versions, // <a> must be in the document for recent Firefox versions,
// otherwise .click() is ignored. // otherwise .click() is ignored.
(document.body || document.documentElement).appendChild(a); (document.body || document.documentElement).append(a);
a.click(); a.click();
a.remove(); a.remove();
} }
@ -107,13 +109,7 @@ class DownloadManager {
return false; return false;
} }
/** download(blob, url, filename) {
* @param sourceEventType {string} Used to signal what triggered the download.
* The version of PDF.js integrated with Firefox uses this to to determine
* which dialog to show. "save" triggers "save as" and "download" triggers
* the "open with" dialog.
*/
download(blob, url, filename, sourceEventType = "download") {
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob);
download(blobUrl, filename); download(blobUrl, filename);
} }

View file

@ -13,29 +13,30 @@
* limitations under the License. * limitations under the License.
*/ */
import { BasePreferences } from './preferences.js' import { BasePreferences } from "./preferences.js";
import { DownloadManager } from './download_manager.js' import { DownloadManager } from "./download_manager.js";
import { GenericScripting } from './generic_scripting.js' import { GenericScripting } from "./generic_scripting.js";
import { shadow } from './pdfjs' import { shadow } from './pdfjs'
if (typeof PDFJSDev !== 'undefined' && !PDFJSDev.test('GENERIC')) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
throw new Error( throw new Error(
'Module "pdfjs-web/genericcom" shall not be used outside GENERIC build.', 'Module "pdfjs-web/genericcom" shall not be used outside GENERIC build.'
) );
} }
const GenericCom = {} const GenericCom = {};
class GenericPreferences extends BasePreferences { class GenericPreferences extends BasePreferences {
async _writeToStorage(prefObj) { async _writeToStorage(prefObj) {
localStorage.setItem('pdfjs.preferences', JSON.stringify(prefObj)) localStorage.setItem("pdfjs.preferences", JSON.stringify(prefObj));
} }
async _readFromStorage(prefObj) { async _readFromStorage(prefObj) {
return JSON.parse(localStorage.getItem('pdfjs.preferences')) return JSON.parse(localStorage.getItem("pdfjs.preferences"));
} }
} }
// NOTE
class GenericExternalServices { class GenericExternalServices {
constructor () { constructor () {
throw new Error('Cannot initialize DefaultExternalServices.') throw new Error('Cannot initialize DefaultExternalServices.')
@ -71,16 +72,21 @@ class GenericExternalServices {
} }
static createDownloadManager(options) { static createDownloadManager(options) {
return new DownloadManager() return new DownloadManager();
} }
static createPreferences() { static createPreferences() {
return new GenericPreferences() return new GenericPreferences();
}
static createL10n({ locale = "en-US" }) {
// NOTE
// return new GenericL10n(locale);
} }
static createScripting({ sandboxBundleSrc }) { static createScripting({ sandboxBundleSrc }) {
return new GenericScripting(sandboxBundleSrc) return new GenericScripting(sandboxBundleSrc);
} }
} }
// NOTE
export { GenericCom, GenericExternalServices } export { GenericCom, GenericExternalServices };

View file

@ -153,7 +153,7 @@ class GrabToPan {
this.element.scrollLeft = scrollLeft; this.element.scrollLeft = scrollLeft;
} }
if (!this.overlay.parentNode) { if (!this.overlay.parentNode) {
document.body.appendChild(this.overlay); document.body.append(this.overlay);
} }
} }

View file

@ -81,6 +81,11 @@ const DEFAULT_L10N_STRINGS = {
printing_not_ready: "Warning: The PDF is not fully loaded for printing.", printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
web_fonts_disabled: web_fonts_disabled:
"Web fonts are disabled: unable to use embedded PDF fonts.", "Web fonts are disabled: unable to use embedded PDF fonts.",
free_text2_default_content: "Start typing…",
editor_free_text2_aria_label: "Text Editor",
editor_ink2_aria_label: "Draw Editor",
editor_ink_canvas_aria_label: "User-created image",
}; };
function getL10nFallback(key, args) { function getL10nFallback(key, args) {

View file

@ -14,123 +14,103 @@
*/ */
class OverlayManager { class OverlayManager {
#overlays = Object.create(null); #overlays = new WeakMap();
#active = null; #active = null;
#keyDownBound = null;
get active() { get active() {
return this.#active; return this.#active;
} }
/** /**
* @param {string} name - The name of the overlay that is registered. * @param {HTMLDialogElement} dialog - The overlay's DOM element.
* @param {HTMLDivElement} element - The overlay's DOM element.
* @param {function} [callerCloseMethod] - The method that, if present, calls
* `OverlayManager.close` from the object registering the
* overlay. Access to this method is necessary in order to
* run cleanup code when e.g. the overlay is force closed.
* The default is `null`.
* @param {boolean} [canForceClose] - Indicates if opening the overlay closes * @param {boolean} [canForceClose] - Indicates if opening the overlay closes
* an active overlay. The default is `false`. * an active overlay. The default is `false`.
* @returns {Promise} A promise that is resolved when the overlay has been * @returns {Promise} A promise that is resolved when the overlay has been
* registered. * registered.
*/ */
async register( async register(dialog, canForceClose = false) {
name, if (typeof dialog !== "object") {
element,
callerCloseMethod = null,
canForceClose = false
) {
let container;
if (!name || !element || !(container = element.parentNode)) {
throw new Error("Not enough parameters."); throw new Error("Not enough parameters.");
} else if (this.#overlays[name]) { } else if (this.#overlays.has(dialog)) {
throw new Error("The overlay is already registered."); throw new Error("The overlay is already registered.");
} }
this.#overlays[name] = { this.#overlays.set(dialog, { canForceClose });
element,
container, // NOTE
callerCloseMethod, // if (
canForceClose, // 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 {string} name - The name of the overlay that is unregistered. * @param {HTMLDialogElement} dialog - The overlay's DOM element.
* @returns {Promise} A promise that is resolved when the overlay has been * @returns {Promise} A promise that is resolved when the overlay has been
* unregistered. * unregistered.
*/ */
async unregister(name) { async unregister(dialog) {
if (!this.#overlays[name]) { if (!this.#overlays.has(dialog)) {
throw new Error("The overlay does not exist."); throw new Error("The overlay does not exist.");
} else if (this.#active === name) { } else if (this.#active === dialog) {
throw new Error("The overlay cannot be removed while it is active."); throw new Error("The overlay cannot be removed while it is active.");
} }
delete this.#overlays[name]; this.#overlays.delete(dialog);
} }
/** /**
* @param {string} name - The name of the overlay that should be opened. * @param {HTMLDialogElement} dialog - The overlay's DOM element.
* @returns {Promise} A promise that is resolved when the overlay has been * @returns {Promise} A promise that is resolved when the overlay has been
* opened. * opened.
*/ */
async open(name) { async open(dialog) {
if (!this.#overlays[name]) { if (!this.#overlays.has(dialog)) {
throw new Error("The overlay does not exist."); throw new Error("The overlay does not exist.");
} else if (this.#active) { } else if (this.#active) {
if (this.#active === name) { if (this.#active === dialog) {
throw new Error("The overlay is already active."); throw new Error("The overlay is already active.");
} else if (this.#overlays[name].canForceClose) { } else if (this.#overlays.get(dialog).canForceClose) {
this.#closeThroughCaller(); await this.close();
} else { } else {
throw new Error("Another overlay is currently active."); throw new Error("Another overlay is currently active.");
} }
} }
this.#active = name; this.#active = dialog;
this.#overlays[this.#active].element.classList.remove("fn__hidden"); dialog.showModal();
this.#overlays[this.#active].container.classList.remove("fn__hidden");
this.#keyDownBound = this.#keyDown.bind(this);
window.addEventListener("keydown", this.#keyDownBound);
} }
/** /**
* @param {string} name - The name of the overlay that should be closed. * @param {HTMLDialogElement} dialog - The overlay's DOM element.
* @returns {Promise} A promise that is resolved when the overlay has been * @returns {Promise} A promise that is resolved when the overlay has been
* closed. * closed.
*/ */
async close(name) { async close(dialog = this.#active) {
if (!this.#overlays[name]) { if (!this.#overlays.has(dialog)) {
throw new Error("The overlay does not exist."); throw new Error("The overlay does not exist.");
} else if (!this.#active) { } else if (!this.#active) {
throw new Error("The overlay is currently not active."); throw new Error("The overlay is currently not active.");
} else if (this.#active !== name) { } else if (this.#active !== dialog) {
throw new Error("Another overlay is currently active."); throw new Error("Another overlay is currently active.");
} }
this.#overlays[this.#active].container.classList.add("fn__hidden"); dialog.close();
this.#overlays[this.#active].element.classList.add("fn__hidden");
this.#active = null; this.#active = null;
window.removeEventListener("keydown", this.#keyDownBound);
this.#keyDownBound = null;
}
#keyDown(evt) {
if (this.#active && evt.keyCode === /* Esc = */ 27) {
this.#closeThroughCaller();
evt.preventDefault();
}
}
#closeThroughCaller() {
if (this.#overlays[this.#active].callerCloseMethod) {
this.#overlays[this.#active].callerCloseMethod();
}
if (this.#active) {
this.close(this.#active);
}
} }
} }

View file

@ -13,12 +13,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { PasswordResponses } from "./pdfjs"; import { createPromiseCapability, PasswordResponses } from "./pdfjs";
/** /**
* @typedef {Object} PasswordPromptOptions * @typedef {Object} PasswordPromptOptions
* @property {string} overlayName - Name of the overlay for the overlay manager. * @property {HTMLDialogElement} dialog - The overlay's DOM element.
* @property {HTMLDivElement} container - Div container for the overlay.
* @property {HTMLParagraphElement} label - Label containing instructions for * @property {HTMLParagraphElement} label - Label containing instructions for
* entering the password. * entering the password.
* @property {HTMLInputElement} input - Input field for entering the password. * @property {HTMLInputElement} input - Input field for entering the password.
@ -29,6 +28,12 @@ import { PasswordResponses } from "./pdfjs";
*/ */
class PasswordPrompt { class PasswordPrompt {
#activeCapability = null;
#updateCallback = null;
#reason = null;
/** /**
* @param {PasswordPromptOptions} options * @param {PasswordPromptOptions} options
* @param {OverlayManager} overlayManager - Manager for the viewer overlays. * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
@ -37,8 +42,7 @@ class PasswordPrompt {
* an <iframe> or an <object>. The default value is `false`. * an <iframe> or an <object>. The default value is `false`.
*/ */
constructor(options, overlayManager, l10n, isViewerEmbedded = false) { constructor(options, overlayManager, l10n, isViewerEmbedded = false) {
this.overlayName = options.overlayName; this.dialog = options.dialog;
this.container = options.container;
this.label = options.label; this.label = options.label;
this.input = options.input; this.input = options.input;
this.submitButton = options.submitButton; this.submitButton = options.submitButton;
@ -47,61 +51,80 @@ class PasswordPrompt {
this.l10n = l10n; this.l10n = l10n;
this._isViewerEmbedded = isViewerEmbedded; this._isViewerEmbedded = isViewerEmbedded;
this.updateCallback = null;
this.reason = null;
// Attach the event listeners. // Attach the event listeners.
this.submitButton.addEventListener("click", this.#verify.bind(this)); this.submitButton.addEventListener("click", this.#verify.bind(this));
this.cancelButton.addEventListener("click", this.#cancel.bind(this)); this.cancelButton.addEventListener("click", this.close.bind(this));
this.input.addEventListener("keydown", e => { this.input.addEventListener("keydown", e => {
if (e.keyCode === /* Enter = */ 13) { if (e.keyCode === /* Enter = */ 13) {
this.#verify(); this.#verify();
} }
}); });
this.overlayManager.register( this.overlayManager.register(this.dialog, /* canForceClose = */ true);
this.overlayName,
this.container, this.dialog.addEventListener("close", this.#cancel.bind(this));
this.#cancel.bind(this),
true
);
} }
async open() { async open() {
await this.overlayManager.open(this.overlayName); if (this.#activeCapability) {
await this.#activeCapability.promise;
}
this.#activeCapability = createPromiseCapability();
try {
await this.overlayManager.open(this.dialog);
} catch (ex) {
this.#activeCapability = null;
throw ex;
}
const passwordIncorrect = const passwordIncorrect =
this.reason === PasswordResponses.INCORRECT_PASSWORD; this.#reason === PasswordResponses.INCORRECT_PASSWORD;
if (!this._isViewerEmbedded || passwordIncorrect) { if (!this._isViewerEmbedded || passwordIncorrect) {
this.input.focus(); this.input.focus();
} }
// NOTE
this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect
? 'invalid' ? 'invalid'
: 'label'}`] : 'label'}`]
} }
async close() { async close() {
await this.overlayManager.close(this.overlayName); if (this.overlayManager.active === this.dialog) {
this.input.value = ""; this.overlayManager.close(this.dialog);
}
} }
#verify() { #verify() {
const password = this.input.value; const password = this.input.value;
if (password?.length > 0) { if (password?.length > 0) {
this.close(); this.#invokeCallback(password);
this.updateCallback(password);
} }
} }
#cancel() { #cancel() {
this.close(); this.#invokeCallback(new Error("PasswordPrompt cancelled."));
this.updateCallback(new Error("PasswordPrompt cancelled.")); this.#activeCapability.resolve();
} }
setUpdateCallback(updateCallback, reason) { #invokeCallback(password) {
this.updateCallback = updateCallback; if (!this.#updateCallback) {
this.reason = reason; return; // Ensure that the callback is only invoked once.
}
this.close();
this.input.value = "";
this.#updateCallback(password);
this.#updateCallback = null;
}
async setUpdateCallback(updateCallback, reason) {
if (this.#activeCapability) {
await this.#activeCapability.promise;
}
this.#updateCallback = updateCallback;
this.#reason = reason;
} }
} }

View file

@ -15,6 +15,7 @@
import { createPromiseCapability, getFilenameFromUrl } from "./pdfjs"; import { createPromiseCapability, getFilenameFromUrl } from "./pdfjs";
import { BaseTreeViewer } from "./base_tree_viewer.js"; import { BaseTreeViewer } from "./base_tree_viewer.js";
import { waitOnEventOrTimeout } from "./event_utils.js";
/** /**
* @typedef {Object} PDFAttachmentViewerOptions * @typedef {Object} PDFAttachmentViewerOptions
@ -38,7 +39,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
this.eventBus._on( this.eventBus._on(
"fileattachmentannotation", "fileattachmentannotation",
this._appendAttachment.bind(this) this.#appendAttachment.bind(this)
); );
} }
@ -51,36 +52,33 @@ class PDFAttachmentViewer extends BaseTreeViewer {
// replaced is when appending FileAttachment annotations. // replaced is when appending FileAttachment annotations.
this._renderedCapability = createPromiseCapability(); this._renderedCapability = createPromiseCapability();
} }
if (this._pendingDispatchEvent) { this._pendingDispatchEvent = false;
clearTimeout(this._pendingDispatchEvent);
}
this._pendingDispatchEvent = null;
} }
/** /**
* @private * @private
*/ */
_dispatchEvent(attachmentsCount) { async _dispatchEvent(attachmentsCount) {
this._renderedCapability.resolve(); this._renderedCapability.resolve();
if (this._pendingDispatchEvent) { if (attachmentsCount === 0 && !this._pendingDispatchEvent) {
clearTimeout(this._pendingDispatchEvent);
this._pendingDispatchEvent = null;
}
if (attachmentsCount === 0) {
// Delay the event when no "regular" attachments exist, to allow time for // Delay the event when no "regular" attachments exist, to allow time for
// parsing of any FileAttachment annotations that may be present on the // parsing of any FileAttachment annotations that may be present on the
// *initially* rendered page; this reduces the likelihood of temporarily // *initially* rendered page; this reduces the likelihood of temporarily
// disabling the attachmentsView when the `PDFSidebar` handles the event. // disabling the attachmentsView when the `PDFSidebar` handles the event.
this._pendingDispatchEvent = setTimeout(() => { this._pendingDispatchEvent = true;
this.eventBus.dispatch("attachmentsloaded", {
source: this, await waitOnEventOrTimeout({
attachmentsCount: 0, target: this.eventBus,
name: "annotationlayerrendered",
delay: 1000,
}); });
this._pendingDispatchEvent = null;
}); if (!this._pendingDispatchEvent) {
return; return; // There was already another `_dispatchEvent`-call`.
} }
}
this._pendingDispatchEvent = false;
this.eventBus.dispatch("attachmentsloaded", { this.eventBus.dispatch("attachmentsloaded", {
source: this, source: this,
@ -129,9 +127,9 @@ class PDFAttachmentViewer extends BaseTreeViewer {
this._bindLink(element, { content, filename }); this._bindLink(element, { content, filename });
element.textContent = this._normalizeTextContent(filename); element.textContent = this._normalizeTextContent(filename);
div.appendChild(element); div.append(element);
fragment.appendChild(div); fragment.append(div);
attachmentsCount++; attachmentsCount++;
} }
@ -140,27 +138,22 @@ class PDFAttachmentViewer extends BaseTreeViewer {
/** /**
* Used to append FileAttachment annotations to the sidebar. * Used to append FileAttachment annotations to the sidebar.
* @private
*/ */
_appendAttachment({ id, filename, content }) { #appendAttachment({ filename, content }) {
const renderedPromise = this._renderedCapability.promise; const renderedPromise = this._renderedCapability.promise;
renderedPromise.then(() => { renderedPromise.then(() => {
if (renderedPromise !== this._renderedCapability.promise) { if (renderedPromise !== this._renderedCapability.promise) {
return; // The FileAttachment annotation belongs to a previous document. return; // The FileAttachment annotation belongs to a previous document.
} }
let attachments = this._attachments; const attachments = this._attachments || Object.create(null);
if (!attachments) {
attachments = Object.create(null);
} else {
for (const name in attachments) { for (const name in attachments) {
if (id === name) { if (filename === name) {
return; // Ignore the new attachment if it already exists. return; // Ignore the new attachment if it already exists.
} }
} }
} attachments[filename] = {
attachments[id] = {
filename, filename,
content, content,
}; };

View file

@ -13,6 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { AnnotationEditorType } from "./pdfjs";
import { GrabToPan } from "./grab_to_pan.js"; import { GrabToPan } from "./grab_to_pan.js";
import { PresentationModeState } from "./ui_utils.js"; import { PresentationModeState } from "./ui_utils.js";
@ -40,7 +41,7 @@ class PDFCursorTools {
this.eventBus = eventBus; this.eventBus = eventBus;
this.active = CursorTool.SELECT; this.active = CursorTool.SELECT;
this.activeBeforePresentationMode = null; this.previouslyActive = null;
this.handTool = new GrabToPan({ this.handTool = new GrabToPan({
element: this.container, element: this.container,
@ -63,13 +64,13 @@ class PDFCursorTools {
} }
/** /**
* NOTE: This method is ignored while Presentation Mode is active.
* @param {number} tool - The cursor mode that should be switched to, * @param {number} tool - The cursor mode that should be switched to,
* must be one of the values in {CursorTool}. * must be one of the values in {CursorTool}.
*/ */
switchTool(tool) { switchTool(tool) {
if (this.activeBeforePresentationMode !== null) { if (this.previouslyActive !== null) {
return; // Cursor tools cannot be used in Presentation Mode. // Cursor tools cannot be used in PresentationMode/AnnotationEditor.
return;
} }
if (tool === this.active) { if (tool === this.active) {
return; // The requested tool is already active. return; // The requested tool is already active.
@ -121,22 +122,54 @@ class PDFCursorTools {
this.switchTool(evt.tool); this.switchTool(evt.tool);
}); });
this.eventBus._on("presentationmodechanged", evt => { let annotationEditorMode = AnnotationEditorType.NONE,
switch (evt.state) { presentationModeState = PresentationModeState.NORMAL;
case PresentationModeState.FULLSCREEN: {
const disableActive = () => {
const previouslyActive = this.active; const previouslyActive = this.active;
this.switchTool(CursorTool.SELECT); this.switchTool(CursorTool.SELECT);
this.activeBeforePresentationMode = previouslyActive; this.previouslyActive ??= previouslyActive; // Keep track of the first one.
break; };
} const enableActive = () => {
case PresentationModeState.NORMAL: { const previouslyActive = this.previouslyActive;
const previouslyActive = this.activeBeforePresentationMode;
this.activeBeforePresentationMode = null; if (
previouslyActive !== null &&
annotationEditorMode === AnnotationEditorType.NONE &&
presentationModeState === PresentationModeState.NORMAL
) {
this.previouslyActive = null;
this.switchTool(previouslyActive); this.switchTool(previouslyActive);
break;
} }
};
this.eventBus._on("secondarytoolbarreset", evt => {
if (this.previouslyActive !== null) {
annotationEditorMode = AnnotationEditorType.NONE;
presentationModeState = PresentationModeState.NORMAL;
enableActive();
}
});
this.eventBus._on("annotationeditormodechanged", ({ mode }) => {
annotationEditorMode = mode;
if (mode === AnnotationEditorType.NONE) {
enableActive();
} else {
disableActive();
}
});
this.eventBus._on("presentationmodechanged", ({ state }) => {
presentationModeState = state;
if (state === PresentationModeState.NORMAL) {
enableActive();
} else if (state === PresentationModeState.FULLSCREEN) {
disableActive();
} }
}); });
} }

View file

@ -13,11 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { createPromiseCapability, PDFDateString } from "./pdfjs";
createPromiseCapability,
getPdfFilenameFromUrl,
PDFDateString,
} from "./pdfjs";
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js"; import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
const DEFAULT_FIELD_CONTENT = "-"; const DEFAULT_FIELD_CONTENT = "-";
@ -45,9 +41,8 @@ function getPageName(size, isPortrait, pageNames) {
/** /**
* @typedef {Object} PDFDocumentPropertiesOptions * @typedef {Object} PDFDocumentPropertiesOptions
* @property {string} overlayName - Name/identifier for the overlay. * @property {HTMLDialogElement} dialog - The overlay's DOM element.
* @property {Object} fields - Names and elements of the overlay's fields. * @property {Object} fields - Names and elements of the overlay's fields.
* @property {HTMLDivElement} container - Div container for the overlay.
* @property {HTMLButtonElement} closeButton - Button for closing the overlay. * @property {HTMLButtonElement} closeButton - Button for closing the overlay.
*/ */
@ -59,28 +54,27 @@ class PDFDocumentProperties {
* @param {OverlayManager} overlayManager - Manager for the viewer overlays. * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
* @param {EventBus} eventBus - The application event bus. * @param {EventBus} eventBus - The application event bus.
* @param {IL10n} l10n - Localization service. * @param {IL10n} l10n - Localization service.
* @param {function} fileNameLookup - The function that is used to lookup
* the document fileName.
*/ */
constructor( constructor(
{ overlayName, fields, container, closeButton }, { dialog, fields, closeButton },
overlayManager, overlayManager,
eventBus, eventBus,
l10n l10n,
fileNameLookup
) { ) {
this.overlayName = overlayName; this.dialog = dialog;
this.fields = fields; this.fields = fields;
this.container = container;
this.overlayManager = overlayManager; this.overlayManager = overlayManager;
this.l10n = l10n; this.l10n = l10n;
this._fileNameLookup = fileNameLookup;
this.#reset(); this.#reset();
// Bind the event listener for the Close button. // Bind the event listener for the Close button.
closeButton.addEventListener("click", this.close.bind(this)); closeButton.addEventListener("click", this.close.bind(this));
this.overlayManager.register( this.overlayManager.register(this.dialog);
this.overlayName,
this.container,
this.close.bind(this)
);
eventBus._on("pagechanging", evt => { eventBus._on("pagechanging", evt => {
this._currentPageNumber = evt.pageNumber; this._currentPageNumber = evt.pageNumber;
@ -90,6 +84,10 @@ class PDFDocumentProperties {
}); });
this._isNonMetricLocale = true; // The default viewer locale is 'en-us'. this._isNonMetricLocale = true; // The default viewer locale is 'en-us'.
// NOTE
// l10n.getLanguage().then(locale => {
// this._isNonMetricLocale = NON_METRIC_LOCALES.includes(locale);
// });
} }
/** /**
@ -97,7 +95,7 @@ class PDFDocumentProperties {
*/ */
async open() { async open() {
await Promise.all([ await Promise.all([
this.overlayManager.open(this.overlayName), this.overlayManager.open(this.dialog),
this._dataAvailableCapability.promise, this._dataAvailableCapability.promise,
]); ]);
const currentPageNumber = this._currentPageNumber; const currentPageNumber = this._currentPageNumber;
@ -118,7 +116,7 @@ class PDFDocumentProperties {
const { const {
info, info,
/* metadata, */ /* metadata, */
contentDispositionFilename, /* contentDispositionFilename, */
contentLength, contentLength,
} = await this.pdfDocument.getMetadata(); } = await this.pdfDocument.getMetadata();
@ -130,7 +128,7 @@ class PDFDocumentProperties {
pageSize, pageSize,
isLinearized, isLinearized,
] = await Promise.all([ ] = await Promise.all([
contentDispositionFilename || getPdfFilenameFromUrl(this.url), this._fileNameLookup(),
this.#parseFileSize(contentLength), this.#parseFileSize(contentLength),
this.#parseDate(info.CreationDate), this.#parseDate(info.CreationDate),
this.#parseDate(info.ModDate), this.#parseDate(info.ModDate),
@ -176,20 +174,18 @@ class PDFDocumentProperties {
/** /**
* Close the document properties overlay. * Close the document properties overlay.
*/ */
close() { async close() {
this.overlayManager.close(this.overlayName); this.overlayManager.close(this.dialog);
} }
/** /**
* Set a reference to the PDF document and the URL in order * Set a reference to the PDF document in order to populate the dialog fields
* to populate the overlay fields with the document properties. * with the document properties. Note that the dialog will contain no
* Note that the overlay will contain no information if this method * information if this method is not called.
* is not called.
* *
* @param {PDFDocumentProxy} pdfDocument - A reference to the PDF document. * @param {PDFDocumentProxy} pdfDocument - A reference to the PDF document.
* @param {string} url - The URL of the document.
*/ */
setDocument(pdfDocument, url = null) { setDocument(pdfDocument) {
if (this.pdfDocument) { if (this.pdfDocument) {
this.#reset(); this.#reset();
this.#updateUI(true); this.#updateUI(true);
@ -198,14 +194,12 @@ class PDFDocumentProperties {
return; return;
} }
this.pdfDocument = pdfDocument; this.pdfDocument = pdfDocument;
this.url = url;
this._dataAvailableCapability.resolve(); this._dataAvailableCapability.resolve();
} }
#reset() { #reset() {
this.pdfDocument = null; this.pdfDocument = null;
this.url = null;
this.#fieldData = null; this.#fieldData = null;
this._dataAvailableCapability = createPromiseCapability(); this._dataAvailableCapability = createPromiseCapability();
@ -225,7 +219,7 @@ class PDFDocumentProperties {
} }
return; return;
} }
if (this.overlayManager.active !== this.overlayName) { if (this.overlayManager.active !== this.dialog) {
// Don't bother updating the dialog if has already been closed, // Don't bother updating the dialog if has already been closed,
// since it will be updated the next time `this.open` is called. // since it will be updated the next time `this.open` is called.
return; return;
@ -243,6 +237,7 @@ class PDFDocumentProperties {
if (!kb) { if (!kb) {
return undefined; return undefined;
} }
// NOTE
if (mb >= 1) { if (mb >= 1) {
return `${mb >= 1 && (+mb.toPrecision( return `${mb >= 1 && (+mb.toPrecision(
3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes)` 3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes)`
@ -315,6 +310,7 @@ class PDFDocumentProperties {
} }
} }
// NOTE
const [{ width, height }, unit, name, orientation] = await Promise.all([ const [{ width, height }, unit, name, orientation] = await Promise.all([
this._isNonMetricLocale ? sizeInches : sizeMillimeters, this._isNonMetricLocale ? sizeInches : sizeMillimeters,
this._isNonMetricLocale this._isNonMetricLocale
@ -337,10 +333,12 @@ class PDFDocumentProperties {
if (!dateObject) { if (!dateObject) {
return undefined; return undefined;
} }
// NOTE
return `${dateObject.toLocaleDateString()}, ${dateObject.toLocaleTimeString()}` return `${dateObject.toLocaleDateString()}, ${dateObject.toLocaleTimeString()}`
} }
#parseLinearization(isLinearized) { #parseLinearization(isLinearized) {
// NOTE
return isLinearized ? 'Yes' : 'No' return isLinearized ? 'Yes' : 'No'
} }
} }

View file

@ -13,9 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { FindState } from './pdf_find_controller.js' import { FindState } from "./pdf_find_controller.js";
const MATCHES_COUNT_LIMIT = 1000 const MATCHES_COUNT_LIMIT = 1000;
/** /**
* Creates a "search bar" given a set of DOM elements that act as controls * Creates a "search bar" given a set of DOM elements that act as controls
@ -25,98 +25,101 @@ const MATCHES_COUNT_LIMIT = 1000
*/ */
class PDFFindBar { class PDFFindBar {
constructor(options, eventBus, l10n) { constructor(options, eventBus, l10n) {
this.opened = false this.opened = false;
this.bar = options.bar this.bar = options.bar;
this.toggleButton = options.toggleButton this.toggleButton = options.toggleButton;
this.findField = options.findField this.findField = options.findField;
this.highlightAll = options.highlightAllCheckbox this.highlightAll = options.highlightAllCheckbox;
this.caseSensitive = options.caseSensitiveCheckbox this.caseSensitive = options.caseSensitiveCheckbox;
this.matchDiacritics = options.matchDiacriticsCheckbox this.matchDiacritics = options.matchDiacriticsCheckbox;
this.entireWord = options.entireWordCheckbox this.entireWord = options.entireWordCheckbox;
this.findMsg = options.findMsg this.findMsg = options.findMsg;
this.findResultsCount = options.findResultsCount this.findResultsCount = options.findResultsCount;
this.findPreviousButton = options.findPreviousButton this.findPreviousButton = options.findPreviousButton;
this.findNextButton = options.findNextButton this.findNextButton = options.findNextButton;
this.eventBus = eventBus this.eventBus = eventBus;
this.l10n = l10n this.l10n = l10n;
// Add event listeners to the DOM elements. // Add event listeners to the DOM elements.
this.toggleButton.addEventListener('click', () => { this.toggleButton.addEventListener("click", () => {
this.toggle() this.toggle();
}) });
this.findField.addEventListener('input', () => { this.findField.addEventListener("input", () => {
this.dispatchEvent('') this.dispatchEvent("");
}) });
this.bar.addEventListener('keydown', e => { this.bar.addEventListener("keydown", e => {
switch (e.keyCode) { switch (e.keyCode) {
case 13: // Enter case 13: // Enter
if (e.target === this.findField) { if (e.target === this.findField) {
this.dispatchEvent('again', e.shiftKey) this.dispatchEvent("again", e.shiftKey);
} }
break break;
case 27: // Escape case 27: // Escape
this.close() this.close();
break break;
} }
}) });
this.findPreviousButton.addEventListener('click', () => { this.findPreviousButton.addEventListener("click", () => {
this.dispatchEvent('again', true) this.dispatchEvent("again", true);
}) });
this.findNextButton.addEventListener('click', () => { this.findNextButton.addEventListener("click", () => {
this.dispatchEvent('again', false) this.dispatchEvent("again", false);
}) });
this.highlightAll.addEventListener('click', () => { this.highlightAll.addEventListener("click", () => {
this.dispatchEvent('highlightallchange') this.dispatchEvent("highlightallchange");
// NOTE: 以下三个相同 https://github.com/siyuan-note/siyuan/issues/5338 // NOTE
if (this.highlightAll.checked) { if (this.highlightAll.checked) {
this.highlightAll.parentElement.classList.remove("b3-button--outline") this.highlightAll.parentElement.classList.remove("b3-button--outline")
} else { } else {
this.highlightAll.parentElement.classList.add("b3-button--outline") this.highlightAll.parentElement.classList.add("b3-button--outline")
} }
}) });
this.caseSensitive.addEventListener('click', () => { this.caseSensitive.addEventListener("click", () => {
this.dispatchEvent('casesensitivitychange') this.dispatchEvent("casesensitivitychange");
// NOTE
if (this.caseSensitive.checked) { if (this.caseSensitive.checked) {
this.caseSensitive.parentElement.classList.remove("b3-button--outline") this.caseSensitive.parentElement.classList.remove("b3-button--outline")
} else { } else {
this.caseSensitive.parentElement.classList.add("b3-button--outline") this.caseSensitive.parentElement.classList.add("b3-button--outline")
} }
}) });
this.entireWord.addEventListener('click', () => { this.entireWord.addEventListener("click", () => {
this.dispatchEvent('entirewordchange') this.dispatchEvent("entirewordchange");
// NOTE
if (this.entireWord.checked) { if (this.entireWord.checked) {
this.entireWord.parentElement.classList.remove("b3-button--outline") this.entireWord.parentElement.classList.remove("b3-button--outline")
} else { } else {
this.entireWord.parentElement.classList.add("b3-button--outline") this.entireWord.parentElement.classList.add("b3-button--outline")
} }
}) });
this.matchDiacritics.addEventListener('click', () => { this.matchDiacritics.addEventListener("click", () => {
this.dispatchEvent('diacriticmatchingchange') this.dispatchEvent("diacriticmatchingchange");
// NOTE
if (this.matchDiacritics.checked) { if (this.matchDiacritics.checked) {
this.matchDiacritics.parentElement.classList.remove("b3-button--outline") this.matchDiacritics.parentElement.classList.remove("b3-button--outline")
} else { } else {
this.matchDiacritics.parentElement.classList.add("b3-button--outline") this.matchDiacritics.parentElement.classList.add("b3-button--outline")
} }
}) });
this.eventBus._on('resize', this.#adjustWidth.bind(this)) this.eventBus._on("resize", this.#adjustWidth.bind(this));
} }
reset() { reset() {
this.updateUIState() this.updateUIState();
} }
dispatchEvent(type, findPrev = false) { dispatchEvent(type, findPrev = false) {
this.eventBus.dispatch('find', { this.eventBus.dispatch("find", {
source: this, source: this,
type, type,
query: this.findField.value, query: this.findField.value,
@ -126,108 +129,115 @@ class PDFFindBar {
highlightAll: this.highlightAll.checked, highlightAll: this.highlightAll.checked,
findPrevious: findPrev, findPrevious: findPrev,
matchDiacritics: this.matchDiacritics.checked, matchDiacritics: this.matchDiacritics.checked,
}) });
} }
updateUIState(state, previous, matchesCount) { updateUIState(state, previous, matchesCount) {
let findMsg = '' // NOTE
let status = '' let findMsg = "";
let status = "";
switch (state) { switch (state) {
case FindState.FOUND: case FindState.FOUND:
break break;
case FindState.PENDING: case FindState.PENDING:
status = 'pending' status = "pending";
break break;
case FindState.NOT_FOUND: case FindState.NOT_FOUND:
// NOTE
findMsg = window.siyuan.languages.find_not_found findMsg = window.siyuan.languages.find_not_found
status = 'notFound' status = "notFound";
break break;
case FindState.WRAPPED: case FindState.WRAPPED:
findMsg = window.siyuan.languages.find_not_found[`find_reached_${previous findMsg = window.siyuan.languages.find_not_found[`find_reached_${previous
? 'top' ? 'top'
: 'bottom'}`] : 'bottom'}`]
break break;
} }
this.findField.setAttribute('data-status', status) this.findField.setAttribute("data-status", status);
this.findField.setAttribute("aria-invalid", state === FindState.NOT_FOUND);
// NOTE
this.findMsg.textContent = findMsg this.findMsg.textContent = findMsg
this.#adjustWidth() this.#adjustWidth()
this.updateResultsCount(matchesCount) this.updateResultsCount(matchesCount);
} }
updateResultsCount({ current = 0, total = 0 } = {}) { updateResultsCount({ current = 0, total = 0 } = {}) {
const limit = MATCHES_COUNT_LIMIT const limit = MATCHES_COUNT_LIMIT;
let msg = '' // // NOTE
let matchCountMsg = "";
if (total > 0) { if (total > 0) {
if (total > limit) { if (total > limit) {
msg = window.siyuan.languages.find_match_count_limit.replace( matchCountMsg = window.siyuan.languages.find_match_count_limit.replace(
'{{limit}}', limit) '{{limit}}', limit)
} else { } else {
msg = window.siyuan.languages.find_match_count.replace('{{current}}', matchCountMsg = window.siyuan.languages.find_match_count.replace('{{current}}',
current).replace('{{total}}', total) current).replace('{{total}}', total)
} }
} }
this.findResultsCount.textContent = msg
this.findResultsCount.classList.toggle('fn__hidden', !total) this.findResultsCount.textContent = matchCountMsg
this.#adjustWidth() this.#adjustWidth()
} }
open() { open() {
if (!this.opened) { if (!this.opened) {
this.opened = true this.opened = true;
this.toggleButton.classList.add('toggled') this.toggleButton.classList.add("toggled");
this.toggleButton.setAttribute('aria-expanded', 'true') this.toggleButton.setAttribute("aria-expanded", "true");
this.bar.classList.remove('fn__hidden') // NOTE
this.bar.classList.remove("fn__hidden");
} }
this.findField.select() this.findField.select();
this.findField.focus() this.findField.focus();
this.#adjustWidth() this.#adjustWidth();
} }
close() { close() {
if (!this.opened) { if (!this.opened) {
return return;
} }
this.opened = false this.opened = false;
this.toggleButton.classList.remove('toggled') this.toggleButton.classList.remove("toggled");
this.toggleButton.setAttribute('aria-expanded', 'false') this.toggleButton.setAttribute("aria-expanded", "false");
this.bar.classList.add('fn__hidden') // NOTE
this.bar.classList.add("fn__hidden");
this.eventBus.dispatch('findbarclose', {source: this}) this.eventBus.dispatch("findbarclose", { source: this });
} }
toggle() { toggle() {
if (this.opened) { if (this.opened) {
this.close() this.close();
} else { } else {
this.open() this.open();
} }
} }
#adjustWidth() { #adjustWidth() {
if (!this.opened) { if (!this.opened) {
return return;
} }
// The find bar has an absolute position and thus the browser extends // 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 // its width to the maximum possible width once the find bar does not fit
// entirely within the window anymore (and its elements are automatically // entirely within the window anymore (and its elements are automatically
// wrapped). Here we detect and fix that. // wrapped). Here we detect and fix that.
this.bar.classList.remove('wrapContainers') this.bar.classList.remove("wrapContainers");
const findbarHeight = this.bar.clientHeight const findbarHeight = this.bar.clientHeight;
const inputContainerHeight = this.bar.firstElementChild.clientHeight const inputContainerHeight = this.bar.firstElementChild.clientHeight;
if (findbarHeight > inputContainerHeight) { if (findbarHeight > inputContainerHeight) {
// The findbar is taller than the input container, which means that // The findbar is taller than the input container, which means that
// the browser wrapped some of the elements. For a consistent look, // the browser wrapped some of the elements. For a consistent look,
// wrap all of them to adjust the width of the find bar. // wrap all of them to adjust the width of the find bar.
this.bar.classList.add('wrapContainers') this.bar.classList.add("wrapContainers");
} }
} }
} }
export { PDFFindBar } export { PDFFindBar };

View file

@ -13,6 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js"; import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
import { createPromiseCapability } from "./pdfjs"; import { createPromiseCapability } from "./pdfjs";
import { getCharacterType } from "./pdf_find_utils.js"; import { getCharacterType } from "./pdf_find_utils.js";
@ -82,19 +86,62 @@ const SPECIAL_CHARS_REG_EXP =
const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u; const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u;
const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u; const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u;
let normalizationRegex = null; // The range [AC00-D7AF] corresponds to the Hangul syllables.
// The few other chars are some CJK Compatibility Ideographs.
const SYLLABLES_REG_EXP = /[\uAC00-\uD7AF\uFA6C\uFACF-\uFAD1\uFAD5-\uFAD7]+/g;
const SYLLABLES_LENGTHS = new Map();
// When decomposed (in using NFD) the above syllables will start
// with one of the chars in this regexp.
const FIRST_CHAR_SYLLABLES_REG_EXP =
"[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]";
let noSyllablesRegExp = null;
let withSyllablesRegExp = null;
function normalize(text) { function normalize(text) {
// The diacritics in the text or in the query can be composed or not. // The diacritics in the text or in the query can be composed or not.
// So we use a decomposed text using NFD (and the same for the query) // So we use a decomposed text using NFD (and the same for the query)
// in order to be sure that diacritics are in the same order. // in order to be sure that diacritics are in the same order.
if (!normalizationRegex) { // Collect syllables length and positions.
const syllablePositions = [];
let m;
while ((m = SYLLABLES_REG_EXP.exec(text)) !== null) {
let { index } = m;
for (const char of m[0]) {
let len = SYLLABLES_LENGTHS.get(char);
if (!len) {
len = char.normalize("NFD").length;
SYLLABLES_LENGTHS.set(char, len);
}
syllablePositions.push([len, index++]);
}
}
let normalizationRegex;
if (syllablePositions.length === 0 && noSyllablesRegExp) {
normalizationRegex = noSyllablesRegExp;
} else if (syllablePositions.length > 0 && withSyllablesRegExp) {
normalizationRegex = withSyllablesRegExp;
} else {
// Compile the regular expression for text normalization once. // Compile the regular expression for text normalization once.
const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join("");
normalizationRegex = new RegExp( const regexp = `([${replace}])|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(\\p{Ideographic}\\n)|(\\n)`;
`([${replace}])|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(\\n)`,
if (syllablePositions.length === 0) {
// Most of the syllables belong to Hangul so there are no need
// to search for them in a non-Hangul document.
// We use the \0 in order to have the same number of groups.
normalizationRegex = noSyllablesRegExp = new RegExp(
regexp + "|(\\u0000)",
"gum" "gum"
); );
} else {
normalizationRegex = withSyllablesRegExp = new RegExp(
regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`,
"gum"
);
}
} }
// The goal of this function is to normalize the string and // The goal of this function is to normalize the string and
@ -126,14 +173,14 @@ function normalize(text) {
// Collect diacritics length and positions. // Collect diacritics length and positions.
const rawDiacriticsPositions = []; const rawDiacriticsPositions = [];
let m;
while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) { while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) {
rawDiacriticsPositions.push([m[0].length, m.index]); rawDiacriticsPositions.push([m[0].length, m.index]);
} }
let normalized = text.normalize("NFD"); let normalized = text.normalize("NFD");
const positions = [[0, 0]]; const positions = [[0, 0]];
let k = 0; let rawDiacriticsIndex = 0;
let syllableIndex = 0;
let shift = 0; let shift = 0;
let shiftOrigin = 0; let shiftOrigin = 0;
let eol = 0; let eol = 0;
@ -141,7 +188,7 @@ function normalize(text) {
normalized = normalized.replace( normalized = normalized.replace(
normalizationRegex, normalizationRegex,
(match, p1, p2, p3, p4, i) => { (match, p1, p2, p3, p4, p5, p6, i) => {
i -= shiftOrigin; i -= shiftOrigin;
if (p1) { if (p1) {
// Maybe fractions or quotations mark... // Maybe fractions or quotations mark...
@ -161,12 +208,12 @@ function normalize(text) {
// Diacritics. // Diacritics.
hasDiacritics = true; hasDiacritics = true;
let jj = len; let jj = len;
if (i + eol === rawDiacriticsPositions[k]?.[1]) { if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) {
jj -= rawDiacriticsPositions[k][0]; jj -= rawDiacriticsPositions[rawDiacriticsIndex][0];
++k; ++rawDiacriticsIndex;
} }
for (let j = 1; j < jj + 1; j++) { for (let j = 1; j <= jj; j++) {
// i is the position of the first diacritic // i is the position of the first diacritic
// so (i - 1) is the position for the letter before. // so (i - 1) is the position for the letter before.
positions.push([i - 1 - shift + j, shift - j]); positions.push([i - 1 - shift + j, shift - j]);
@ -200,7 +247,16 @@ function normalize(text) {
return p3.charAt(0); return p3.charAt(0);
} }
// p4 if (p4) {
// An ideographic at the end of a line doesn't imply adding an extra
// white space.
positions.push([i - shift + 1, shift]);
shiftOrigin += 1;
eol += 1;
return p4.charAt(0);
}
if (p5) {
// eol is replaced by space: "foo\nbar" is likely equivalent to // eol is replaced by space: "foo\nbar" is likely equivalent to
// "foo bar". // "foo bar".
positions.push([i - shift + 1, shift - 1]); positions.push([i - shift + 1, shift - 1]);
@ -209,6 +265,21 @@ function normalize(text) {
eol += 1; eol += 1;
return " "; return " ";
} }
// p6
if (i + eol === syllablePositions[syllableIndex]?.[1]) {
// A syllable (1 char) is replaced with several chars (n) so
// newCharsLen = n - 1.
const newCharLen = syllablePositions[syllableIndex][0] - 1;
++syllableIndex;
for (let j = 1; j <= newCharLen; j++) {
positions.push([i - (shift - j), shift - j]);
}
shift -= newCharLen;
shiftOrigin += newCharLen;
}
return p6;
}
); );
positions.push([normalized.length, shift]); positions.push([normalized.length, shift]);

View file

@ -13,11 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { /** @typedef {import("./event_utils").EventBus} EventBus */
isValidRotation, /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
parseQueryString,
PresentationModeState, import { isValidRotation, parseQueryString } from "./ui_utils.js";
} from "./ui_utils.js";
import { waitOnEventOrTimeout } from "./event_utils.js"; import { waitOnEventOrTimeout } from "./event_utils.js";
// Heuristic value used when force-resetting `this._blockHashChange`. // Heuristic value used when force-resetting `this._blockHashChange`.
@ -66,13 +65,8 @@ class PDFHistory {
this.reset(); this.reset();
this._boundEvents = null; this._boundEvents = null;
this._isViewerInPresentationMode = false; // Ensure that we don't miss a "pagesinit" event,
// Ensure that we don't miss either a 'presentationmodechanged' or a // by registering the listener immediately.
// 'pagesinit' event, by registering the listeners immediately.
this.eventBus._on("presentationmodechanged", evt => {
this._isViewerInPresentationMode =
evt.state !== PresentationModeState.NORMAL;
});
this.eventBus._on("pagesinit", () => { this.eventBus._on("pagesinit", () => {
this._isPagesLoaded = false; this._isPagesLoaded = false;
@ -563,9 +557,7 @@ class PDFHistory {
} }
this._position = { this._position = {
hash: this._isViewerInPresentationMode hash: location.pdfOpenParams.substring(1),
? `page=${location.pageNumber}`
: location.pdfOpenParams.substring(1),
page: this.linkService.page, page: this.linkService.page,
first: location.pageNumber, first: location.pageNumber,
rotation: location.rotation, rotation: location.rotation,

View file

@ -34,13 +34,19 @@ class PDFLayerViewer extends BaseTreeViewer {
super(options); super(options);
this.l10n = options.l10n; this.l10n = options.l10n;
this.eventBus._on("resetlayers", this._resetLayers.bind(this)); this.eventBus._on("optionalcontentconfigchanged", evt => {
this.#updateLayers(evt.promise);
});
this.eventBus._on("resetlayers", () => {
this.#updateLayers();
});
this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this)); this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this));
} }
reset() { reset() {
super.reset(); super.reset();
this._optionalContentConfig = null; this._optionalContentConfig = null;
this._optionalContentHash = null;
} }
/** /**
@ -59,6 +65,7 @@ class PDFLayerViewer extends BaseTreeViewer {
_bindLink(element, { groupId, input }) { _bindLink(element, { groupId, input }) {
const setVisibility = () => { const setVisibility = () => {
this._optionalContentConfig.setVisibility(groupId, input.checked); this._optionalContentConfig.setVisibility(groupId, input.checked);
this._optionalContentHash = this._optionalContentConfig.getHash();
this.eventBus.dispatch("optionalcontentconfig", { this.eventBus.dispatch("optionalcontentconfig", {
source: this, source: this,
@ -87,8 +94,9 @@ class PDFLayerViewer extends BaseTreeViewer {
element.textContent = this._normalizeTextContent(name); element.textContent = this._normalizeTextContent(name);
return; return;
} }
// NOTE
element.textContent = window.siyuan.languages.additionalLayers element.textContent = window.siyuan.languages.additionalLayers
element.style.fontStyle = 'italic' element.style.fontStyle = "italic";
} }
/** /**
@ -123,6 +131,7 @@ class PDFLayerViewer extends BaseTreeViewer {
this._dispatchEvent(/* layersCount = */ 0); this._dispatchEvent(/* layersCount = */ 0);
return; return;
} }
this._optionalContentHash = optionalContentConfig.getHash();
const fragment = document.createDocumentFragment(), const fragment = document.createDocumentFragment(),
queue = [{ parent: fragment, groups }]; queue = [{ parent: fragment, groups }];
@ -135,7 +144,7 @@ class PDFLayerViewer extends BaseTreeViewer {
div.className = "treeItem"; div.className = "treeItem";
const element = document.createElement("a"); const element = document.createElement("a");
div.appendChild(element); div.append(element);
if (typeof groupId === "object") { if (typeof groupId === "object") {
hasAnyNesting = true; hasAnyNesting = true;
@ -144,7 +153,7 @@ class PDFLayerViewer extends BaseTreeViewer {
const itemsDiv = document.createElement("div"); const itemsDiv = document.createElement("div");
itemsDiv.className = "treeItems"; itemsDiv.className = "treeItems";
div.appendChild(itemsDiv); div.append(itemsDiv);
queue.push({ parent: itemsDiv, groups: groupId.order }); queue.push({ parent: itemsDiv, groups: groupId.order });
} else { } else {
@ -153,43 +162,46 @@ class PDFLayerViewer extends BaseTreeViewer {
const input = document.createElement("input"); const input = document.createElement("input");
this._bindLink(element, { groupId, input }); this._bindLink(element, { groupId, input });
input.type = "checkbox"; input.type = "checkbox";
input.id = groupId;
input.checked = group.visible; input.checked = group.visible;
const label = document.createElement("label"); const label = document.createElement("label");
label.setAttribute("for", groupId);
label.textContent = this._normalizeTextContent(group.name); label.textContent = this._normalizeTextContent(group.name);
element.appendChild(input); label.append(input);
element.appendChild(label); element.append(label);
layersCount++; layersCount++;
} }
levelData.parent.appendChild(div); levelData.parent.append(div);
} }
} }
this._finishRendering(fragment, layersCount, hasAnyNesting); this._finishRendering(fragment, layersCount, hasAnyNesting);
} }
/** async #updateLayers(promise = null) {
* @private
*/
async _resetLayers() {
if (!this._optionalContentConfig) { if (!this._optionalContentConfig) {
return; return;
} }
// Fetch the default optional content configuration... const pdfDocument = this._pdfDocument;
const optionalContentConfig = const optionalContentConfig = await (promise ||
await this._pdfDocument.getOptionalContentConfig(); pdfDocument.getOptionalContentConfig());
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.
}
} else {
this.eventBus.dispatch("optionalcontentconfig", { this.eventBus.dispatch("optionalcontentconfig", {
source: this, source: this,
promise: Promise.resolve(optionalContentConfig), promise: Promise.resolve(optionalContentConfig),
}); });
}
// ... and reset the sidebarView to the default state. // Reset the sidebarView to the new state.
this.render({ this.render({
optionalContentConfig, optionalContentConfig,
pdfDocument: this._pdfDocument, pdfDocument: this._pdfDocument,

View file

@ -13,6 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { parseQueryString, removeNullCharacters } from "./ui_utils.js"; import { parseQueryString, removeNullCharacters } from "./ui_utils.js";
const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
@ -490,6 +493,48 @@ class PDFLinkService {
}); });
} }
/**
* @param {Object} action
*/
async executeSetOCGState(action) {
const pdfDocument = this.pdfDocument;
const 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;
}
}
this.pdfViewer.optionalContentConfigPromise = Promise.resolve(
optionalContentConfig
);
}
/** /**
* @param {number} pageNum - page number. * @param {number} pageNum - page number.
* @param {Object} pageRef - reference to the page. * @param {Object} pageRef - reference to the page.
@ -673,6 +718,11 @@ class SimpleLinkService {
*/ */
executeNamedAction(action) {} executeNamedAction(action) {}
/**
* @param {Object} action
*/
executeSetOCGState(action) {}
/** /**
* @param {number} pageNum - page number. * @param {number} pageNum - page number.
* @param {Object} pageRef - reference to the page. * @param {Object} pageRef - reference to the page.

View file

@ -109,13 +109,29 @@ class PDFOutlineViewer extends BaseTreeViewer {
/** /**
* @private * @private
*/ */
_bindLink(element, { url, newWindow, dest }) { _bindLink(element, { url, newWindow, action, dest, setOCGState }) {
const { linkService } = this; const { linkService } = this;
if (url) { if (url) {
linkService.addLinkAttributes(element, url, newWindow); linkService.addLinkAttributes(element, url, newWindow);
return; return;
} }
if (action) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
linkService.executeNamedAction(action);
return false;
};
return;
}
if (setOCGState) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
linkService.executeSetOCGState(setOCGState);
return false;
};
return;
}
element.href = linkService.getDestinationHash(dest); element.href = linkService.getDestinationHash(dest);
element.onclick = evt => { element.onclick = evt => {
@ -204,7 +220,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
this._setStyles(element, item); this._setStyles(element, item);
element.textContent = this._normalizeTextContent(item.title); element.textContent = this._normalizeTextContent(item.title);
div.appendChild(element); div.append(element);
if (item.items.length > 0) { if (item.items.length > 0) {
hasAnyNesting = true; hasAnyNesting = true;
@ -212,12 +228,12 @@ class PDFOutlineViewer extends BaseTreeViewer {
const itemsDiv = document.createElement("div"); const itemsDiv = document.createElement("div");
itemsDiv.className = "treeItems"; itemsDiv.className = "treeItems";
div.appendChild(itemsDiv); div.append(itemsDiv);
queue.push({ parent: itemsDiv, items: item.items }); queue.push({ parent: itemsDiv, items: item.items });
} }
levelData.parent.appendChild(div); levelData.parent.append(div);
outlineCount++; outlineCount++;
} }
} }

View file

@ -13,6 +13,25 @@
* limitations under the License. * limitations under the License.
*/ */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
import { import {
AnnotationMode, AnnotationMode,
createPromiseCapability, createPromiseCapability,
@ -23,6 +42,7 @@ import {
import { import {
approximateFraction, approximateFraction,
DEFAULT_SCALE, DEFAULT_SCALE,
docStyle,
OutputScale, OutputScale,
RendererType, RendererType,
RenderingStates, RenderingStates,
@ -31,41 +51,44 @@ import {
} from "./ui_utils.js"; } from "./ui_utils.js";
import { compatibilityParams } from "./app_options.js"; import { compatibilityParams } from "./app_options.js";
import { NullL10n } from "./l10n_utils.js"; import { NullL10n } from "./l10n_utils.js";
import { TextAccessibilityManager } from "./text_accessibility.js";
/** /**
* @typedef {Object} PDFPageViewOptions * @typedef {Object} PDFPageViewOptions
* @property {HTMLDivElement} [container] - The viewer element. * @property {HTMLDivElement} [container] - The viewer element.
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
* @property {number} id - The page unique ID (normally its number). * @property {number} id - The page unique ID (normally its number).
* @property {number} scale - The page scale display. * @property {number} [scale] - The page scale display.
* @property {PageViewport} defaultViewport - The page viewport. * @property {PageViewport} defaultViewport - The page viewport.
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] - * @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
* A promise that is resolved with an {@link OptionalContentConfig} instance. * A promise that is resolved with an {@link OptionalContentConfig} instance.
* The default value is `null`. * The default value is `null`.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {PDFRenderingQueue} [renderingQueue] - The rendering queue object.
* @property {IPDFTextLayerFactory} textLayerFactory * @property {IPDFTextLayerFactory} [textLayerFactory]
* @property {number} [textLayerMode] - Controls if the text layer used for * @property {number} [textLayerMode] - Controls if the text layer used for
* selection and searching is created, and if the improved text selection * selection and searching is created. The constants from {TextLayerMode}
* behaviour is enabled. The constants from {TextLayerMode} should be used. * should be used. The default value is `TextLayerMode.ENABLE`.
* The default value is `TextLayerMode.ENABLE`.
* @property {number} [annotationMode] - Controls if the annotation layer is * @property {number} [annotationMode] - Controls if the annotation layer is
* created, and if interactive form elements or `AnnotationStorage`-data are * created, and if interactive form elements or `AnnotationStorage`-data are
* being rendered. The constants from {@link AnnotationMode} should be used; * being rendered. The constants from {@link AnnotationMode} should be used;
* see also {@link RenderParameters} and {@link GetOperatorListParameters}. * see also {@link RenderParameters} and {@link GetOperatorListParameters}.
* The default value is `AnnotationMode.ENABLE_FORMS`. * The default value is `AnnotationMode.ENABLE_FORMS`.
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory * @property {IPDFAnnotationLayerFactory} [annotationLayerFactory]
* @property {IPDFXfaLayerFactory} xfaLayerFactory * @property {IPDFAnnotationEditorLayerFactory} [annotationEditorLayerFactory]
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory * @property {IPDFXfaLayerFactory} [xfaLayerFactory]
* @property {IPDFStructTreeLayerFactory} [structTreeLayerFactory]
* @property {Object} [textHighlighterFactory] * @property {Object} [textHighlighterFactory]
* @property {string} [imageResourcesPath] - Path for image resources, mainly * @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
* @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'.
* @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
* value is `false`. * value is `false`.
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
* total pixels, i.e. width * height. Use -1 for no limit. The default value * total pixels, i.e. width * height. Use -1 for no limit. The default value
* is 4096 * 4096 (16 mega-pixels). * is 4096 * 4096 (16 mega-pixels).
* @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 {IL10n} [l10n] - Localization service.
*/ */
const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216; const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216;
@ -76,6 +99,11 @@ const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216;
class PDFPageView { class PDFPageView {
#annotationMode = AnnotationMode.ENABLE_FORMS; #annotationMode = AnnotationMode.ENABLE_FORMS;
#useThumbnailCanvas = {
initialOptionalContent: true,
regularAnnotations: true,
};
/** /**
* @param {PDFPageViewOptions} options * @param {PDFPageViewOptions} options
*/ */
@ -101,19 +129,26 @@ class PDFPageView {
this.imageResourcesPath = options.imageResourcesPath || ""; this.imageResourcesPath = options.imageResourcesPath || "";
this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.useOnlyCssZoom = options.useOnlyCssZoom || false;
this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS; this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
this.pageColors = options.pageColors || null;
this.eventBus = options.eventBus; this.eventBus = options.eventBus;
this.renderingQueue = options.renderingQueue; this.renderingQueue = options.renderingQueue;
this.textLayerFactory = options.textLayerFactory; this.textLayerFactory = options.textLayerFactory;
this.annotationLayerFactory = options.annotationLayerFactory; this.annotationLayerFactory = options.annotationLayerFactory;
this.annotationEditorLayerFactory = options.annotationEditorLayerFactory;
this.xfaLayerFactory = options.xfaLayerFactory; this.xfaLayerFactory = options.xfaLayerFactory;
this.textHighlighter = this.textHighlighter =
options.textHighlighterFactory?.createTextHighlighter( options.textHighlighterFactory?.createTextHighlighter({
this.id - 1, pageIndex: this.id - 1,
this.eventBus eventBus: this.eventBus,
); });
this.structTreeLayerFactory = options.structTreeLayerFactory; this.structTreeLayerFactory = options.structTreeLayerFactory;
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")
) {
this.renderer = options.renderer || RendererType.CANVAS; this.renderer = options.renderer || RendererType.CANVAS;
}
this.l10n = options.l10n || NullL10n; this.l10n = options.l10n || NullL10n;
this.paintTask = null; this.paintTask = null;
@ -121,11 +156,17 @@ class PDFPageView {
this.renderingState = RenderingStates.INITIAL; this.renderingState = RenderingStates.INITIAL;
this.resume = null; this.resume = null;
this._renderError = null; this._renderError = null;
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")
) {
this._isStandalone = !this.renderingQueue?.hasViewer(); this._isStandalone = !this.renderingQueue?.hasViewer();
}
this._annotationCanvasMap = null; this._annotationCanvasMap = null;
this.annotationLayer = null; this.annotationLayer = null;
this.annotationEditorLayer = null;
this.textLayer = null; this.textLayer = null;
this.zoomLayer = null; this.zoomLayer = null;
this.xfaLayer = null; this.xfaLayer = null;
@ -142,7 +183,28 @@ class PDFPageView {
window.siyuan.languages.thumbPageTitle.replace('{{page}}', this.id)) window.siyuan.languages.thumbPageTitle.replace('{{page}}', this.id))
this.div = div; this.div = div;
container?.appendChild(div); container?.append(div);
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this._isStandalone
) {
const { optionalContentConfigPromise } = options;
if (optionalContentConfigPromise) {
// Ensure that the thumbnails always display the *initial* document
// state, for documents with optional content.
optionalContentConfigPromise.then(optionalContentConfig => {
if (
optionalContentConfigPromise !== this._optionalContentConfigPromise
) {
return;
}
this.#useThumbnailCanvas.initialOptionalContent =
optionalContentConfig.hasInitialVisibility;
});
}
}
} }
setPdfPage(pdfPage) { setPdfPage(pdfPage) {
@ -159,9 +221,7 @@ class PDFPageView {
destroy() { destroy() {
this.reset(); this.reset();
if (this.pdfPage) { this.pdfPage?.cleanup();
this.pdfPage.cleanup();
}
} }
/** /**
@ -172,6 +232,7 @@ class PDFPageView {
try { try {
await this.annotationLayer.render(this.viewport, "display"); await this.annotationLayer.render(this.viewport, "display");
} catch (ex) { } catch (ex) {
console.error(`_renderAnnotationLayer: "${ex}".`);
error = ex; error = ex;
} finally { } finally {
this.eventBus.dispatch("annotationlayerrendered", { this.eventBus.dispatch("annotationlayerrendered", {
@ -182,6 +243,25 @@ class PDFPageView {
} }
} }
/**
* @private
*/
async _renderAnnotationEditorLayer() {
let error = null;
try {
await this.annotationEditorLayer.render(this.viewport, "display");
} catch (ex) {
console.error(`_renderAnnotationEditorLayer: "${ex}".`);
error = ex;
} finally {
this.eventBus.dispatch("annotationeditorlayerrendered", {
source: this,
pageNumber: this.id,
error,
});
}
}
/** /**
* @private * @private
*/ */
@ -189,10 +269,11 @@ class PDFPageView {
let error = null; let error = null;
try { try {
const result = await this.xfaLayer.render(this.viewport, "display"); const result = await this.xfaLayer.render(this.viewport, "display");
if (this.textHighlighter) { if (result?.textDivs && this.textHighlighter) {
this._buildXfaTextContentItems(result.textDivs); this._buildXfaTextContentItems(result.textDivs);
} }
} catch (ex) { } catch (ex) {
console.error(`_renderXfaLayer: "${ex}".`);
error = ex; error = ex;
} finally { } finally {
this.eventBus.dispatch("xfalayerrendered", { this.eventBus.dispatch("xfalayerrendered", {
@ -237,9 +318,14 @@ class PDFPageView {
reset({ reset({
keepZoomLayer = false, keepZoomLayer = false,
keepAnnotationLayer = false, keepAnnotationLayer = false,
keepAnnotationEditorLayer = false,
keepXfaLayer = false, keepXfaLayer = false,
} = {}) { } = {}) {
this.cancelRendering({ keepAnnotationLayer, keepXfaLayer }); this.cancelRendering({
keepAnnotationLayer,
keepAnnotationEditorLayer,
keepXfaLayer,
});
this.renderingState = RenderingStates.INITIAL; this.renderingState = RenderingStates.INITIAL;
const div = this.div; const div = this.div;
@ -250,12 +336,15 @@ class PDFPageView {
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null, zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
annotationLayerNode = annotationLayerNode =
(keepAnnotationLayer && this.annotationLayer?.div) || null, (keepAnnotationLayer && this.annotationLayer?.div) || null,
annotationEditorLayerNode =
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null; xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
for (let i = childNodes.length - 1; i >= 0; i--) { for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i]; const node = childNodes[i];
switch (node) { switch (node) {
case zoomLayerNode: case zoomLayerNode:
case annotationLayerNode: case annotationLayerNode:
case annotationEditorLayerNode:
case xfaLayerNode: case xfaLayerNode:
continue; continue;
} }
@ -268,6 +357,12 @@ class PDFPageView {
// so they are not displayed on the already resized page. // so they are not displayed on the already resized page.
this.annotationLayer.hide(); this.annotationLayer.hide();
} }
if (annotationEditorLayerNode) {
this.annotationEditorLayer.hide();
} else {
this.annotationEditorLayer?.destroy();
}
if (xfaLayerNode) { if (xfaLayerNode) {
// Hide the XFA layer until all elements are resized // Hide the XFA layer until all elements are resized
// so they are not displayed on the already resized page. // so they are not displayed on the already resized page.
@ -285,20 +380,29 @@ class PDFPageView {
} }
this._resetZoomLayer(); this._resetZoomLayer();
} }
if (this.svg) { if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this.svg
) {
this.paintedViewportMap.delete(this.svg); this.paintedViewportMap.delete(this.svg);
delete this.svg; delete this.svg;
} }
this.loadingIconDiv = document.createElement("div"); this.loadingIconDiv = document.createElement("div");
this.loadingIconDiv.className = "loadingIcon notVisible"; this.loadingIconDiv.className = "loadingIcon notVisible";
if (this._isStandalone) { if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this._isStandalone
) {
this.toggleLoadingIconSpinner(/* viewVisible = */ true); this.toggleLoadingIconSpinner(/* viewVisible = */ true);
} }
this.loadingIconDiv.setAttribute("role", "img"); this.loadingIconDiv.setAttribute("role", "img");
// NOTE
this.loadingIconDiv?.setAttribute('aria-label', this.loadingIconDiv?.setAttribute('aria-label',
window.siyuan.languages.loading) window.siyuan.languages.loading)
div.appendChild(this.loadingIconDiv); div.append(this.loadingIconDiv);
} }
update({ scale = 0, rotation = null, optionalContentConfigPromise = null }) { update({ scale = 0, rotation = null, optionalContentConfigPromise = null }) {
@ -308,25 +412,43 @@ class PDFPageView {
} }
if (optionalContentConfigPromise instanceof Promise) { if (optionalContentConfigPromise instanceof Promise) {
this._optionalContentConfigPromise = optionalContentConfigPromise; this._optionalContentConfigPromise = optionalContentConfigPromise;
// Ensure that the thumbnails always display the *initial* document state,
// for documents with optional content.
optionalContentConfigPromise.then(optionalContentConfig => {
if (
optionalContentConfigPromise !== this._optionalContentConfigPromise
) {
return;
}
this.#useThumbnailCanvas.initialOptionalContent =
optionalContentConfig.hasInitialVisibility;
});
} }
const totalRotation = (this.rotation + this.pdfPageRotate) % 360; const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
const viewportScale = this.scale * PixelsPerInch.PDF_TO_CSS_UNITS;
this.viewport = this.viewport.clone({ this.viewport = this.viewport.clone({
scale: viewportScale, scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
rotation: totalRotation, rotation: totalRotation,
}); });
if (this._isStandalone) { if (
const { style } = document.documentElement; (typeof PDFJSDev === "undefined" ||
style.setProperty("--zoom-factor", this.scale); PDFJSDev.test("!PRODUCTION || GENERIC")) &&
style.setProperty("--viewport-scale-factor", viewportScale); this._isStandalone
) {
docStyle.setProperty("--scale-factor", this.viewport.scale);
} }
if (this.svg) { if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this.svg
) {
this.cssTransform({ this.cssTransform({
target: this.svg, target: this.svg,
redrawAnnotationLayer: true, redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true, redrawXfaLayer: true,
}); });
@ -360,6 +482,7 @@ class PDFPageView {
this.cssTransform({ this.cssTransform({
target: this.canvas, target: this.canvas,
redrawAnnotationLayer: true, redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true, redrawXfaLayer: true,
}); });
@ -383,6 +506,7 @@ class PDFPageView {
this.reset({ this.reset({
keepZoomLayer: true, keepZoomLayer: true,
keepAnnotationLayer: true, keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true, keepXfaLayer: true,
}); });
} }
@ -391,7 +515,11 @@ class PDFPageView {
* PLEASE NOTE: Most likely you want to use the `this.reset()` method, * PLEASE NOTE: Most likely you want to use the `this.reset()` method,
* rather than calling this one directly. * rather than calling this one directly.
*/ */
cancelRendering({ keepAnnotationLayer = false, keepXfaLayer = false } = {}) { cancelRendering({
keepAnnotationLayer = false,
keepAnnotationEditorLayer = false,
keepXfaLayer = false,
} = {}) {
if (this.paintTask) { if (this.paintTask) {
this.paintTask.cancel(); this.paintTask.cancel();
this.paintTask = null; this.paintTask = null;
@ -410,6 +538,13 @@ class PDFPageView {
this.annotationLayer = null; this.annotationLayer = null;
this._annotationCanvasMap = null; this._annotationCanvasMap = null;
} }
if (
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
) {
this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null;
}
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel(); this.xfaLayer.cancel();
this.xfaLayer = null; this.xfaLayer = null;
@ -424,6 +559,7 @@ class PDFPageView {
cssTransform({ cssTransform({
target, target,
redrawAnnotationLayer = false, redrawAnnotationLayer = false,
redrawAnnotationEditorLayer = false,
redrawXfaLayer = false, redrawXfaLayer = false,
}) { }) {
// Scale target (canvas or svg), its wrapper and page container. // Scale target (canvas or svg), its wrapper and page container.
@ -497,6 +633,9 @@ class PDFPageView {
if (redrawAnnotationLayer && this.annotationLayer) { if (redrawAnnotationLayer && this.annotationLayer) {
this._renderAnnotationLayer(); this._renderAnnotationLayer();
} }
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
this._renderAnnotationEditorLayer();
}
if (redrawXfaLayer && this.xfaLayer) { if (redrawXfaLayer && this.xfaLayer) {
this._renderXfaLayer(); this._renderXfaLayer();
} }
@ -547,34 +686,38 @@ class PDFPageView {
canvasWrapper.style.height = div.style.height; canvasWrapper.style.height = div.style.height;
canvasWrapper.classList.add("canvasWrapper"); canvasWrapper.classList.add("canvasWrapper");
if (this.annotationLayer?.div) { const lastDivBeforeTextDiv =
this.annotationLayer?.div || this.annotationEditorLayer?.div;
if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top. // The annotation layer needs to stay on top.
div.insertBefore(canvasWrapper, this.annotationLayer.div); lastDivBeforeTextDiv.before(canvasWrapper);
} else { } else {
div.appendChild(canvasWrapper); div.append(canvasWrapper);
} }
let textLayer = null; let textLayer = null;
if (this.textLayerMode !== TextLayerMode.DISABLE && this.textLayerFactory) { if (this.textLayerMode !== TextLayerMode.DISABLE && this.textLayerFactory) {
this._accessibilityManager ||= new TextAccessibilityManager();
const textLayerDiv = document.createElement("div"); const textLayerDiv = document.createElement("div");
textLayerDiv.className = "textLayer"; textLayerDiv.className = "textLayer";
textLayerDiv.style.width = canvasWrapper.style.width; textLayerDiv.style.width = canvasWrapper.style.width;
textLayerDiv.style.height = canvasWrapper.style.height; textLayerDiv.style.height = canvasWrapper.style.height;
if (this.annotationLayer?.div) { if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top. // The annotation layer needs to stay on top.
div.insertBefore(textLayerDiv, this.annotationLayer.div); lastDivBeforeTextDiv.before(textLayerDiv);
} else { } else {
div.appendChild(textLayerDiv); div.append(textLayerDiv);
} }
textLayer = this.textLayerFactory.createTextLayerBuilder( textLayer = this.textLayerFactory.createTextLayerBuilder({
textLayerDiv, textLayerDiv,
this.id - 1, pageIndex: this.id - 1,
this.viewport, viewport: this.viewport,
this.textLayerMode === TextLayerMode.ENABLE_ENHANCE, eventBus: this.eventBus,
this.eventBus, highlighter: this.textHighlighter,
this.textHighlighter accessibilityManager: this._accessibilityManager,
); });
} }
this.textLayer = textLayer; this.textLayer = textLayer;
@ -584,24 +727,20 @@ class PDFPageView {
) { ) {
this._annotationCanvasMap ||= new Map(); this._annotationCanvasMap ||= new Map();
this.annotationLayer ||= this.annotationLayer ||=
this.annotationLayerFactory.createAnnotationLayerBuilder( this.annotationLayerFactory.createAnnotationLayerBuilder({
div, pageDiv: div,
pdfPage, pdfPage,
/* annotationStorage = */ null, imageResourcesPath: this.imageResourcesPath,
this.imageResourcesPath, renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS,
this.#annotationMode === AnnotationMode.ENABLE_FORMS, l10n: this.l10n,
this.l10n, annotationCanvasMap: this._annotationCanvasMap,
/* enableScripting = */ null, accessibilityManager: this._accessibilityManager,
/* hasJSActionsPromise = */ null, });
/* mouseState = */ null,
/* fieldObjectsPromise = */ null,
/* annotationCanvasMap */ this._annotationCanvasMap
);
} }
if (this.xfaLayer?.div) { if (this.xfaLayer?.div) {
// The xfa layer needs to stay on top. // The xfa layer needs to stay on top.
div.appendChild(this.xfaLayer.div); div.append(this.xfaLayer.div);
} }
let renderContinueCallback = null; let renderContinueCallback = null;
@ -641,6 +780,10 @@ class PDFPageView {
} }
this._resetZoomLayer(/* removeFromDOM = */ true); this._resetZoomLayer(/* removeFromDOM = */ true);
// Ensure that the thumbnails won't become partially (or fully) blank,
// for documents that contain interactive form elements.
this.#useThumbnailCanvas.regularAnnotations = !paintTask.separateAnnots;
this.eventBus.dispatch("pagerendered", { this.eventBus.dispatch("pagerendered", {
source: this, source: this,
pageNumber: this.id, pageNumber: this.id,
@ -655,6 +798,8 @@ class PDFPageView {
}; };
const paintTask = const paintTask =
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this.renderer === RendererType.SVG this.renderer === RendererType.SVG
? this.paintOnSvg(canvasWrapper) ? this.paintOnSvg(canvasWrapper)
: this.paintOnCanvas(canvasWrapper); : this.paintOnCanvas(canvasWrapper);
@ -673,7 +818,20 @@ class PDFPageView {
} }
if (this.annotationLayer) { if (this.annotationLayer) {
this._renderAnnotationLayer(); this._renderAnnotationLayer().then(() => {
if (this.annotationEditorLayerFactory) {
this.annotationEditorLayer ||=
this.annotationEditorLayerFactory.createAnnotationEditorLayerBuilder(
{
pageDiv: div,
pdfPage,
l10n: this.l10n,
accessibilityManager: this._accessibilityManager,
}
);
this._renderAnnotationEditorLayer();
}
});
} }
}); });
}, },
@ -683,13 +841,10 @@ class PDFPageView {
); );
if (this.xfaLayerFactory) { if (this.xfaLayerFactory) {
if (!this.xfaLayer) { this.xfaLayer ||= this.xfaLayerFactory.createXfaLayerBuilder({
this.xfaLayer = this.xfaLayerFactory.createXfaLayerBuilder( pageDiv: div,
div,
pdfPage, pdfPage,
/* annotationStorage = */ null });
);
}
this._renderXfaLayer(); this._renderXfaLayer();
} }
@ -717,12 +872,12 @@ class PDFPageView {
} }
const treeDom = this.structTreeLayer.render(tree); const treeDom = this.structTreeLayer.render(tree);
treeDom.classList.add("structTree"); treeDom.classList.add("structTree");
this.canvas.appendChild(treeDom); this.canvas.append(treeDom);
}); });
}; };
this.eventBus._on("textlayerrendered", this._onTextLayerRendered); this.eventBus._on("textlayerrendered", this._onTextLayerRendered);
this.structTreeLayer = this.structTreeLayer =
this.structTreeLayerFactory.createStructTreeLayerBuilder(pdfPage); this.structTreeLayerFactory.createStructTreeLayerBuilder({ pdfPage });
} }
div.setAttribute("data-loaded", true); div.setAttribute("data-loaded", true);
@ -744,10 +899,14 @@ class PDFPageView {
cancel() { cancel() {
renderTask.cancel(); renderTask.cancel();
}, },
get separateAnnots() {
return renderTask.separateAnnots;
},
}; };
const viewport = this.viewport; const viewport = this.viewport;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.setAttribute("role", "presentation");
// Keep the canvas hidden until the first draw callback, or until drawing // Keep the canvas hidden until the first draw callback, or until drawing
// is complete when `!this.renderingQueue`, to prevent black flickering. // is complete when `!this.renderingQueue`, to prevent black flickering.
@ -760,16 +919,9 @@ class PDFPageView {
} }
}; };
canvasWrapper.appendChild(canvas); canvasWrapper.append(canvas);
this.canvas = canvas; this.canvas = canvas;
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("MOZCENTRAL || GENERIC")
) {
canvas.mozOpaque = true;
}
const ctx = canvas.getContext("2d", { alpha: false }); const ctx = canvas.getContext("2d", { alpha: false });
const outputScale = (this.outputScale = new OutputScale()); const outputScale = (this.outputScale = new OutputScale());
@ -816,6 +968,7 @@ class PDFPageView {
annotationMode: this.#annotationMode, annotationMode: this.#annotationMode,
optionalContentConfigPromise: this._optionalContentConfigPromise, optionalContentConfigPromise: this._optionalContentConfigPromise,
annotationCanvasMap: this._annotationCanvasMap, annotationCanvasMap: this._annotationCanvasMap,
pageColors: this.pageColors,
}; };
const renderTask = this.pdfPage.render(renderContext); const renderTask = this.pdfPage.render(renderContext);
renderTask.onContinue = function (cont) { renderTask.onContinue = function (cont) {
@ -842,18 +995,13 @@ class PDFPageView {
paintOnSvg(wrapper) { paintOnSvg(wrapper) {
if ( if (
typeof PDFJSDev !== "undefined" && !(
PDFJSDev.test("MOZCENTRAL || CHROME") typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")
)
) { ) {
// Return a mock object, to prevent errors such as e.g. throw new Error("Not implemented: paintOnSvg");
// "TypeError: paintTask.promise is undefined".
return {
promise: Promise.reject(new Error("SVG rendering is not supported.")),
onRenderContinue(cont) {},
cancel() {},
};
} }
let cancelled = false; let cancelled = false;
const ensureNotCancelled = () => { const ensureNotCancelled = () => {
if (cancelled) { if (cancelled) {
@ -883,7 +1031,7 @@ class PDFPageView {
svg.style.width = wrapper.style.width; svg.style.width = wrapper.style.width;
svg.style.height = wrapper.style.height; svg.style.height = wrapper.style.height;
this.renderingState = RenderingStates.FINISHED; this.renderingState = RenderingStates.FINISHED;
wrapper.appendChild(svg); wrapper.append(svg);
}); });
}); });
@ -895,6 +1043,9 @@ class PDFPageView {
cancel() { cancel() {
cancelled = true; cancelled = true;
}, },
get separateAnnots() {
return false;
},
}; };
} }
@ -910,6 +1061,16 @@ class PDFPageView {
this.div.removeAttribute("data-page-label"); this.div.removeAttribute("data-page-label");
} }
} }
/**
* For use by the `PDFThumbnailView.setImage`-method.
* @ignore
*/
get thumbnailCanvas() {
const { initialOptionalContent, regularAnnotations } =
this.#useThumbnailCanvas;
return initialOptionalContent && regularAnnotations ? this.canvas : null;
}
} }
export { PDFPageView }; export { PDFPageView };

View file

@ -19,8 +19,8 @@ import {
ScrollMode, ScrollMode,
SpreadMode, SpreadMode,
} from "./ui_utils.js"; } from "./ui_utils.js";
import { AnnotationEditorType } from "./pdfjs";
const DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1500; // in ms
const DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms const DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms
const ACTIVE_SELECTOR = "pdfPresentationMode"; const ACTIVE_SELECTOR = "pdfPresentationMode";
const CONTROLS_SELECTOR = "pdfPresentationModeControls"; const CONTROLS_SELECTOR = "pdfPresentationModeControls";
@ -42,6 +42,10 @@ const SWIPE_ANGLE_THRESHOLD = Math.PI / 6;
*/ */
class PDFPresentationMode { class PDFPresentationMode {
#state = PresentationModeState.UNKNOWN;
#args = null;
/** /**
* @param {PDFPresentationModeOptions} options * @param {PDFPresentationModeOptions} options
*/ */
@ -50,8 +54,6 @@ class PDFPresentationMode {
this.pdfViewer = pdfViewer; this.pdfViewer = pdfViewer;
this.eventBus = eventBus; this.eventBus = eventBus;
this.active = false;
this.args = null;
this.contextMenuOpen = false; this.contextMenuOpen = false;
this.mouseScrollTimeStamp = 0; this.mouseScrollTimeStamp = 0;
this.mouseScrollDelta = 0; this.mouseScrollDelta = 0;
@ -60,37 +62,63 @@ class PDFPresentationMode {
/** /**
* Request the browser to enter fullscreen mode. * Request the browser to enter fullscreen mode.
* @returns {boolean} Indicating if the request was successful. * @returns {Promise<boolean>} Indicating if the request was successful.
*/ */
request() { async request() {
if ( const { container, pdfViewer } = this;
this.switchInProgress ||
this.active || if (this.active || !pdfViewer.pagesCount || !container.requestFullscreen) {
!this.pdfViewer.pagesCount ||
!this.container.requestFullscreen
) {
return false; return false;
} }
this.#addFullscreenChangeListeners(); this.#addFullscreenChangeListeners();
this.#setSwitchInProgress(); this.#notifyStateChange(PresentationModeState.CHANGING);
this.#notifyStateChange();
this.container.requestFullscreen(); const promise = container.requestFullscreen();
this.args = { this.#args = {
pageNumber: this.pdfViewer.currentPageNumber, pageNumber: pdfViewer.currentPageNumber,
scaleValue: this.pdfViewer.currentScaleValue, scaleValue: pdfViewer.currentScaleValue,
scrollMode: this.pdfViewer.scrollMode, scrollMode: pdfViewer.scrollMode,
spreadMode: this.pdfViewer.spreadMode, spreadMode: null,
annotationEditorMode: null,
}; };
if (
pdfViewer.spreadMode !== SpreadMode.NONE &&
!(pdfViewer.pageViewsReady && pdfViewer.hasEqualPageSizes)
) {
console.warn(
"Ignoring Spread modes when entering PresentationMode, " +
"since the document may contain varying page sizes."
);
this.#args.spreadMode = pdfViewer.spreadMode;
}
if (pdfViewer.annotationEditorMode !== AnnotationEditorType.DISABLE) {
this.#args.annotationEditorMode = pdfViewer.annotationEditorMode;
}
try {
await promise;
pdfViewer.focus(); // Fixes bug 1787456.
return true; return true;
} catch (reason) {
this.#removeFullscreenChangeListeners();
this.#notifyStateChange(PresentationModeState.NORMAL);
}
return false;
}
get active() {
return (
this.#state === PresentationModeState.CHANGING ||
this.#state === PresentationModeState.FULLSCREEN
);
} }
#mouseWheel(evt) { #mouseWheel(evt) {
if (!this.active) { if (!this.active) {
return; return;
} }
evt.preventDefault(); evt.preventDefault();
const delta = normalizeWheelEventDelta(evt); const delta = normalizeWheelEventDelta(evt);
@ -126,57 +154,29 @@ class PDFPresentationMode {
} }
} }
#notifyStateChange() { #notifyStateChange(state) {
let state = PresentationModeState.NORMAL; this.#state = state;
if (this.switchInProgress) {
state = PresentationModeState.CHANGING;
} else if (this.active) {
state = PresentationModeState.FULLSCREEN;
}
this.eventBus.dispatch("presentationmodechanged", {
source: this,
state,
});
}
/** this.eventBus.dispatch("presentationmodechanged", { source: this, state });
* Used to initialize a timeout when requesting Presentation Mode,
* i.e. when the browser is requested to enter fullscreen mode.
* This timeout is used to prevent the current page from being scrolled
* partially, or completely, out of view when entering Presentation Mode.
* NOTE: This issue seems limited to certain zoom levels (e.g. page-width).
*/
#setSwitchInProgress() {
if (this.switchInProgress) {
clearTimeout(this.switchInProgress);
}
this.switchInProgress = setTimeout(() => {
this.#removeFullscreenChangeListeners();
delete this.switchInProgress;
this.#notifyStateChange();
}, DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS);
}
#resetSwitchInProgress() {
if (this.switchInProgress) {
clearTimeout(this.switchInProgress);
delete this.switchInProgress;
}
} }
#enter() { #enter() {
this.active = true; this.#notifyStateChange(PresentationModeState.FULLSCREEN);
this.#resetSwitchInProgress();
this.#notifyStateChange();
this.container.classList.add(ACTIVE_SELECTOR); this.container.classList.add(ACTIVE_SELECTOR);
// Ensure that the correct page is scrolled into view when entering // Ensure that the correct page is scrolled into view when entering
// Presentation Mode, by waiting until fullscreen mode in enabled. // Presentation Mode, by waiting until fullscreen mode in enabled.
setTimeout(() => { setTimeout(() => {
this.pdfViewer.scrollMode = ScrollMode.PAGE; this.pdfViewer.scrollMode = ScrollMode.PAGE;
if (this.#args.spreadMode !== null) {
this.pdfViewer.spreadMode = SpreadMode.NONE; this.pdfViewer.spreadMode = SpreadMode.NONE;
this.pdfViewer.currentPageNumber = this.args.pageNumber; }
this.pdfViewer.currentPageNumber = this.#args.pageNumber;
this.pdfViewer.currentScaleValue = "page-fit"; this.pdfViewer.currentScaleValue = "page-fit";
if (this.#args.annotationEditorMode !== null) {
this.pdfViewer.annotationEditorMode = AnnotationEditorType.NONE;
}
}, 0); }, 0);
this.#addWindowListeners(); this.#addWindowListeners();
@ -196,15 +196,20 @@ class PDFPresentationMode {
// Ensure that the correct page is scrolled into view when exiting // Ensure that the correct page is scrolled into view when exiting
// Presentation Mode, by waiting until fullscreen mode is disabled. // Presentation Mode, by waiting until fullscreen mode is disabled.
setTimeout(() => { setTimeout(() => {
this.active = false;
this.#removeFullscreenChangeListeners(); this.#removeFullscreenChangeListeners();
this.#notifyStateChange(); this.#notifyStateChange(PresentationModeState.NORMAL);
this.pdfViewer.scrollMode = this.args.scrollMode; this.pdfViewer.scrollMode = this.#args.scrollMode;
this.pdfViewer.spreadMode = this.args.spreadMode; if (this.#args.spreadMode !== null) {
this.pdfViewer.currentScaleValue = this.args.scaleValue; this.pdfViewer.spreadMode = this.#args.spreadMode;
}
this.pdfViewer.currentScaleValue = this.#args.scaleValue;
this.pdfViewer.currentPageNumber = pageNumber; this.pdfViewer.currentPageNumber = pageNumber;
this.args = null;
if (this.#args.annotationEditorMode !== null) {
this.pdfViewer.annotationEditorMode = this.#args.annotationEditorMode;
}
this.#args = null;
}, 0); }, 0);
this.#removeWindowListeners(); this.#removeWindowListeners();

View file

@ -13,6 +13,11 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
/** @typedef {import("./pdf_viewer").PDFViewer} PDFViewer */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_thumbnail_viewer").PDFThumbnailViewer} PDFThumbnailViewer */
import { RenderingCancelledException } from "./pdfjs"; import { RenderingCancelledException } from "./pdfjs";
import { RenderingStates } from "./ui_utils.js"; import { RenderingStates } from "./ui_utils.js";

View file

@ -13,6 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./event_utils").EventBus} EventBus */
import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js"; import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js";
import { createPromiseCapability, shadow } from "./pdfjs"; import { createPromiseCapability, shadow } from "./pdfjs";
@ -152,7 +154,7 @@ class PDFScriptingManager {
this._eventBus._on(name, listener); this._eventBus._on(name, listener);
} }
for (const [name, listener] of this._domEvents) { for (const [name, listener] of this._domEvents) {
window.addEventListener(name, listener); window.addEventListener(name, listener, true);
} }
try { try {
@ -309,7 +311,7 @@ class PDFScriptingManager {
this._pdfViewer.currentScaleValue = value; this._pdfViewer.currentScaleValue = value;
break; break;
case "SaveAs": case "SaveAs":
this._eventBus.dispatch("save", { source: this }); this._eventBus.dispatch("download", { source: this });
break; break;
case "FirstPage": case "FirstPage":
this._pdfViewer.currentPageNumber = 1; this._pdfViewer.currentPageNumber = 1;
@ -349,7 +351,9 @@ class PDFScriptingManager {
const ids = siblings ? [id, ...siblings] : [id]; const ids = siblings ? [id, ...siblings] : [id];
for (const elementId of ids) { for (const elementId of ids) {
const element = document.getElementById(elementId); const element = document.querySelector(
`[data-element-id="${elementId}"]`
);
if (element) { if (element) {
element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail })); element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
} else { } else {
@ -505,7 +509,7 @@ class PDFScriptingManager {
this._internalEvents.clear(); this._internalEvents.clear();
for (const [name, listener] of this._domEvents) { for (const [name, listener] of this._domEvents) {
window.removeEventListener(name, listener); window.removeEventListener(name, listener, true);
} }
this._domEvents.clear(); this._domEvents.clear();

View file

@ -34,8 +34,8 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
* @typedef {Object} PDFSidebarElements * @typedef {Object} PDFSidebarElements
* @property {HTMLDivElement} outerContainer - The outer container * @property {HTMLDivElement} outerContainer - The outer container
* (encasing both the viewer and sidebar elements). * (encasing both the viewer and sidebar elements).
* @property {HTMLDivElement} viewerContainer - The viewer container * @property {HTMLDivElement} sidebarContainer - The sidebar container
* (in which the viewer element is placed). * (in which the views are placed).
* @property {HTMLButtonElement} toggleButton - The button used for * @property {HTMLButtonElement} toggleButton - The button used for
* opening/closing the sidebar. * opening/closing the sidebar.
* @property {HTMLButtonElement} thumbnailButton - The button used to show * @property {HTMLButtonElement} thumbnailButton - The button used to show
@ -68,6 +68,7 @@ class PDFSidebar {
this.isOpen = false; this.isOpen = false;
this.active = SidebarView.THUMBS; this.active = SidebarView.THUMBS;
this.isInitialViewSet = false; this.isInitialViewSet = false;
this.isInitialEventDispatched = false;
/** /**
* Callback used when the sidebar has been opened/closed, to ensure that * Callback used when the sidebar has been opened/closed, to ensure that
@ -79,7 +80,7 @@ class PDFSidebar {
this.pdfThumbnailViewer = pdfThumbnailViewer; this.pdfThumbnailViewer = pdfThumbnailViewer;
this.outerContainer = elements.outerContainer; this.outerContainer = elements.outerContainer;
this.viewerContainer = elements.viewerContainer; this.sidebarContainer = elements.sidebarContainer;
this.toggleButton = elements.toggleButton; this.toggleButton = elements.toggleButton;
this.thumbnailButton = elements.thumbnailButton; this.thumbnailButton = elements.thumbnailButton;
@ -98,13 +99,14 @@ class PDFSidebar {
this.eventBus = eventBus; this.eventBus = eventBus;
this.l10n = l10n; this.l10n = l10n;
this._addEventListeners(); this.#addEventListeners();
} }
reset() { reset() {
this.isInitialViewSet = false; this.isInitialViewSet = false;
this.isInitialEventDispatched = false;
this._hideUINotification(/* reset = */ true); this.#hideUINotification(/* reset = */ true);
this.switchView(SidebarView.THUMBS); this.switchView(SidebarView.THUMBS);
this.outlineButton.disabled = false; this.outlineButton.disabled = false;
@ -120,22 +122,6 @@ class PDFSidebar {
return this.isOpen ? this.active : SidebarView.NONE; return this.isOpen ? this.active : SidebarView.NONE;
} }
get isThumbnailViewVisible() {
return this.isOpen && this.active === SidebarView.THUMBS;
}
get isOutlineViewVisible() {
return this.isOpen && this.active === SidebarView.OUTLINE;
}
get isAttachmentsViewVisible() {
return this.isOpen && this.active === SidebarView.ATTACHMENTS;
}
get isLayersViewVisible() {
return this.isOpen && this.active === SidebarView.LAYERS;
}
/** /**
* @param {number} view - The sidebar view that should become visible, * @param {number} view - The sidebar view that should become visible,
* must be one of the values in {SidebarView}. * must be one of the values in {SidebarView}.
@ -149,13 +135,15 @@ class PDFSidebar {
// If the user has already manually opened the sidebar, immediately closing // If the user has already manually opened the sidebar, immediately closing
// it would be bad UX; also ignore the "unknown" sidebar view value. // it would be bad UX; also ignore the "unknown" sidebar view value.
if (view === SidebarView.NONE || view === SidebarView.UNKNOWN) { if (view === SidebarView.NONE || view === SidebarView.UNKNOWN) {
this._dispatchEvent(); this.#dispatchEvent();
return; return;
} }
// Prevent dispatching two back-to-back `sidebarviewchanged` events, this.switchView(view, /* forceOpen = */ true);
// since `this._switchView` dispatched the event if the view changed.
if (!this._switchView(view, /* forceOpen */ true)) { // Prevent dispatching two back-to-back "sidebarviewchanged" events,
this._dispatchEvent(); // since `this.switchView` dispatched the event if the view changed.
if (!this.isInitialEventDispatched) {
this.#dispatchEvent();
} }
} }
@ -166,14 +154,6 @@ class PDFSidebar {
* The default value is `false`. * The default value is `false`.
*/ */
switchView(view, forceOpen = false) { switchView(view, forceOpen = false) {
this._switchView(view, forceOpen);
}
/**
* @returns {boolean} Indicating if `this._dispatchEvent` was called.
* @private
*/
_switchView(view, forceOpen = false) {
const isViewChanged = view !== this.active; const isViewChanged = view !== this.active;
let shouldForceRendering = false; let shouldForceRendering = false;
@ -181,9 +161,8 @@ class PDFSidebar {
case SidebarView.NONE: case SidebarView.NONE:
if (this.isOpen) { if (this.isOpen) {
this.close(); this.close();
return true; // Closing will trigger rendering and dispatch the event.
} }
return false; return; // Closing will trigger rendering and dispatch the event.
case SidebarView.THUMBS: case SidebarView.THUMBS:
if (this.isOpen && isViewChanged) { if (this.isOpen && isViewChanged) {
shouldForceRendering = true; shouldForceRendering = true;
@ -191,22 +170,22 @@ class PDFSidebar {
break; break;
case SidebarView.OUTLINE: case SidebarView.OUTLINE:
if (this.outlineButton.disabled) { if (this.outlineButton.disabled) {
return false; return;
} }
break; break;
case SidebarView.ATTACHMENTS: case SidebarView.ATTACHMENTS:
if (this.attachmentsButton.disabled) { if (this.attachmentsButton.disabled) {
return false; return;
} }
break; break;
case SidebarView.LAYERS: case SidebarView.LAYERS:
if (this.layersButton.disabled) { if (this.layersButton.disabled) {
return false; return;
} }
break; break;
default: default:
console.error(`PDFSidebar._switchView: "${view}" is not a valid view.`); console.error(`PDFSidebar.switchView: "${view}" is not a valid view.`);
return false; return;
} }
// Update the active view *after* it has been validated above, // Update the active view *after* it has been validated above,
// in order to prevent setting it to an invalid state. // in order to prevent setting it to an invalid state.
@ -223,31 +202,32 @@ class PDFSidebar {
this.attachmentsButton.classList.toggle("toggled", isAttachments); this.attachmentsButton.classList.toggle("toggled", isAttachments);
this.layersButton.classList.toggle("toggled", isLayers); this.layersButton.classList.toggle("toggled", isLayers);
this.thumbnailButton.setAttribute("aria-checked", `${isThumbs}`); this.thumbnailButton.setAttribute("aria-checked", isThumbs);
this.outlineButton.setAttribute("aria-checked", `${isOutline}`); this.outlineButton.setAttribute("aria-checked", isOutline);
this.attachmentsButton.setAttribute("aria-checked", `${isAttachments}`); this.attachmentsButton.setAttribute("aria-checked", isAttachments);
this.layersButton.setAttribute("aria-checked", `${isLayers}`); this.layersButton.setAttribute("aria-checked", isLayers);
// ... and for all views. // ... and for all views.
// NOTE
this.thumbnailView.classList.toggle("fn__hidden", !isThumbs); this.thumbnailView.classList.toggle("fn__hidden", !isThumbs);
this.outlineView.classList.toggle("fn__hidden", !isOutline); this.outlineView.classList.toggle("fn__hidden", !isOutline);
this.attachmentsView.classList.toggle("fn__hidden", !isAttachments); this.attachmentsView.classList.toggle("fn__hidden", !isAttachments);
this.layersView.classList.toggle("fn__hidden", !isLayers); this.layersView.classList.toggle("fn__hidden", !isLayers);
// Finally, update view-specific CSS classes. // Finally, update view-specific CSS classes.
// NOTE
this._outlineOptionsContainer.classList.toggle("fn__hidden", !isOutline); this._outlineOptionsContainer.classList.toggle("fn__hidden", !isOutline);
if (forceOpen && !this.isOpen) { if (forceOpen && !this.isOpen) {
this.open(); this.open();
return true; // Opening will trigger rendering and dispatch the event. return; // Opening will trigger rendering and dispatch the event.
} }
if (shouldForceRendering) { if (shouldForceRendering) {
this._updateThumbnailViewer(); this.#updateThumbnailViewer();
this._forceRendering(); this.#forceRendering();
} }
if (isViewChanged) { if (isViewChanged) {
this._dispatchEvent(); this.#dispatchEvent();
} }
return isViewChanged;
} }
open() { open() {
@ -261,12 +241,12 @@ class PDFSidebar {
this.outerContainer.classList.add("sidebarMoving", "sidebarOpen"); this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");
if (this.active === SidebarView.THUMBS) { if (this.active === SidebarView.THUMBS) {
this._updateThumbnailViewer(); this.#updateThumbnailViewer();
} }
this._forceRendering(); this.#forceRendering();
this._dispatchEvent(); this.#dispatchEvent();
this._hideUINotification(); this.#hideUINotification();
} }
close() { close() {
@ -280,8 +260,8 @@ class PDFSidebar {
this.outerContainer.classList.add("sidebarMoving"); this.outerContainer.classList.add("sidebarMoving");
this.outerContainer.classList.remove("sidebarOpen"); this.outerContainer.classList.remove("sidebarOpen");
this._forceRendering(); this.#forceRendering();
this._dispatchEvent(); this.#dispatchEvent();
} }
toggle() { toggle() {
@ -292,20 +272,18 @@ class PDFSidebar {
} }
} }
/** #dispatchEvent() {
* @private if (this.isInitialViewSet && !this.isInitialEventDispatched) {
*/ this.isInitialEventDispatched = true;
_dispatchEvent() { }
this.eventBus.dispatch("sidebarviewchanged", { this.eventBus.dispatch("sidebarviewchanged", {
source: this, source: this,
view: this.visibleView, view: this.visibleView,
}); });
} }
/** #forceRendering() {
* @private
*/
_forceRendering() {
if (this.onToggled) { if (this.onToggled) {
this.onToggled(); this.onToggled();
} else { } else {
@ -315,10 +293,7 @@ class PDFSidebar {
} }
} }
/** #updateThumbnailViewer() {
* @private
*/
_updateThumbnailViewer() {
const { pdfViewer, pdfThumbnailViewer } = this; const { pdfViewer, pdfThumbnailViewer } = this;
// Use the rendered pages to set the corresponding thumbnail images. // Use the rendered pages to set the corresponding thumbnail images.
@ -333,10 +308,8 @@ class PDFSidebar {
pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber); pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);
} }
/** #showUINotification() {
* @private // NOTE
*/
_showUINotification() {
this.toggleButton.title = window.siyuan.languages.toggleSidebarNotification2Title this.toggleButton.title = window.siyuan.languages.toggleSidebarNotification2Title
if (!this.isOpen) { if (!this.isOpen) {
@ -346,10 +319,7 @@ class PDFSidebar {
} }
} }
/** #hideUINotification(reset = false) {
* @private
*/
_hideUINotification(reset = false) {
if (this.isOpen || reset) { if (this.isOpen || reset) {
// Only hide the notification on the `toggleButton` if the sidebar is // Only hide the notification on the `toggleButton` if the sidebar is
// currently open, or when the current PDF document is being closed. // currently open, or when the current PDF document is being closed.
@ -357,16 +327,14 @@ class PDFSidebar {
} }
if (reset) { if (reset) {
// NOTE
this.toggleButton.title = window.siyuan.languages.toggleSidebarTitle this.toggleButton.title = window.siyuan.languages.toggleSidebarTitle
} }
} }
/** #addEventListeners() {
* @private this.sidebarContainer.addEventListener("transitionend", evt => {
*/ if (evt.target === this.sidebarContainer) {
_addEventListeners() {
this.viewerContainer.addEventListener("transitionend", evt => {
if (evt.target === this.viewerContainer) {
this.outerContainer.classList.remove("sidebarMoving"); this.outerContainer.classList.remove("sidebarMoving");
} }
}); });
@ -408,7 +376,7 @@ class PDFSidebar {
button.disabled = !count; button.disabled = !count;
if (count) { if (count) {
this._showUINotification(); this.#showUINotification();
} else if (this.active === view) { } else if (this.active === view) {
// If the `view` was opened by the user during document load, // If the `view` was opened by the user during document load,
// switch away from it if it turns out to be empty. // switch away from it if it turns out to be empty.
@ -443,9 +411,9 @@ class PDFSidebar {
this.eventBus._on("presentationmodechanged", evt => { this.eventBus._on("presentationmodechanged", evt => {
if ( if (
evt.state === PresentationModeState.NORMAL && evt.state === PresentationModeState.NORMAL &&
this.isThumbnailViewVisible this.visibleView === SidebarView.THUMBS
) { ) {
this._updateThumbnailViewer(); this.#updateThumbnailViewer();
} }
}); });
} }

View file

@ -13,6 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { docStyle } from "./ui_utils.js";
// NOTE
const SIDEBAR_WIDTH_VAR = "--b3-pdf-sidebar-width"; const SIDEBAR_WIDTH_VAR = "--b3-pdf-sidebar-width";
const SIDEBAR_MIN_WIDTH = 200; // pixels const SIDEBAR_MIN_WIDTH = 200; // pixels
const SIDEBAR_RESIZING_CLASS = "sidebarResizing"; const SIDEBAR_RESIZING_CLASS = "sidebarResizing";
@ -34,7 +37,6 @@ class PDFSidebarResizer {
constructor(options, eventBus, l10n) { constructor(options, eventBus, l10n) {
this.isRTL = false; this.isRTL = false;
this.sidebarOpen = false; this.sidebarOpen = false;
this.doc = document.documentElement;
this._width = null; this._width = null;
this._outerContainerWidth = null; this._outerContainerWidth = null;
this._boundEvents = Object.create(null); this._boundEvents = Object.create(null);
@ -42,6 +44,7 @@ class PDFSidebarResizer {
this.outerContainer = options.outerContainer; this.outerContainer = options.outerContainer;
this.resizer = options.resizer; this.resizer = options.resizer;
this.eventBus = eventBus; this.eventBus = eventBus;
// NOTE
this.isRTL = false; this.isRTL = false;
this._addEventListeners(); this._addEventListeners();
} }
@ -72,7 +75,8 @@ class PDFSidebarResizer {
return false; return false;
} }
this._width = width; this._width = width;
this.doc.style.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
return true; return true;
} }

View file

@ -13,13 +13,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { OutputScale, RenderingStates } from './ui_utils.js' /** @typedef {import("./interfaces").IL10n} IL10n */
import { RenderingCancelledException } from './pdfjs' /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
const DRAW_UPSCALE_FACTOR = 2 // See comment in `PDFThumbnailView.draw` below. import { OutputScale, RenderingStates } from "./ui_utils.js";
const MAX_NUM_SCALING_STEPS = 3 import { RenderingCancelledException } from "./pdfjs";
const THUMBNAIL_CANVAS_BORDER_WIDTH = 1 // px
const THUMBNAIL_WIDTH = 98 // px 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 * @typedef {Object} PDFThumbnailViewOptions
@ -31,44 +37,39 @@ const THUMBNAIL_WIDTH = 98 // px
* The default value is `null`. * The default value is `null`.
* @property {IPDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {function} checkSetImageDisabled
* @property {IL10n} l10n - Localization service. * @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.
*/ */
class TempImageFactory { class TempImageFactory {
static #tempCanvas = null static #tempCanvas = null;
static getCanvas(width, height) { static getCanvas(width, height) {
const tempCanvas = (this.#tempCanvas ||= document.createElement('canvas')) const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas"));
tempCanvas.width = width tempCanvas.width = width;
tempCanvas.height = height tempCanvas.height = height;
// Since this is a temporary canvas, we need to fill it with a white // 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.
if ( const ctx = tempCanvas.getContext("2d", { alpha: false });
typeof PDFJSDev === 'undefined' || ctx.save();
PDFJSDev.test('MOZCENTRAL || GENERIC') ctx.fillStyle = "rgb(255, 255, 255)";
) { ctx.fillRect(0, 0, width, height);
tempCanvas.mozOpaque = true ctx.restore();
} return [tempCanvas, tempCanvas.getContext("2d")];
const ctx = tempCanvas.getContext('2d', {alpha: false})
ctx.save()
ctx.fillStyle = 'rgb(255, 255, 255)'
ctx.fillRect(0, 0, width, height)
ctx.restore()
return [tempCanvas, tempCanvas.getContext('2d')]
} }
static destroyCanvas() { static destroyCanvas() {
const tempCanvas = this.#tempCanvas const tempCanvas = this.#tempCanvas;
if (tempCanvas) { if (tempCanvas) {
// Zeroing the width and height causes Firefox to release graphics // Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption. // resources immediately, which can greatly reduce memory consumption.
tempCanvas.width = 0 tempCanvas.width = 0;
tempCanvas.height = 0 tempCanvas.height = 0;
} }
this.#tempCanvas = null this.#tempCanvas = null;
} }
} }
@ -86,116 +87,113 @@ class PDFThumbnailView {
optionalContentConfigPromise, optionalContentConfigPromise,
linkService, linkService,
renderingQueue, renderingQueue,
checkSetImageDisabled,
l10n, l10n,
pageColors,
}) { }) {
this.id = id this.id = id;
this.renderingId = 'thumbnail' + id this.renderingId = "thumbnail" + id;
this.pageLabel = null this.pageLabel = null;
this.pdfPage = null this.pdfPage = null;
this.rotation = 0 this.rotation = 0;
this.viewport = defaultViewport this.viewport = defaultViewport;
this.pdfPageRotate = defaultViewport.rotation this.pdfPageRotate = defaultViewport.rotation;
this._optionalContentConfigPromise = optionalContentConfigPromise || null this._optionalContentConfigPromise = optionalContentConfigPromise || null;
this.pageColors = pageColors || null;
this.linkService = linkService this.linkService = linkService;
this.renderingQueue = renderingQueue this.renderingQueue = renderingQueue;
this.renderTask = null this.renderTask = null;
this.renderingState = RenderingStates.INITIAL this.renderingState = RenderingStates.INITIAL;
this.resume = null this.resume = null;
this._checkSetImageDisabled =
checkSetImageDisabled ||
function () {
return false
}
const pageWidth = this.viewport.width, const pageWidth = this.viewport.width,
pageHeight = this.viewport.height, pageHeight = this.viewport.height,
pageRatio = pageWidth / pageHeight pageRatio = pageWidth / pageHeight;
this.canvasWidth = THUMBNAIL_WIDTH this.canvasWidth = THUMBNAIL_WIDTH;
this.canvasHeight = (this.canvasWidth / pageRatio) | 0 this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
this.scale = this.canvasWidth / pageWidth this.scale = this.canvasWidth / pageWidth;
this.l10n = l10n this.l10n = l10n;
const anchor = document.createElement('a') const anchor = document.createElement("a");
anchor.href = linkService.getAnchorUrl('#page=' + id) anchor.href = linkService.getAnchorUrl("#page=" + id);
// NOTE
anchor.title = this._thumbPageTitle anchor.title = this._thumbPageTitle
anchor.onclick = function () { anchor.onclick = function () {
linkService.goToPage(id) linkService.goToPage(id);
return false return false;
} };
this.anchor = anchor this.anchor = anchor;
const div = document.createElement('div') const div = document.createElement("div");
div.className = 'thumbnail' div.className = "thumbnail";
div.setAttribute('data-page-number', this.id) div.setAttribute("data-page-number", this.id);
this.div = div this.div = div;
const ring = document.createElement('div') const ring = document.createElement("div");
ring.className = 'thumbnailSelectionRing' ring.className = "thumbnailSelectionRing";
const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
ring.style.width = this.canvasWidth + borderAdjustment + 'px' ring.style.width = this.canvasWidth + borderAdjustment + "px";
ring.style.height = this.canvasHeight + borderAdjustment + 'px' ring.style.height = this.canvasHeight + borderAdjustment + "px";
this.ring = ring this.ring = ring;
div.appendChild(ring) div.append(ring);
anchor.appendChild(div) anchor.append(div);
container.appendChild(anchor) container.append(anchor);
} }
setPdfPage(pdfPage) { setPdfPage(pdfPage) {
this.pdfPage = pdfPage this.pdfPage = pdfPage;
this.pdfPageRotate = pdfPage.rotate this.pdfPageRotate = pdfPage.rotate;
const totalRotation = (this.rotation + this.pdfPageRotate) % 360 const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = pdfPage.getViewport({scale: 1, rotation: totalRotation}) this.viewport = pdfPage.getViewport({ scale: 1, rotation: totalRotation });
this.reset() this.reset();
} }
reset() { reset() {
this.cancelRendering() this.cancelRendering();
this.renderingState = RenderingStates.INITIAL this.renderingState = RenderingStates.INITIAL;
const pageWidth = this.viewport.width, const pageWidth = this.viewport.width,
pageHeight = this.viewport.height, pageHeight = this.viewport.height,
pageRatio = pageWidth / pageHeight pageRatio = pageWidth / pageHeight;
this.canvasHeight = (this.canvasWidth / pageRatio) | 0 this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
this.scale = this.canvasWidth / pageWidth this.scale = this.canvasWidth / pageWidth;
this.div.removeAttribute('data-loaded') this.div.removeAttribute("data-loaded");
const ring = this.ring const ring = this.ring;
ring.textContent = '' // Remove the thumbnail from the DOM. ring.textContent = ""; // Remove the thumbnail from the DOM.
const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
ring.style.width = this.canvasWidth + borderAdjustment + 'px' ring.style.width = this.canvasWidth + borderAdjustment + "px";
ring.style.height = this.canvasHeight + borderAdjustment + 'px' ring.style.height = this.canvasHeight + borderAdjustment + "px";
if (this.canvas) { if (this.canvas) {
// Zeroing the width and height causes Firefox to release graphics // Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption. // resources immediately, which can greatly reduce memory consumption.
this.canvas.width = 0 this.canvas.width = 0;
this.canvas.height = 0 this.canvas.height = 0;
delete this.canvas delete this.canvas;
} }
if (this.image) { if (this.image) {
this.image.removeAttribute('src') this.image.removeAttribute("src");
delete this.image delete this.image;
} }
} }
update({ rotation = null }) { update({ rotation = null }) {
if (typeof rotation === 'number') { if (typeof rotation === "number") {
this.rotation = rotation // The rotation may be zero. this.rotation = rotation; // The rotation may be zero.
} }
const totalRotation = (this.rotation + this.pdfPageRotate) % 360 const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = this.viewport.clone({ this.viewport = this.viewport.clone({
scale: 1, scale: 1,
rotation: totalRotation, rotation: totalRotation,
}) });
this.reset() this.reset();
} }
/** /**
@ -204,10 +202,10 @@ class PDFThumbnailView {
*/ */
cancelRendering() { cancelRendering() {
if (this.renderTask) { if (this.renderTask) {
this.renderTask.cancel() this.renderTask.cancel();
this.renderTask = null this.renderTask = null;
} }
this.resume = null this.resume = null;
} }
/** /**
@ -216,25 +214,18 @@ class PDFThumbnailView {
_getPageDrawContext(upscaleFactor = 1) { _getPageDrawContext(upscaleFactor = 1) {
// Keep the no-thumbnail outline visible, i.e. `data-loaded === false`, // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
// until rendering/image conversion is complete, to avoid display issues. // until rendering/image conversion is complete, to avoid display issues.
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { alpha: false });
const outputScale = new OutputScale();
if ( canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0;
typeof PDFJSDev === 'undefined' || canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0;
PDFJSDev.test('MOZCENTRAL || GENERIC')
) {
canvas.mozOpaque = true
}
const ctx = canvas.getContext('2d', {alpha: false})
const outputScale = new OutputScale()
canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0
canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0
const transform = outputScale.scaled const transform = outputScale.scaled
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
: null : null;
return {ctx, canvas, transform} return { ctx, canvas, transform };
} }
/** /**
@ -242,60 +233,62 @@ class PDFThumbnailView {
*/ */
_convertCanvasToImage(canvas) { _convertCanvasToImage(canvas) {
if (this.renderingState !== RenderingStates.FINISHED) { 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') const image = document.createElement("img");
image.className = 'thumbnailImage' image.className = "thumbnailImage";
image.setAttribute('aria-label', this._thumbPageCanvas) this._thumbPageCanvas.then(msg => {
image.style.width = this.canvasWidth + 'px' image.setAttribute("aria-label", msg);
image.style.height = this.canvasHeight + 'px' });
image.style.width = this.canvasWidth + "px";
image.style.height = this.canvasHeight + "px";
image.src = reducedCanvas.toDataURL() image.src = reducedCanvas.toDataURL();
this.image = image this.image = image;
this.div.setAttribute('data-loaded', true) this.div.setAttribute("data-loaded", true);
this.ring.appendChild(image) this.ring.append(image);
// Zeroing the width and height causes Firefox to release graphics // Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption. // resources immediately, which can greatly reduce memory consumption.
reducedCanvas.width = 0 reducedCanvas.width = 0;
reducedCanvas.height = 0 reducedCanvas.height = 0;
} }
draw() { draw() {
if (this.renderingState !== RenderingStates.INITIAL) { if (this.renderingState !== RenderingStates.INITIAL) {
console.error('Must be in new state before drawing') console.error("Must be in new state before drawing");
return Promise.resolve() return Promise.resolve();
} }
const {pdfPage} = this const { pdfPage } = this;
if (!pdfPage) { if (!pdfPage) {
this.renderingState = RenderingStates.FINISHED this.renderingState = RenderingStates.FINISHED;
return Promise.reject(new Error('pdfPage is not loaded')) return Promise.reject(new Error("pdfPage is not loaded"));
} }
this.renderingState = RenderingStates.RUNNING this.renderingState = RenderingStates.RUNNING;
const finishRenderTask = async (error = null) => { const finishRenderTask = async (error = null) => {
// The renderTask may have been replaced by a new one, so only remove // 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 // the reference to the renderTask if it matches the one that is
// triggering this callback. // triggering this callback.
if (renderTask === this.renderTask) { if (renderTask === this.renderTask) {
this.renderTask = null this.renderTask = null;
} }
if (error instanceof RenderingCancelledException) { if (error instanceof RenderingCancelledException) {
return return;
} }
this.renderingState = RenderingStates.FINISHED this.renderingState = RenderingStates.FINISHED;
this._convertCanvasToImage(canvas) this._convertCanvasToImage(canvas);
if (error) { if (error) {
throw error throw error;
}
} }
};
// Render the thumbnail at a larger size and downsize the canvas (similar // Render the thumbnail at a larger size and downsize the canvas (similar
// to `setImage`), to improve consistency between thumbnails created by // to `setImage`), to improve consistency between thumbnails created by
@ -303,79 +296,81 @@ class PDFThumbnailView {
// NOTE: To primarily avoid increasing memory usage too much, but also to // NOTE: To primarily avoid increasing memory usage too much, but also to
// reduce downsizing overhead, we purposely limit the up-scaling factor. // reduce downsizing overhead, we purposely limit the up-scaling factor.
const { ctx, canvas, transform } = const { ctx, canvas, transform } =
this._getPageDrawContext(DRAW_UPSCALE_FACTOR) this._getPageDrawContext(DRAW_UPSCALE_FACTOR);
const drawViewport = this.viewport.clone({ const drawViewport = this.viewport.clone({
scale: DRAW_UPSCALE_FACTOR * this.scale, scale: DRAW_UPSCALE_FACTOR * this.scale,
}) });
const renderContinueCallback = cont => { const renderContinueCallback = cont => {
if (!this.renderingQueue.isHighestPriority(this)) { if (!this.renderingQueue.isHighestPriority(this)) {
this.renderingState = RenderingStates.PAUSED this.renderingState = RenderingStates.PAUSED;
this.resume = () => { this.resume = () => {
this.renderingState = RenderingStates.RUNNING this.renderingState = RenderingStates.RUNNING;
cont() cont();
} };
return return;
}
cont()
} }
cont();
};
const renderContext = { const renderContext = {
canvasContext: ctx, canvasContext: ctx,
transform, transform,
viewport: drawViewport, viewport: drawViewport,
optionalContentConfigPromise: this._optionalContentConfigPromise, optionalContentConfigPromise: this._optionalContentConfigPromise,
} pageColors: this.pageColors,
const renderTask = (this.renderTask = pdfPage.render(renderContext)) };
renderTask.onContinue = renderContinueCallback const renderTask = (this.renderTask = pdfPage.render(renderContext));
renderTask.onContinue = renderContinueCallback;
const resultPromise = renderTask.promise.then( const resultPromise = renderTask.promise.then(
function () { function () {
return finishRenderTask(null) return finishRenderTask(null);
}, },
function (error) { function (error) {
return finishRenderTask(error) return finishRenderTask(error);
}, }
) );
resultPromise.finally(() => { resultPromise.finally(() => {
// Zeroing the width and height causes Firefox to release graphics // Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption. // resources immediately, which can greatly reduce memory consumption.
canvas.width = 0 canvas.width = 0;
canvas.height = 0 canvas.height = 0;
// Only trigger cleanup, once rendering has finished, when the current // Only trigger cleanup, once rendering has finished, when the current
// pageView is *not* cached on the `BaseViewer`-instance. // pageView is *not* cached on the `BaseViewer`-instance.
const pageCached = this.linkService.isPageCached(this.id) const pageCached = this.linkService.isPageCached(this.id);
if (!pageCached) { if (!pageCached) {
this.pdfPage?.cleanup() this.pdfPage?.cleanup();
} }
}) });
return resultPromise return resultPromise;
} }
setImage(pageView) { setImage(pageView) {
if (this._checkSetImageDisabled()) {
return
}
if (this.renderingState !== RenderingStates.INITIAL) { if (this.renderingState !== RenderingStates.INITIAL) {
return return;
} }
const {canvas, pdfPage} = pageView const { thumbnailCanvas: canvas, pdfPage, scale } = pageView;
if (!canvas) { if (!canvas) {
return return;
} }
if (!this.pdfPage) { if (!this.pdfPage) {
this.setPdfPage(pdfPage) this.setPdfPage(pdfPage);
} }
this.renderingState = RenderingStates.FINISHED if (scale < this.scale) {
this._convertCanvasToImage(canvas) // Avoid upscaling the image, since that makes the thumbnail look blurry.
return;
}
this.renderingState = RenderingStates.FINISHED;
this._convertCanvasToImage(canvas);
} }
/** /**
* @private * @private
*/ */
_reduceImage(img) { _reduceImage(img) {
const {ctx, canvas} = this._getPageDrawContext() const { ctx, canvas } = this._getPageDrawContext();
if (img.width <= 2 * canvas.width) { if (img.width <= 2 * canvas.width) {
ctx.drawImage( ctx.drawImage(
@ -387,21 +382,21 @@ class PDFThumbnailView {
0, 0,
0, 0,
canvas.width, canvas.width,
canvas.height, canvas.height
) );
return canvas return canvas;
} }
// drawImage does an awful job of rescaling the image, doing it gradually. // drawImage does an awful job of rescaling the image, doing it gradually.
let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas( const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas(
reducedWidth, reducedWidth,
reducedHeight, reducedHeight
) );
while (reducedWidth > img.width || reducedHeight > img.height) { while (reducedWidth > img.width || reducedHeight > img.height) {
reducedWidth >>= 1 reducedWidth >>= 1;
reducedHeight >>= 1 reducedHeight >>= 1;
} }
reducedImageCtx.drawImage( reducedImageCtx.drawImage(
img, img,
@ -412,8 +407,8 @@ class PDFThumbnailView {
0, 0,
0, 0,
reducedWidth, reducedWidth,
reducedHeight, reducedHeight
) );
while (reducedWidth > 2 * canvas.width) { while (reducedWidth > 2 * canvas.width) {
reducedImageCtx.drawImage( reducedImageCtx.drawImage(
reducedImage, reducedImage,
@ -424,10 +419,10 @@ class PDFThumbnailView {
0, 0,
0, 0,
reducedWidth >> 1, reducedWidth >> 1,
reducedHeight >> 1, reducedHeight >> 1
) );
reducedWidth >>= 1 reducedWidth >>= 1;
reducedHeight >>= 1 reducedHeight >>= 1;
} }
ctx.drawImage( ctx.drawImage(
reducedImage, reducedImage,
@ -438,17 +433,19 @@ class PDFThumbnailView {
0, 0,
0, 0,
canvas.width, canvas.width,
canvas.height, canvas.height
) );
return canvas return canvas;
} }
get _thumbPageTitle() { get _thumbPageTitle() {
// NOTE
return window.siyuan.languages.thumbPageTitle.replace('{{page}}', return window.siyuan.languages.thumbPageTitle.replace('{{page}}',
this.pageLabel ?? this.id) this.pageLabel ?? this.id)
} }
get _thumbPageCanvas() { get _thumbPageCanvas() {
// NOTE
return window.siyuan.languages.thumbPage.replace('{{page}}', return window.siyuan.languages.thumbPage.replace('{{page}}',
this.pageLabel ?? this.id) this.pageLabel ?? this.id)
} }
@ -457,16 +454,16 @@ class PDFThumbnailView {
* @param {string|null} label * @param {string|null} label
*/ */
setPageLabel(label) { setPageLabel(label) {
this.pageLabel = typeof label === 'string' ? label : null this.pageLabel = typeof label === "string" ? label : null;
// NOTE
this.anchor.title = this._thumbPageTitle this.anchor.title = this._thumbPageTitle
if (this.renderingState !== RenderingStates.FINISHED) { if (this.renderingState !== RenderingStates.FINISHED) {
return return;
} }
this.image?.setAttribute('aria-label', this._thumbPageCanvas) this.image?.setAttribute('aria-label', this._thumbPageCanvas)
} }
} }
export { PDFThumbnailView, TempImageFactory } export { PDFThumbnailView, TempImageFactory };

View file

@ -13,6 +13,13 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IL10n} IL10n */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
import { import {
getVisibleElements, getVisibleElements,
isValidRotation, isValidRotation,
@ -33,6 +40,9 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
* @property {IPDFLinkService} linkService - The navigation/linking service. * @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {IL10n} l10n - Localization service. * @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.
*/ */
/** /**
@ -42,20 +52,39 @@ class PDFThumbnailViewer {
/** /**
* @param {PDFThumbnailViewerOptions} options * @param {PDFThumbnailViewerOptions} options
*/ */
constructor({ container, eventBus, linkService, renderingQueue, l10n }) { constructor({
container,
eventBus,
linkService,
renderingQueue,
l10n,
pageColors,
}) {
this.container = container; this.container = container;
this.linkService = linkService; this.linkService = linkService;
this.renderingQueue = renderingQueue; this.renderingQueue = renderingQueue;
this.l10n = l10n; this.l10n = l10n;
this.pageColors = pageColors || null;
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.scroll = watchScroll(this.container, this._scrollUpdated.bind(this));
this._resetView(); this._resetView();
eventBus._on("optionalcontentconfigchanged", () => {
// Ensure that the thumbnails always render with the *default* optional
// content configuration.
this._setImageDisabled = true;
});
} }
/** /**
@ -144,12 +173,9 @@ class PDFThumbnailViewer {
} }
cleanup() { cleanup() {
for (let i = 0, ii = this._thumbnails.length; i < ii; i++) { for (const thumbnail of this._thumbnails) {
if ( if (thumbnail.renderingState !== RenderingStates.FINISHED) {
this._thumbnails[i] && thumbnail.reset();
this._thumbnails[i].renderingState !== RenderingStates.FINISHED
) {
this._thumbnails[i].reset();
} }
} }
TempImageFactory.destroyCanvas(); TempImageFactory.destroyCanvas();
@ -163,8 +189,6 @@ class PDFThumbnailViewer {
this._currentPageNumber = 1; this._currentPageNumber = 1;
this._pageLabels = null; this._pageLabels = null;
this._pagesRotation = 0; this._pagesRotation = 0;
this._optionalContentConfigPromise = null;
this._setImageDisabled = false;
// Remove the thumbnails from the DOM. // Remove the thumbnails from the DOM.
this.container.textContent = ""; this.container.textContent = "";
@ -188,13 +212,8 @@ class PDFThumbnailViewer {
firstPagePromise firstPagePromise
.then(firstPdfPage => { .then(firstPdfPage => {
this._optionalContentConfigPromise = optionalContentConfigPromise;
const pagesCount = pdfDocument.numPages; const pagesCount = pdfDocument.numPages;
const viewport = firstPdfPage.getViewport({ scale: 1 }); const viewport = firstPdfPage.getViewport({ scale: 1 });
const checkSetImageDisabled = () => {
return this._setImageDisabled;
};
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const thumbnail = new PDFThumbnailView({ const thumbnail = new PDFThumbnailView({
@ -204,18 +223,15 @@ class PDFThumbnailViewer {
optionalContentConfigPromise, optionalContentConfigPromise,
linkService: this.linkService, linkService: this.linkService,
renderingQueue: this.renderingQueue, renderingQueue: this.renderingQueue,
checkSetImageDisabled,
l10n: this.l10n, l10n: this.l10n,
pageColors: this.pageColors,
}); });
this._thumbnails.push(thumbnail); this._thumbnails.push(thumbnail);
} }
// Set the first `pdfPage` immediately, since it's already loaded, // Set the first `pdfPage` immediately, since it's already loaded,
// rather than having to repeat the `PDFDocumentProxy.getPage` call in // rather than having to repeat the `PDFDocumentProxy.getPage` call in
// the `this.#ensurePdfPageLoaded` method before rendering can start. // the `this.#ensurePdfPageLoaded` method before rendering can start.
const firstThumbnailView = this._thumbnails[0]; this._thumbnails[0]?.setPdfPage(firstPdfPage);
if (firstThumbnailView) {
firstThumbnailView.setPdfPage(firstPdfPage);
}
// Ensure that the current thumbnail is always highlighted on load. // Ensure that the current thumbnail is always highlighted on load.
const thumbnailView = this._thumbnails[this._currentPageNumber - 1]; const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
@ -230,10 +246,8 @@ class PDFThumbnailViewer {
* @private * @private
*/ */
_cancelRendering() { _cancelRendering() {
for (let i = 0, ii = this._thumbnails.length; i < ii; i++) { for (const thumbnail of this._thumbnails) {
if (this._thumbnails[i]) { thumbnail.cancelRendering();
this._thumbnails[i].cancelRendering();
}
} }
} }

View file

@ -24,17 +24,23 @@ import {
PDFLinkService, PDFLinkService,
SimpleLinkService, SimpleLinkService,
} from "./pdf_link_service.js"; } from "./pdf_link_service.js";
import { parseQueryString, ProgressBar } from "./ui_utils.js"; import {
import { PDFSinglePageViewer, PDFViewer } from "./pdf_viewer.js"; parseQueryString,
ProgressBar,
RenderingStates,
ScrollMode,
SpreadMode,
} from "./ui_utils.js";
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
import { DownloadManager } from "./download_manager.js"; import { DownloadManager } from "./download_manager.js";
import { EventBus } from "./event_utils.js"; import { EventBus } from "./event_utils.js";
import { GenericL10n } from "./genericl10n.js";
import { NullL10n } from "./l10n_utils.js"; import { NullL10n } from "./l10n_utils.js";
import { PDFFindController } from "./pdf_find_controller.js"; import { PDFFindController } from "./pdf_find_controller.js";
import { PDFHistory } from "./pdf_history.js"; import { PDFHistory } from "./pdf_history.js";
import { PDFPageView } from "./pdf_page_view.js"; import { PDFPageView } from "./pdf_page_view.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.js"; import { PDFScriptingManager } from "./pdf_scripting_manager.js";
import { PDFSinglePageViewer } from "./pdf_single_page_viewer.js";
import { PDFViewer } from "./pdf_viewer.js";
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
import { TextLayerBuilder } from "./text_layer_builder.js"; import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js"; import { XfaLayerBuilder } from "./xfa_layer_builder.js";
@ -52,7 +58,6 @@ export {
DefaultXfaLayerFactory, DefaultXfaLayerFactory,
DownloadManager, DownloadManager,
EventBus, EventBus,
GenericL10n,
LinkTarget, LinkTarget,
NullL10n, NullL10n,
parseQueryString, parseQueryString,
@ -64,7 +69,10 @@ export {
PDFSinglePageViewer, PDFSinglePageViewer,
PDFViewer, PDFViewer,
ProgressBar, ProgressBar,
RenderingStates,
ScrollMode,
SimpleLinkService, SimpleLinkService,
SpreadMode,
StructTreeLayerBuilder, StructTreeLayerBuilder,
TextLayerBuilder, TextLayerBuilder,
XfaLayerBuilder, XfaLayerBuilder,

File diff suppressed because it is too large Load diff

View file

@ -18,5 +18,5 @@
const {addScriptSync} = require('../../protyle/util/addScript') const {addScriptSync} = require('../../protyle/util/addScript')
const {Constants} = require('../../constants') const {Constants} = require('../../constants')
addScriptSync(`${Constants.PROTYLE_CDN}/js/pdf/pdf.js?v=2.14.102`, 'pdfjsScript') addScriptSync(`${Constants.PROTYLE_CDN}/js/pdf/pdf.js?v=3.0.150`, 'pdfjsScript')
module.exports = window["pdfjs-dist/build/pdf"]; module.exports = window["pdfjs-dist/build/pdf"];

View file

@ -13,18 +13,15 @@
* limitations under the License. * limitations under the License.
*/ */
import { SCROLLBAR_PADDING, ScrollMode, SpreadMode } from "./ui_utils.js"; import { ScrollMode, SpreadMode } from "./ui_utils.js";
import { CursorTool } from "./pdf_cursor_tools.js"; import { CursorTool } from "./pdf_cursor_tools.js";
import { PagesCountLimit } from "./base_viewer.js"; import { PagesCountLimit } from "./pdf_viewer.js";
/** /**
* @typedef {Object} SecondaryToolbarOptions * @typedef {Object} SecondaryToolbarOptions
* @property {HTMLDivElement} toolbar - Container for the secondary toolbar. * @property {HTMLDivElement} toolbar - Container for the secondary toolbar.
* @property {HTMLButtonElement} toggleButton - Button to toggle the visibility * @property {HTMLButtonElement} toggleButton - Button to toggle the visibility
* of the secondary toolbar. * of the secondary toolbar.
* @property {HTMLDivElement} toolbarButtonContainer - Container where all the
* toolbar buttons are placed. The maximum height of the toolbar is controlled
* dynamically by adjusting the 'max-height' CSS property of this DOM element.
* @property {HTMLButtonElement} presentationModeButton - Button for entering * @property {HTMLButtonElement} presentationModeButton - Button for entering
* presentation mode. * presentation mode.
* @property {HTMLButtonElement} openFileButton - Button to open a file. * @property {HTMLButtonElement} openFileButton - Button to open a file.
@ -52,24 +49,21 @@ import { PagesCountLimit } from "./base_viewer.js";
class SecondaryToolbar { class SecondaryToolbar {
/** /**
* @param {SecondaryToolbarOptions} options * @param {SecondaryToolbarOptions} options
* @param {HTMLDivElement} mainContainer
* @param {EventBus} eventBus * @param {EventBus} eventBus
*/ */
constructor(options, mainContainer, eventBus) { constructor(options, eventBus, externalServices) {
this.toolbar = options.toolbar; this.toolbar = options.toolbar;
this.toggleButton = options.toggleButton; this.toggleButton = options.toggleButton;
this.toolbarButtonContainer = options.toolbarButtonContainer;
this.buttons = [ this.buttons = [
{
element: options.presentationModeButton,
eventName: "presentationmode",
close: true,
},
// NOTE // NOTE
// {
// element: options.presentationModeButton,
// eventName: "presentationmode",
// close: true,
// },
// { element: options.openFileButton, eventName: "openfile", close: true },
// { element: options.printButton, eventName: "print", close: true }, // { element: options.printButton, eventName: "print", close: true },
// { element: options.downloadButton, eventName: "download", close: true }, // { element: options.downloadButton, eventName: "download", close: true },
// { element: options.viewBookmarkButton, eventName: null, close: true }, { element: options.viewBookmarkButton, eventName: null, close: true },
{ element: options.firstPageButton, eventName: "firstpage", close: true }, { element: options.firstPageButton, eventName: "firstpage", close: true },
{ element: options.lastPageButton, eventName: "lastpage", close: true }, { element: options.lastPageButton, eventName: "lastpage", close: true },
{ {
@ -142,6 +136,14 @@ class SecondaryToolbar {
close: true, close: true,
}, },
]; ];
// NOTE
// if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// this.buttons.push({
// element: options.openFileButton,
// eventName: "openfile",
// close: true,
// });
// }
this.items = { this.items = {
firstPage: options.firstPageButton, firstPage: options.firstPageButton,
lastPage: options.lastPageButton, lastPage: options.lastPageButton,
@ -149,14 +151,9 @@ class SecondaryToolbar {
pageRotateCcw: options.pageRotateCcwButton, pageRotateCcw: options.pageRotateCcwButton,
}; };
this.mainContainer = mainContainer;
this.eventBus = eventBus; this.eventBus = eventBus;
this.externalServices = externalServices;
this.opened = false; this.opened = false;
this.containerHeight = null;
this.previousContainerHeight = null;
this.reset();
// Bind the event listeners for click, cursor tool, and scroll/spread mode // Bind the event listeners for click, cursor tool, and scroll/spread mode
// actions. // actions.
@ -165,8 +162,7 @@ class SecondaryToolbar {
this.#bindScrollModeListener(options); this.#bindScrollModeListener(options);
this.#bindSpreadModeListener(options); this.#bindSpreadModeListener(options);
// Bind the event listener for adjusting the 'max-height' of the toolbar. this.reset();
this.eventBus._on("resize", this.#setMaxHeight.bind(this));
} }
/** /**
@ -219,6 +215,10 @@ class SecondaryToolbar {
if (close) { if (close) {
this.close(); this.close();
} }
this.externalServices.reportTelemetry({
type: "buttons",
data: { id: element.id },
});
}); });
} }
} }
@ -231,8 +231,8 @@ class SecondaryToolbar {
cursorSelectToolButton.classList.toggle("toggled", isSelect); cursorSelectToolButton.classList.toggle("toggled", isSelect);
cursorHandToolButton.classList.toggle("toggled", isHand); cursorHandToolButton.classList.toggle("toggled", isHand);
cursorSelectToolButton.setAttribute("aria-checked", `${isSelect}`); cursorSelectToolButton.setAttribute("aria-checked", isSelect);
cursorHandToolButton.setAttribute("aria-checked", `${isHand}`); cursorHandToolButton.setAttribute("aria-checked", isHand);
}); });
} }
@ -256,10 +256,10 @@ class SecondaryToolbar {
scrollHorizontalButton.classList.toggle("toggled", isHorizontal); scrollHorizontalButton.classList.toggle("toggled", isHorizontal);
scrollWrappedButton.classList.toggle("toggled", isWrapped); scrollWrappedButton.classList.toggle("toggled", isWrapped);
scrollPageButton.setAttribute("aria-checked", `${isPage}`); scrollPageButton.setAttribute("aria-checked", isPage);
scrollVerticalButton.setAttribute("aria-checked", `${isVertical}`); scrollVerticalButton.setAttribute("aria-checked", isVertical);
scrollHorizontalButton.setAttribute("aria-checked", `${isHorizontal}`); scrollHorizontalButton.setAttribute("aria-checked", isHorizontal);
scrollWrappedButton.setAttribute("aria-checked", `${isWrapped}`); scrollWrappedButton.setAttribute("aria-checked", isWrapped);
// Permanently *disable* the Scroll buttons when PAGE-scrolling is being // Permanently *disable* the Scroll buttons when PAGE-scrolling is being
// enforced for *very* long/large documents; please see the `BaseViewer`. // enforced for *very* long/large documents; please see the `BaseViewer`.
@ -299,9 +299,9 @@ class SecondaryToolbar {
spreadOddButton.classList.toggle("toggled", isOdd); spreadOddButton.classList.toggle("toggled", isOdd);
spreadEvenButton.classList.toggle("toggled", isEven); spreadEvenButton.classList.toggle("toggled", isEven);
spreadNoneButton.setAttribute("aria-checked", `${isNone}`); spreadNoneButton.setAttribute("aria-checked", isNone);
spreadOddButton.setAttribute("aria-checked", `${isOdd}`); spreadOddButton.setAttribute("aria-checked", isOdd);
spreadEvenButton.setAttribute("aria-checked", `${isEven}`); spreadEvenButton.setAttribute("aria-checked", isEven);
} }
this.eventBus._on("spreadmodechanged", spreadModeChanged); this.eventBus._on("spreadmodechanged", spreadModeChanged);
@ -317,10 +317,9 @@ class SecondaryToolbar {
return; return;
} }
this.opened = true; this.opened = true;
this.#setMaxHeight();
this.toggleButton.classList.add("toggled"); this.toggleButton.classList.add("toggled");
this.toggleButton.setAttribute("aria-expanded", "true"); this.toggleButton.setAttribute("aria-expanded", "true");
// NOTE
this.toolbar.classList.remove("fn__hidden"); this.toolbar.classList.remove("fn__hidden");
} }
@ -329,6 +328,7 @@ class SecondaryToolbar {
return; return;
} }
this.opened = false; this.opened = false;
// NOTE
this.toolbar.classList.add("fn__hidden"); this.toolbar.classList.add("fn__hidden");
this.toggleButton.classList.remove("toggled"); this.toggleButton.classList.remove("toggled");
this.toggleButton.setAttribute("aria-expanded", "false"); this.toggleButton.setAttribute("aria-expanded", "false");
@ -341,22 +341,6 @@ class SecondaryToolbar {
this.open(); this.open();
} }
} }
#setMaxHeight() {
if (!this.opened) {
return; // Only adjust the 'max-height' if the toolbar is visible.
}
this.containerHeight = this.mainContainer.clientHeight;
if (this.containerHeight === this.previousContainerHeight) {
return;
}
this.toolbarButtonContainer.style.maxHeight = `${
this.containerHeight - SCROLLBAR_PADDING
}px`;
this.previousContainerHeight = this.containerHeight;
}
} }
export { SecondaryToolbar }; export { SecondaryToolbar };

View file

@ -13,6 +13,8 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
const PDF_ROLE_TO_HTML_ROLE = { const PDF_ROLE_TO_HTML_ROLE = {
// Document level structure types // Document level structure types
Document: null, // There's a "document" role, but it doesn't make sense here. Document: null, // There's a "document" role, but it doesn't make sense here.
@ -126,7 +128,7 @@ class StructTreeLayerBuilder {
this._setAttributes(node.children[0], element); this._setAttributes(node.children[0], element);
} else { } else {
for (const kid of node.children) { for (const kid of node.children) {
element.appendChild(this._walk(kid)); element.append(this._walk(kid));
} }
} }
} }

View file

@ -13,6 +13,10 @@
* limitations under the License. * limitations under the License.
*/ */
/** @typedef {import("./event_utils").EventBus} EventBus */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */
/** /**
* @typedef {Object} TextHighlighterOptions * @typedef {Object} TextHighlighterOptions
* @property {PDFFindController} findController * @property {PDFFindController} findController
@ -172,8 +176,8 @@ class TextHighlighter {
let div = textDivs[divIdx]; let div = textDivs[divIdx];
if (div.nodeType === Node.TEXT_NODE) { if (div.nodeType === Node.TEXT_NODE) {
const span = document.createElement("span"); const span = document.createElement("span");
div.parentNode.insertBefore(span, div); div.before(span);
span.appendChild(div); span.append(div);
textDivs[divIdx] = span; textDivs[divIdx] = span;
div = span; div = span;
} }
@ -185,11 +189,11 @@ class TextHighlighter {
if (className) { if (className) {
const span = document.createElement("span"); const span = document.createElement("span");
span.className = `${className} appended`; span.className = `${className} appended`;
span.appendChild(node); span.append(node);
div.appendChild(span); div.append(span);
return className.includes("selected") ? span.offsetLeft : 0; return className.includes("selected") ? span.offsetLeft : 0;
} }
div.appendChild(node); div.append(node);
return 0; return 0;
} }

View file

@ -13,11 +13,16 @@
* limitations under the License. * limitations under the License.
*/ */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
// eslint-disable-next-line max-len
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
import { renderTextLayer } from "./pdfjs"; import { renderTextLayer } from "./pdfjs";
import { getHighlight } from '../anno' import { getHighlight } from '../anno'
const EXPAND_DIVS_TIMEOUT = 300; // ms
/** /**
* @typedef {Object} TextLayerBuilderOptions * @typedef {Object} TextLayerBuilderOptions
* @property {HTMLDivElement} textLayerDiv - The text layer container. * @property {HTMLDivElement} textLayerDiv - The text layer container.
@ -26,8 +31,7 @@ const EXPAND_DIVS_TIMEOUT = 300; // ms
* @property {PageViewport} viewport - The viewport of the text layer. * @property {PageViewport} viewport - The viewport of the text layer.
* @property {TextHighlighter} highlighter - Optional object that will handle * @property {TextHighlighter} highlighter - Optional object that will handle
* highlighting text from the find controller. * highlighting text from the find controller.
* @property {boolean} enhanceTextSelection - Option to turn on improved * @property {TextAccessibilityManager} [accessibilityManager]
* text selection.
*/ */
/** /**
@ -42,7 +46,7 @@ class TextLayerBuilder {
pageIndex, pageIndex,
viewport, viewport,
highlighter = null, highlighter = null,
enhanceTextSelection = false, accessibilityManager = null,
}) { }) {
this.textLayerDiv = textLayerDiv; this.textLayerDiv = textLayerDiv;
this.eventBus = eventBus; this.eventBus = eventBus;
@ -55,22 +59,17 @@ class TextLayerBuilder {
this.textDivs = []; this.textDivs = [];
this.textLayerRenderTask = null; this.textLayerRenderTask = null;
this.highlighter = highlighter; this.highlighter = highlighter;
this.enhanceTextSelection = enhanceTextSelection; this.accessibilityManager = accessibilityManager;
this._bindMouse(); this.#bindMouse();
} }
/** #finishRendering() {
* @private
*/
_finishRendering() {
this.renderingDone = true; this.renderingDone = true;
if (!this.enhanceTextSelection) {
const endOfContent = document.createElement("div"); const endOfContent = document.createElement("div");
endOfContent.className = "endOfContent"; endOfContent.className = "endOfContent";
this.textLayerDiv.appendChild(endOfContent); this.textLayerDiv.append(endOfContent);
}
this.eventBus.dispatch("textlayerrendered", { this.eventBus.dispatch("textlayerrendered", {
source: this, source: this,
@ -95,6 +94,7 @@ class TextLayerBuilder {
this.textDivs.length = 0; this.textDivs.length = 0;
this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr); this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
this.accessibilityManager?.setTextMapping(this.textDivs);
const textLayerFrag = document.createDocumentFragment(); const textLayerFrag = document.createDocumentFragment();
this.textLayerRenderTask = renderTextLayer({ this.textLayerRenderTask = renderTextLayer({
@ -105,13 +105,13 @@ class TextLayerBuilder {
textDivs: this.textDivs, textDivs: this.textDivs,
textContentItemsStr: this.textContentItemsStr, textContentItemsStr: this.textContentItemsStr,
timeout, timeout,
enhanceTextSelection: this.enhanceTextSelection,
}); });
this.textLayerRenderTask.promise.then( this.textLayerRenderTask.promise.then(
() => { () => {
this.textLayerDiv.appendChild(textLayerFrag); this.textLayerDiv.append(textLayerFrag);
this._finishRendering(); this.#finishRendering();
this.highlighter?.enable(); this.highlighter?.enable();
this.accessibilityManager?.enable();
}, },
function (reason) { function (reason) {
// Cancelled or failed to render text layer; skipping errors. // Cancelled or failed to render text layer; skipping errors.
@ -128,6 +128,7 @@ class TextLayerBuilder {
this.textLayerRenderTask = null; this.textLayerRenderTask = null;
} }
this.highlighter?.disable(); this.highlighter?.disable();
this.accessibilityManager?.disable();
} }
setTextContentStream(readableStream) { setTextContentStream(readableStream) {
@ -144,26 +145,11 @@ class TextLayerBuilder {
* Improves text selection by adding an additional div where the mouse was * Improves text selection by adding an additional div where the mouse was
* clicked. This reduces flickering of the content if the mouse is slowly * clicked. This reduces flickering of the content if the mouse is slowly
* dragged up or down. * dragged up or down.
*
* @private
*/ */
_bindMouse() { #bindMouse() {
const div = this.textLayerDiv; const div = this.textLayerDiv;
let expandDivsTimer = null;
div.addEventListener("mousedown", evt => { div.addEventListener("mousedown", evt => {
if (this.enhanceTextSelection && this.textLayerRenderTask) {
this.textLayerRenderTask.expandTextDivs(true);
if (
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
expandDivsTimer
) {
clearTimeout(expandDivsTimer);
expandDivsTimer = null;
}
return;
}
const end = div.querySelector(".endOfContent"); const end = div.querySelector(".endOfContent");
if (!end) { if (!end) {
return; return;
@ -175,11 +161,9 @@ class TextLayerBuilder {
// However it does not work when selection is started on empty space. // However it does not work when selection is started on empty space.
let adjustTop = evt.target !== div; let adjustTop = evt.target !== div;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
adjustTop = adjustTop &&=
adjustTop && getComputedStyle(end).getPropertyValue("-moz-user-select") !==
window "none";
.getComputedStyle(end)
.getPropertyValue("-moz-user-select") !== "none";
} }
if (adjustTop) { if (adjustTop) {
const divBounds = div.getBoundingClientRect(); const divBounds = div.getBoundingClientRect();
@ -191,20 +175,6 @@ class TextLayerBuilder {
}); });
div.addEventListener("mouseup", () => { div.addEventListener("mouseup", () => {
if (this.enhanceTextSelection && this.textLayerRenderTask) {
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
expandDivsTimer = setTimeout(() => {
if (this.textLayerRenderTask) {
this.textLayerRenderTask.expandTextDivs(false);
}
expandDivsTimer = null;
}, EXPAND_DIVS_TIMEOUT);
} else {
this.textLayerRenderTask.expandTextDivs(false);
}
return;
}
const end = div.querySelector(".endOfContent"); const end = div.querySelector(".endOfContent");
if (!end) { if (!end) {
return; return;

View file

@ -17,12 +17,14 @@ import {
animationStarted, animationStarted,
DEFAULT_SCALE, DEFAULT_SCALE,
DEFAULT_SCALE_VALUE, DEFAULT_SCALE_VALUE,
docStyle,
MAX_SCALE, MAX_SCALE,
MIN_SCALE, MIN_SCALE,
noContextMenuHandler, noContextMenuHandler,
} from './ui_utils.js' } from "./ui_utils.js";
import { AnnotationEditorType } from "./pdfjs";
const PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading' const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
/** /**
* @typedef {Object} ToolbarOptions * @typedef {Object} ToolbarOptions
@ -40,38 +42,60 @@ const PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading'
* @property {HTMLButtonElement} zoomOut - Button to zoom out the pages. * @property {HTMLButtonElement} zoomOut - Button to zoom out the pages.
* @property {HTMLButtonElement} viewFind - Button to open find bar. * @property {HTMLButtonElement} viewFind - Button to open find bar.
* @property {HTMLButtonElement} openFile - Button to open a new document. * @property {HTMLButtonElement} openFile - Button to open a new document.
* @property {HTMLButtonElement} presentationModeButton - Button to switch to * @property {HTMLButtonElement} editorFreeTextButton - Button to switch to
* presentation mode. * FreeText editing.
* @property {HTMLButtonElement} download - Button to download the document. * @property {HTMLButtonElement} download - Button to download the document.
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
* to the current location in the document.
*/ */
class Toolbar { class Toolbar {
#wasLocalized = false;
/** /**
* @param {ToolbarOptions} options * @param {ToolbarOptions} options
* @param {EventBus} eventBus * @param {EventBus} eventBus
* @param {IL10n} l10n - Localization service. * @param {IL10n} l10n - Localization service.
*/ */
constructor(options, eventBus, l10n) { constructor(options, eventBus, l10n) {
this.toolbar = options.container this.toolbar = options.container;
this.eventBus = eventBus this.eventBus = eventBus;
this.l10n = l10n this.l10n = l10n;
this.buttons = [ this.buttons = [
{element: options.previous, eventName: 'previouspage'}, { element: options.previous, eventName: "previouspage" },
{element: options.next, eventName: 'nextpage'}, { element: options.next, eventName: "nextpage" },
{element: options.zoomIn, eventName: 'zoomin'}, { element: options.zoomIn, eventName: "zoomin" },
{element: options.zoomOut, eventName: 'zoomout'}, { element: options.zoomOut, eventName: "zoomout" },
// NOTE // NOTE
// {element: options.openFile, eventName: 'openfile'}, // { element: options.print, eventName: "print" },
// {element: options.print, eventName: 'print'}, // { element: options.download, eventName: "download" },
{ // {
element: options.presentationModeButton, // element: options.editorFreeTextButton,
eventName: 'presentationmode', // eventName: "switchannotationeditormode",
}, // eventDetails: {
// {element: options.download, eventName: 'download'}, // get mode() {
{element: options.viewBookmark, eventName: null}, // const { classList } = options.editorFreeTextButton;
] // return classList.contains("toggled")
// ? AnnotationEditorType.NONE
// : AnnotationEditorType.FREETEXT;
// },
// },
// },
// {
// element: options.editorInkButton,
// eventName: "switchannotationeditormode",
// eventDetails: {
// get mode() {
// const { classList } = options.editorInkButton;
// return classList.contains("toggled")
// ? AnnotationEditorType.NONE
// : AnnotationEditorType.INK;
// },
// },
// },
];
// NOTE
// if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// this.buttons.push({ element: options.openFile, eventName: "openfile" });
// }
this.items = { this.items = {
numPages: options.numPages, numPages: options.numPages,
pageNumber: options.pageNumber, pageNumber: options.pageNumber,
@ -81,205 +105,251 @@ class Toolbar {
next: options.next, next: options.next,
zoomIn: options.zoomIn, zoomIn: options.zoomIn,
zoomOut: options.zoomOut, zoomOut: options.zoomOut,
} };
this._wasLocalized = false
this.reset()
// Bind the event listeners for click and various other actions. // Bind the event listeners for click and various other actions.
this._bindListeners() this.#bindListeners(options);
this.reset();
} }
setPageNumber(pageNumber, pageLabel) { setPageNumber(pageNumber, pageLabel) {
this.pageNumber = pageNumber this.pageNumber = pageNumber;
this.pageLabel = pageLabel this.pageLabel = pageLabel;
this._updateUIState(false) this.#updateUIState(false);
} }
setPagesCount(pagesCount, hasPageLabels) { setPagesCount(pagesCount, hasPageLabels) {
this.pagesCount = pagesCount this.pagesCount = pagesCount;
this.hasPageLabels = hasPageLabels this.hasPageLabels = hasPageLabels;
this._updateUIState(true) this.#updateUIState(true);
} }
setPageScale(pageScaleValue, pageScale) { setPageScale(pageScaleValue, pageScale) {
this.pageScaleValue = (pageScaleValue || pageScale).toString() this.pageScaleValue = (pageScaleValue || pageScale).toString();
this.pageScale = pageScale this.pageScale = pageScale;
this._updateUIState(false) this.#updateUIState(false);
} }
reset() { reset() {
this.pageNumber = 0 this.pageNumber = 0;
this.pageLabel = null this.pageLabel = null;
this.hasPageLabels = false this.hasPageLabels = false;
this.pagesCount = 0 this.pagesCount = 0;
this.pageScaleValue = DEFAULT_SCALE_VALUE this.pageScaleValue = DEFAULT_SCALE_VALUE;
this.pageScale = DEFAULT_SCALE this.pageScale = DEFAULT_SCALE;
this._updateUIState(true) this.#updateUIState(true);
this.updateLoadingIndicatorState() this.updateLoadingIndicatorState();
// Reset the Editor buttons too, since they're document specific.
this.eventBus.dispatch("toolbarreset", { source: this });
} }
_bindListeners () { #bindListeners(options) {
const {pageNumber, scaleSelect} = this.items const { pageNumber, scaleSelect } = this.items;
const self = this const self = this;
// The buttons within the toolbar. // The buttons within the toolbar.
for (const {element, eventName} of this.buttons) { for (const { element, eventName, eventDetails } of this.buttons) {
element.addEventListener('click', evt => { element.addEventListener("click", evt => {
if (eventName !== null) { if (eventName !== null) {
this.eventBus.dispatch(eventName, {source: this}) const details = { source: this };
if (eventDetails) {
for (const property in eventDetails) {
details[property] = eventDetails[property];
} }
}) }
this.eventBus.dispatch(eventName, details);
}
});
} }
// The non-button elements within the toolbar. // The non-button elements within the toolbar.
pageNumber.addEventListener('click', function () { pageNumber.addEventListener("click", function () {
this.select() this.select();
}) });
pageNumber.addEventListener('change', function () { pageNumber.addEventListener("change", function () {
self.eventBus.dispatch('pagenumberchanged', { self.eventBus.dispatch("pagenumberchanged", {
source: self, source: self,
value: this.value, value: this.value,
}) });
}) });
scaleSelect.addEventListener('change', function () { scaleSelect.addEventListener("change", function () {
if (this.value === 'custom') { if (this.value === "custom") {
return return;
} }
self.eventBus.dispatch('scalechanged', { self.eventBus.dispatch("scalechanged", {
source: self, source: self,
value: this.value, value: this.value,
}) });
}) });
// Here we depend on browsers dispatching the "click" event *after* the // Here we depend on browsers dispatching the "click" event *after* the
// "change" event, when the <select>-element changes. // "change" event, when the <select>-element changes.
scaleSelect.addEventListener('click', function (evt) { scaleSelect.addEventListener("click", function (evt) {
const target = evt.target const target = evt.target;
// Remove focus when an <option>-element was *clicked*, to improve the UX // Remove focus when an <option>-element was *clicked*, to improve the UX
// for mouse users (fixes bug 1300525 and issue 4923). // for mouse users (fixes bug 1300525 and issue 4923).
if ( if (
this.value === self.pageScaleValue && this.value === self.pageScaleValue &&
target.tagName.toUpperCase() === 'OPTION' target.tagName.toUpperCase() === "OPTION"
) { ) {
this.blur() this.blur();
} }
}) });
// Suppress context menus for some controls. // Suppress context menus for some controls.
scaleSelect.oncontextmenu = noContextMenuHandler scaleSelect.oncontextmenu = noContextMenuHandler;
this.eventBus._on('localized', () => { this.eventBus._on("localized", () => {
this._wasLocalized = true this.#wasLocalized = true;
this._adjustScaleWidth() this.#adjustScaleWidth();
this._updateUIState(true) this.#updateUIState(true);
}) });
// NOTE
// this.#bindEditorToolsListener(options);
} }
_updateUIState (resetNumPages = false) { #bindEditorToolsListener({
if (!this._wasLocalized) { 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,
},
];
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);
this.eventBus._on("toolbarreset", evt => {
if (evt.source === this) {
editorModeChanged(
{ mode: AnnotationEditorType.NONE },
/* disableButtons = */ true
);
}
});
}
#updateUIState(resetNumPages = false) {
if (!this.#wasLocalized) {
// Don't update the UI state until we localize the toolbar. // Don't update the UI state until we localize the toolbar.
return return;
} }
const {pageNumber, pagesCount, pageScaleValue, pageScale, items} = this const { pageNumber, pagesCount, pageScaleValue, pageScale, items } = this;
if (resetNumPages) { if (resetNumPages) {
if (this.hasPageLabels) { if (this.hasPageLabels) {
items.pageNumber.type = 'text' items.pageNumber.type = "text";
} else { } else {
items.pageNumber.type = 'number' items.pageNumber.type = "number";
items.numPages.textContent = "/ " + pagesCount; items.numPages.textContent = "/ " + pagesCount;
} }
items.pageNumber.max = pagesCount items.pageNumber.max = pagesCount;
} }
if (this.hasPageLabels) { if (this.hasPageLabels) {
items.pageNumber.value = this.pageLabel items.pageNumber.value = this.pageLabel;
items.numPages.textContent = `(${pageNumber} / ${pagesCount})` items.numPages.textContent = `(${pageNumber} / ${pagesCount})`
} else { } else {
items.pageNumber.value = pageNumber items.pageNumber.value = pageNumber;
} }
items.previous.disabled = pageNumber <= 1 items.previous.disabled = pageNumber <= 1;
items.next.disabled = pageNumber >= pagesCount items.next.disabled = pageNumber >= pagesCount;
items.zoomOut.disabled = pageScale <= MIN_SCALE items.zoomOut.disabled = pageScale <= MIN_SCALE;
items.zoomIn.disabled = pageScale >= MAX_SCALE items.zoomIn.disabled = pageScale >= MAX_SCALE;
let predefinedValueFound = false // NOTE
let predefinedValueFound = false;
for (const option of items.scaleSelect.options) { for (const option of items.scaleSelect.options) {
if (option.value !== pageScaleValue) { if (option.value !== pageScaleValue) {
option.selected = false option.selected = false;
continue continue;
} }
option.selected = true option.selected = true;
predefinedValueFound = true predefinedValueFound = true;
} }
if (!predefinedValueFound) { if (!predefinedValueFound) {
items.customScaleOption.textContent = `${Math.round(pageScale * 10000) / 100}%` items.customScaleOption.textContent = `${Math.round(pageScale * 10000) / 100}%`;
items.customScaleOption.selected = true items.customScaleOption.selected = true;
} }
} }
updateLoadingIndicatorState(loading = false) { updateLoadingIndicatorState(loading = false) {
const pageNumberInput = this.items.pageNumber const { pageNumber } = this.items;
pageNumberInput.classList.toggle(PAGE_NUMBER_LOADING_INDICATOR, loading) pageNumber.classList.toggle(PAGE_NUMBER_LOADING_INDICATOR, loading);
} }
/** /**
* Increase the width of the zoom dropdown DOM element if, and only if, it's * 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. * too narrow to fit the *longest* of the localized strings.
* @private
*/ */
async _adjustScaleWidth () { async #adjustScaleWidth() {
const {items, l10n} = this const { items, l10n } = this;
const style = getComputedStyle(items.scaleSelect), // NOTE
scaleSelectContainerWidth = parseInt( const predefinedValuesPromise = [
style.getPropertyValue('--scale-select-container-width'),
10,
),
scaleSelectOverflow = parseInt(
style.getPropertyValue('--scale-select-overflow'),
10,
)
// The temporary canvas is used to measure text length in the DOM.
let canvas = document.createElement('canvas')
if (
typeof PDFJSDev === 'undefined' ||
PDFJSDev.test('MOZCENTRAL || GENERIC')
) {
canvas.mozOpaque = true
}
let ctx = canvas.getContext('2d', {alpha: false})
await animationStarted
ctx.font = `${style.fontSize} ${style.fontFamily}`
let maxWidth = 0
for (const predefinedValue of [
window.siyuan.languages.pageScaleAuto, window.siyuan.languages.pageScaleAuto,
window.siyuan.languages.pageScaleActual, window.siyuan.languages.pageScaleActual,
window.siyuan.languages.pageScaleFit, window.siyuan.languages.pageScaleFit,
window.siyuan.languages.pageScaleWidth]) { window.siyuan.languages.pageScaleWidth];
const {width} = ctx.measureText(predefinedValue)
await animationStarted;
const style = getComputedStyle(items.scaleSelect),
scaleSelectContainerWidth = parseInt(
style.getPropertyValue("--scale-select-container-width"),
10
),
scaleSelectOverflow = parseInt(
style.getPropertyValue("--scale-select-overflow"),
10
);
// 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 predefinedValuesPromise) {
const { width } = ctx.measureText(predefinedValue);
if (width > maxWidth) { if (width > maxWidth) {
maxWidth = width maxWidth = width;
} }
} }
maxWidth += 2 * scaleSelectOverflow maxWidth += 2 * scaleSelectOverflow;
if (maxWidth > scaleSelectContainerWidth) { if (maxWidth > scaleSelectContainerWidth) {
const doc = document.documentElement docStyle.setProperty("--scale-select-container-width", `${maxWidth}px`);
doc.style.setProperty('--scale-select-container-width', `${maxWidth}px`)
} }
// Zeroing the width and height cause Firefox to release graphics resources // Zeroing the width and height cause Firefox to release graphics resources
// immediately, which can greatly reduce memory consumption. // immediately, which can greatly reduce memory consumption.
canvas.width = 0 canvas.width = 0;
canvas.height = 0 canvas.height = 0;
canvas = ctx = null
} }
} }
export { Toolbar } export { Toolbar };

View file

@ -23,8 +23,6 @@ const MAX_AUTO_SCALE = 1.25;
const SCROLLBAR_PADDING = 40; const SCROLLBAR_PADDING = 40;
const VERTICAL_PADDING = 5; const VERTICAL_PADDING = 5;
const LOADINGBAR_END_OFFSET_VAR = "--loadingBar-end-offset";
const RenderingStates = { const RenderingStates = {
INITIAL: 0, INITIAL: 0,
RUNNING: 1, RUNNING: 1,
@ -48,15 +46,17 @@ const SidebarView = {
LAYERS: 4, LAYERS: 4,
}; };
const RendererType = { const RendererType =
typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || GENERIC")
? {
CANVAS: "canvas", CANVAS: "canvas",
SVG: "svg", SVG: "svg",
}; }
: null;
const TextLayerMode = { const TextLayerMode = {
DISABLE: 0, DISABLE: 0,
ENABLE: 1, ENABLE: 1,
ENABLE_ENHANCE: 2,
}; };
const ScrollMode = { const ScrollMode = {
@ -590,7 +590,7 @@ function getVisibleElements({
} }
const first = visible[0], const first = visible[0],
last = visible[visible.length - 1]; last = visible.at(-1);
if (sortByVisibility) { if (sortByVisibility) {
visible.sort(function (a, b) { visible.sort(function (a, b) {
@ -679,49 +679,43 @@ const animationStarted = new Promise(function (resolve) {
window.requestAnimationFrame(resolve); window.requestAnimationFrame(resolve);
}); });
const docStyle =
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("LIB") &&
typeof document === "undefined"
? null
: document.documentElement.style;
function clamp(v, min, max) { function clamp(v, min, max) {
return Math.min(Math.max(v, min), max); return Math.min(Math.max(v, min), max);
} }
class ProgressBar { class ProgressBar {
constructor(element, { height, width, units } = {}) { #classList = null;
this.visible = true;
// Fetch the sub-elements for later. #percent = 0;
this.div = element.querySelector("#loadingBar .progress");
// Get the loading bar element, so it can be resized to fit the viewer.
this.bar = this.div.parentNode;
// Get options, with sensible defaults. #visible = true;
this.height = height || 100;
this.width = width || 100;
this.units = units || "%";
// Initialize heights. constructor(id) {
this.div.style.height = this.height + this.units; const bar = document.getElementById(id);
this.percent = 0; this.#classList = bar.classList;
}
_updateBar() {
if (this._indeterminate) {
this.div.classList.add("indeterminate");
this.div.style.width = this.width + this.units;
return;
}
this.div.classList.remove("indeterminate");
const progressSize = (this.width * this._percent) / 100;
this.div.style.width = progressSize + this.units;
} }
get percent() { get percent() {
return this._percent; return this.#percent;
} }
set percent(val) { set percent(val) {
this._indeterminate = isNaN(val); this.#percent = clamp(val, 0, 100);
this._percent = clamp(val, 0, 100);
this._updateBar(); if (isNaN(val)) {
this.#classList.add("indeterminate");
return;
}
this.#classList.remove("indeterminate");
docStyle.setProperty("--progressBar-percent", `${this.#percent}%`);
} }
setWidth(viewer) { setWidth(viewer) {
@ -731,25 +725,26 @@ class ProgressBar {
const container = viewer.parentNode; const container = viewer.parentNode;
const scrollbarWidth = container.offsetWidth - viewer.offsetWidth; const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
if (scrollbarWidth > 0) { if (scrollbarWidth > 0) {
const doc = document.documentElement; docStyle.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`);
doc.style.setProperty(LOADINGBAR_END_OFFSET_VAR, `${scrollbarWidth}px`);
} }
} }
hide() { hide() {
if (!this.visible) { if (!this.#visible) {
return; return;
} }
this.visible = false; this.#visible = false;
this.bar.classList.add("fn__hidden"); // NOTE
this.#classList.add("fn__hidden");
} }
show() { show() {
if (this.visible) { if (this.#visible) {
return; return;
} }
this.visible = true; this.#visible = true;
this.bar.classList.remove("fn__hidden"); // NOTE
this.#classList.remove("fn__hidden");
} }
} }
@ -844,6 +839,7 @@ export {
DEFAULT_SCALE, DEFAULT_SCALE,
DEFAULT_SCALE_DELTA, DEFAULT_SCALE_DELTA,
DEFAULT_SCALE_VALUE, DEFAULT_SCALE_VALUE,
docStyle,
getActiveOrFocusedElement, getActiveOrFocusedElement,
getPageSizeInches, getPageSizeInches,
getVisibleElements, getVisibleElements,

View file

@ -13,7 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { RenderingStates, ScrollMode, SpreadMode } from './ui_utils.js'
import { AppOptions } from './app_options.js' import { AppOptions } from './app_options.js'
import { LinkTarget } from './pdf_link_service.js'
import { PDFViewerApplication } from './app.js' import { PDFViewerApplication } from './app.js'
import { initAnno } from '../anno' import { initAnno } from '../anno'
@ -24,18 +26,22 @@ const pdfjsVersion =
const pdfjsBuild = const pdfjsBuild =
typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_BUILD') : void 0 typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_BUILD') : void 0
const AppConstants =
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? {LinkTarget, RenderingStates, ScrollMode, SpreadMode}
: null
window.PDFViewerApplication = PDFViewerApplication window.PDFViewerApplication = PDFViewerApplication
window.PDFViewerApplicationConstants = AppConstants
window.PDFViewerApplicationOptions = AppOptions window.PDFViewerApplicationOptions = AppOptions
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) { if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
var defaultUrl; // eslint-disable-line no-var
(function rewriteUrlClosure () { (function rewriteUrlClosure () {
// Run this code outside DOMContentLoaded to make sure that the URL // Run this code outside DOMContentLoaded to make sure that the URL
// is rewritten as soon as possible. // is rewritten as soon as possible.
const queryString = document.location.search.slice(1) const queryString = document.location.search.slice(1)
const m = /(^|&)file=([^&]*)/.exec(queryString) const m = /(^|&)file=([^&]*)/.exec(queryString)
defaultUrl = m ? decodeURIComponent(m[2]) : '' const defaultUrl = m ? decodeURIComponent(m[2]) : ''
// Example: chrome-extension://.../http://example.com/file.pdf // Example: chrome-extension://.../http://example.com/file.pdf
const humanReadableUrl = '/' + defaultUrl + location.hash const humanReadableUrl = '/' + defaultUrl + location.hash
@ -44,27 +50,34 @@ if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
chrome.runtime.sendMessage('showPageAction') chrome.runtime.sendMessage('showPageAction')
} }
AppOptions.set('defaultUrl', defaultUrl)
})() })()
} }
function getViewerConfiguration (element) { // NOTE
let errorWrapper = null // if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) {
if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { // require('./firefoxcom.js')
errorWrapper = { // require('./firefox_print_service.js')
container: element.querySelector('#errorWrapper'), // }
errorMessage: element.querySelector('#errorMessage'), // if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
closeButton: element.querySelector('#errorClose'), // require('./genericcom.js')
errorMoreInfo: element.querySelector('#errorMoreInfo'), // }
moreInfoButton: element.querySelector('#errorShowMore'), // if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
lessInfoButton: element.querySelector('#errorShowLess'), // require('./chromecom.js')
} // }
} // if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME || GENERIC')) {
// require('./pdf_print_service.js')
// }
// NOTE
function getViewerConfiguration (element) {
return { return {
appContainer: element, appContainer: element,
mainContainer: element.querySelector('#viewerContainer'), mainContainer: element.querySelector('#viewerContainer'),
viewerContainer: element.querySelector('#viewer'), viewerContainer: element.querySelector('#viewer'),
toolbar: { toolbar: {
// NOTE
rectAnno: element.querySelector('#rectAnno'), rectAnno: element.querySelector('#rectAnno'),
container: element.querySelector('#toolbarViewer'), container: element.querySelector('#toolbarViewer'),
numPages: element.querySelector('#numPages'), numPages: element.querySelector('#numPages'),
@ -76,23 +89,28 @@ function getViewerConfiguration (element) {
zoomIn: element.querySelector('#zoomIn'), zoomIn: element.querySelector('#zoomIn'),
zoomOut: element.querySelector('#zoomOut'), zoomOut: element.querySelector('#zoomOut'),
viewFind: element.querySelector('#viewFind'), viewFind: element.querySelector('#viewFind'),
openFile: element.querySelector('#openFile'), openFile:
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? element.querySelector('#openFile')
: null,
print: element.querySelector('#print'), print: element.querySelector('#print'),
presentationModeButton: element.querySelector('#presentationMode'), editorFreeTextButton: element.querySelector('#editorFreeText'),
editorFreeTextParamsToolbar: element.querySelector('#editorFreeTextParamsToolbar'),
editorInkButton: element.querySelector('#editorInk'),
editorInkParamsToolbar: element.querySelector('#editorInkParamsToolbar'),
download: element.querySelector('#download'), download: element.querySelector('#download'),
viewBookmark: element.querySelector('#viewBookmark'),
}, },
secondaryToolbar: { secondaryToolbar: {
toolbar: element.querySelector('#secondaryToolbar'), toolbar: element.querySelector('#secondaryToolbar'),
toggleButton: element.querySelector('#secondaryToolbarToggle'), toggleButton: element.querySelector('#secondaryToolbarToggle'),
toolbarButtonContainer: element.querySelector( presentationModeButton: element.querySelector('#presentationMode'),
'#secondaryToolbarButtonContainer'), openFileButton:
presentationModeButton: element.querySelector( typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
'#secondaryPresentationMode'), ? element.querySelector('#secondaryOpenFile')
openFileButton: element.querySelector('#secondaryOpenFile'), : null,
printButton: element.querySelector('#secondaryPrint'), printButton: element.querySelector('#secondaryPrint'),
downloadButton: element.querySelector('#secondaryDownload'), downloadButton: element.querySelector('#secondaryDownload'),
viewBookmarkButton: element.querySelector('#secondaryViewBookmark'), viewBookmarkButton: element.querySelector('#viewBookmark'),
firstPageButton: element.querySelector('#firstPage'), firstPageButton: element.querySelector('#firstPage'),
lastPageButton: element.querySelector('#lastPage'), lastPageButton: element.querySelector('#lastPage'),
pageRotateCwButton: element.querySelector('#pageRotateCw'), pageRotateCwButton: element.querySelector('#pageRotateCw'),
@ -111,7 +129,7 @@ function getViewerConfiguration (element) {
sidebar: { sidebar: {
// Divs (and sidebar button) // Divs (and sidebar button)
outerContainer: element.querySelector('#outerContainer'), outerContainer: element.querySelector('#outerContainer'),
viewerContainer: element.querySelector('#viewerContainer'), sidebarContainer: element.querySelector('#sidebarContainer'),
toggleButton: element.querySelector('#sidebarToggle'), toggleButton: element.querySelector('#sidebarToggle'),
// Buttons // Buttons
thumbnailButton: element.querySelector('#viewThumbnail'), thumbnailButton: element.querySelector('#viewThumbnail'),
@ -124,8 +142,7 @@ function getViewerConfiguration (element) {
attachmentsView: element.querySelector('#attachmentsView'), attachmentsView: element.querySelector('#attachmentsView'),
layersView: element.querySelector('#layersView'), layersView: element.querySelector('#layersView'),
// View-specific options // View-specific options
outlineOptionsContainer: element.querySelector( outlineOptionsContainer: element.querySelector('#outlineOptionsContainer'),
'#outlineOptionsContainer'),
currentOutlineItemButton: element.querySelector('#currentOutlineItem'), currentOutlineItemButton: element.querySelector('#currentOutlineItem'),
}, },
sidebarResizer: { sidebarResizer: {
@ -146,16 +163,14 @@ function getViewerConfiguration (element) {
findNextButton: element.querySelector('#findNext'), findNextButton: element.querySelector('#findNext'),
}, },
passwordOverlay: { passwordOverlay: {
overlayName: 'passwordOverlay', dialog: element.querySelector('#passwordDialog'),
container: element.querySelector('#passwordOverlay'),
label: element.querySelector('#passwordText'), label: element.querySelector('#passwordText'),
input: element.querySelector('#password'), input: element.querySelector('#password'),
submitButton: element.querySelector('#passwordSubmit'), submitButton: element.querySelector('#passwordSubmit'),
cancelButton: element.querySelector('#passwordCancel'), cancelButton: element.querySelector('#passwordCancel'),
}, },
documentProperties: { documentProperties: {
overlayName: 'documentPropertiesOverlay', dialog: element.querySelector('#documentPropertiesDialog'),
container: element.querySelector('#documentPropertiesOverlay'),
closeButton: element.querySelector('#documentPropertiesClose'), closeButton: element.querySelector('#documentPropertiesClose'),
fields: { fields: {
fileName: element.querySelector('#fileNameField'), fileName: element.querySelector('#fileNameField'),
@ -174,9 +189,29 @@ function getViewerConfiguration (element) {
linearized: element.querySelector('#linearizedField'), linearized: element.querySelector('#linearizedField'),
}, },
}, },
errorWrapper, annotationEditorParams: {
editorFreeTextFontSize: element.querySelector('#editorFreeTextFontSize'),
editorFreeTextColor: element.querySelector('#editorFreeTextColor'),
editorInkColor: element.querySelector('#editorInkColor'),
editorInkThickness: element.querySelector('#editorInkThickness'),
editorInkOpacity: element.querySelector('#editorInkOpacity'),
},
errorWrapper:
typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')
? {
container: element.querySelector('#errorWrapper'),
errorMessage: element.querySelector('#errorMessage'),
closeButton: element.querySelector('#errorClose'),
errorMoreInfo: element.querySelector('#errorMoreInfo'),
moreInfoButton: element.querySelector('#errorShowMore'),
lessInfoButton: element.querySelector('#errorShowLess'),
}
: null,
printContainer: element.querySelector('#printContainer'), printContainer: element.querySelector('#printContainer'),
openFileInputName: 'fileInput', openFileInput:
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? element.querySelector('#fileInput')
: null,
debuggerScriptPath: './debugger.js', debuggerScriptPath: './debugger.js',
} }
} }
@ -189,10 +224,6 @@ function webViewerLoad (file, element, pdfPage, annoId) {
if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')) { if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')) {
config.file = file config.file = file
} else { } else {
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
AppOptions.set('defaultUrl', defaultUrl)
}
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) { if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
// Give custom implementations of the default viewer a simpler way to // Give custom implementations of the default viewer a simpler way to
// set various `AppOptions`, by dispatching an event once all viewer // set various `AppOptions`, by dispatching an event once all viewer
@ -221,8 +252,20 @@ function webViewerLoad (file, element, pdfPage, annoId) {
// Block the "load" event until all pages are loaded, to ensure that printing // Block the "load" event until all pages are loaded, to ensure that printing
// works in Firefox; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553 // works in Firefox; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
if (document.blockUnblockOnload) {
document.blockUnblockOnload(true)
}
export { PDFViewerApplication, webViewerLoad } document.blockUnblockOnload?.(true)
// NOTE
// if (
// document.readyState === 'interactive' ||
// document.readyState === 'complete'
// ) {
// webViewerLoad()
// } else {
// document.addEventListener('DOMContentLoaded', webViewerLoad, true)
// }
// NOTE
export {
PDFViewerApplication,
webViewerLoad
}

View file

@ -13,6 +13,11 @@
* limitations under the License. * 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("./interfaces").IPDFLinkService} IPDFLinkService */
import { XfaLayer } from "./pdfjs"; import { XfaLayer } from "./pdfjs";
/** /**
@ -65,7 +70,7 @@ class XfaLayerBuilder {
// Create an xfa layer div and render the form // Create an xfa layer div and render the form
const div = document.createElement("div"); const div = document.createElement("div");
this.pageDiv.appendChild(div); this.pageDiv.append(div);
parameters.div = div; parameters.div = div;
const result = XfaLayer.render(parameters); const result = XfaLayer.render(parameters);
@ -94,7 +99,7 @@ class XfaLayerBuilder {
} }
// Create an xfa layer div and render the form // Create an xfa layer div and render the form
this.div = document.createElement("div"); this.div = document.createElement("div");
this.pageDiv.appendChild(this.div); this.pageDiv.append(this.div);
parameters.div = this.div; parameters.div = this.div;
return XfaLayer.render(parameters); return XfaLayer.render(parameters);
}) })

View file

@ -544,7 +544,7 @@ export class Wnd {
return; return;
} }
if (model instanceof Asset) { if (model instanceof Asset) {
if (model.pdfObject) { if (model.pdfObject && model.pdfObject.pdfLoadingTask) {
model.pdfObject.pdfLoadingTask.destroy(); model.pdfObject.pdfLoadingTask.destroy();
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long