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;
}
const canvasRect = pdf.pdfViewer._getCurrentVisiblePage().first.view.canvas.getBoundingClientRect();
const canvasRect = pdf.pdfViewer._getVisiblePages().first.view.canvas.getBoundingClientRect();
const containerRet = config.mainContainer.getBoundingClientRect();
const mostLeft = canvasRect.left;
const mostRight = canvasRect.right;

View file

@ -322,7 +322,7 @@ export class Asset extends Model {
</div>
</div>
<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="row">
<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 id="documentPropertiesOverlay" class="container fn__hidden">
<div id="documentPropertiesDialog" class="container fn__hidden">
<div class="dialog b3-menu">
<div class="row">
<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>
</button>
</div>
<div class="fn__none">
<input id="editorFreeTextFontSize">
<input id="editorFreeTextColor">
<input id="editorInkColor">
<input id="editorInkThickness">
<input id="editorInkOpacity">
</div>
</div> <!-- outerContainer -->
<div id="printContainer"></div>`;
// 初始化完成后需等待页签是否显示设置完成,才可以判断 pdf 是否能进行渲染

View file

@ -13,6 +13,15 @@
* 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 { NullL10n } from "./l10n_utils.js";
@ -33,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
* [fieldObjectsPromise]
* @property {Object} [mouseState]
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
* @property {TextAccessibilityManager} accessibilityManager
*/
class AnnotationLayerBuilder {
@ -53,6 +63,7 @@ class AnnotationLayerBuilder {
fieldObjectsPromise = null,
mouseState = null,
annotationCanvasMap = null,
accessibilityManager = null,
}) {
this.pageDiv = pageDiv;
this.pdfPage = pdfPage;
@ -67,6 +78,7 @@ class AnnotationLayerBuilder {
this._fieldObjectsPromise = fieldObjectsPromise;
this._mouseState = mouseState;
this._annotationCanvasMap = annotationCanvasMap;
this._accessibilityManager = accessibilityManager;
this.div = null;
this._cancelled = false;
@ -105,6 +117,7 @@ class AnnotationLayerBuilder {
fieldObjects,
mouseState: this._mouseState,
annotationCanvasMap: this._annotationCanvasMap,
accessibilityManager: this._accessibilityManager,
};
if (this.div) {
@ -116,7 +129,7 @@ class AnnotationLayerBuilder {
// if there is at least one annotation.
this.div = document.createElement("div");
this.div.className = "annotationLayer";
this.pageDiv.appendChild(this.div);
this.pageDiv.append(this.div);
parameters.div = this.div;
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)
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
const userAgent =
(typeof navigator !== 'undefined' && navigator.userAgent) || ''
const platform =
(typeof navigator !== 'undefined' && navigator.platform) || ''
const maxTouchPoints =
(typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1
if (
typeof PDFJSDev !== 'undefined' &&
PDFJSDev.test('LIB') &&
typeof navigator === 'undefined'
) {
globalThis.navigator = Object.create(null)
}
const userAgent = navigator.userAgent || ''
const platform = navigator.platform || ''
const maxTouchPoints = navigator.maxTouchPoints || 1
const isAndroid = /Android/.test(userAgent)
const isIOS =
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
(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
}
})();
(platform === 'MacIntel' && maxTouchPoints > 1);
// Limit canvas size to 5 mega-pixels on mobile.
// Support: Android, iOS
@ -62,9 +55,14 @@ const OptionKind = {
* primitive types and cannot rely on any imported types.
*/
const defaultOptions = {
annotationEditorMode: {
/** @type {number} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
annotationMode: {
/** @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,
},
cursorToolOnLoad: {
@ -72,11 +70,6 @@ const defaultOptions = {
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
defaultUrl: {
/** @type {string} */
value: 'compressed.tracemonkey-pldi-09.pdf',
kind: OptionKind.VIEWER,
},
defaultZoomValue: {
/** @type {string} */
value: '',
@ -135,9 +128,23 @@ const defaultOptions = {
maxCanvasPixels: {
/** @type {number} */
value: 16777216,
compatibility: compatibilityParams.maxCanvasPixels,
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: {
/** @type {boolean} */
value: typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION'),
@ -148,11 +155,6 @@ const defaultOptions = {
value: 150,
kind: OptionKind.VIEWER,
},
renderer: {
/** @type {string} */
value: 'canvas',
kind: OptionKind.VIEWER,
},
sidebarViewOnLoad: {
/** @type {number} */
value: -1,
@ -196,7 +198,10 @@ const defaultOptions = {
},
cMapUrl: {
/** @type {string} */
value: 'cmaps/',
value:
typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')
? '../external/bcmaps/'
: 'cmaps/', // NOTE
kind: OptionKind.API,
},
disableAutoFetch: {
@ -270,7 +275,8 @@ const defaultOptions = {
},
workerSrc: {
/** @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,
},
}
@ -278,6 +284,11 @@ if (
typeof PDFJSDev === 'undefined' ||
PDFJSDev.test('!PRODUCTION || GENERIC')
) {
defaultOptions.defaultUrl = {
/** @type {string} */
value: 'compressed.tracemonkey-pldi-09.pdf',
kind: OptionKind.VIEWER,
}
defaultOptions.disablePreferences = {
/** @type {boolean} */
value: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('TESTING'),
@ -285,9 +296,14 @@ if (
}
defaultOptions.locale = {
/** @type {string} */
value: typeof navigator !== 'undefined' ? navigator.language : 'en-US',
value: navigator.language || 'en-US',
kind: OptionKind.VIEWER,
}
defaultOptions.renderer = {
/** @type {string} */
value: 'canvas',
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}
defaultOptions.sandboxBundleSrc = {
/** @type {string} */
value:
@ -296,9 +312,12 @@ if (
: '../build/pdf.sandbox.js',
kind: OptionKind.VIEWER,
}
defaultOptions.renderer.kind += OptionKind.PREFERENCE
} else if (PDFJSDev.test('CHROME')) {
defaultOptions.defaultUrl = {
/** @type {string} */
value: '',
kind: OptionKind.VIEWER,
}
defaultOptions.disableTelemetry = {
/** @type {boolean} */
value: false,
@ -325,7 +344,7 @@ class AppOptions {
}
const defaultOption = defaultOptions[name]
if (defaultOption !== undefined) {
return defaultOption.compatibility ?? defaultOption.value
return compatibilityParams[name] ?? defaultOption.value
}
return undefined
}
@ -357,7 +376,7 @@ class AppOptions {
options[name] =
userOption !== undefined
? userOption
: defaultOption.compatibility ?? defaultOption.value
: compatibilityParams[name] ?? defaultOption.value
}
return options
}

View file

@ -13,7 +13,7 @@
* limitations under the License.
*/
import { removeNullCharacters } from "./ui_utils";
import { removeNullCharacters } from "./ui_utils.js";
const TREEITEM_OFFSET_TOP = -100; // px
const TREEITEM_SELECTED_CLASS = "selected";
@ -74,6 +74,7 @@ class BaseTreeViewer {
*/
_addToggleButton(div, hidden = false) {
const toggler = document.createElement("div");
// NOTE
toggler.innerHTML = `<svg><use xlink:href="#iconDown"></use></svg>`
toggler.className = "treeItemToggler";
if (hidden) {
@ -88,7 +89,7 @@ class BaseTreeViewer {
this._toggleTreeItem(div, shouldShowAll);
}
};
div.insertBefore(toggler, div.firstChild);
div.prepend(toggler);
}
/**
@ -123,7 +124,7 @@ class BaseTreeViewer {
this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden");
}
this.container.appendChild(fragment);
this.container.append(fragment);
this._dispatchEvent(count);
}
@ -168,7 +169,7 @@ class BaseTreeViewer {
this.container.scrollTo(
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.
*/
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
import { createValidAbsoluteUrl, isPdfFile } from "./pdfjs";
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,
// otherwise .click() is ignored.
(document.body || document.documentElement).appendChild(a);
(document.body || document.documentElement).append(a);
a.click();
a.remove();
}
@ -107,13 +109,7 @@ class DownloadManager {
return false;
}
/**
* @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") {
download(blob, url, filename) {
const blobUrl = URL.createObjectURL(blob);
download(blobUrl, filename);
}

View file

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

View file

@ -153,7 +153,7 @@ class GrabToPan {
this.element.scrollLeft = scrollLeft;
}
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.",
web_fonts_disabled:
"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) {

View file

@ -14,123 +14,103 @@
*/
class OverlayManager {
#overlays = Object.create(null);
#overlays = new WeakMap();
#active = null;
#keyDownBound = null;
get active() {
return this.#active;
}
/**
* @param {string} name - The name of the overlay that is registered.
* @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 {HTMLDialogElement} dialog - The overlay's DOM element.
* @param {boolean} [canForceClose] - Indicates if opening the overlay closes
* an active overlay. The default is `false`.
* @returns {Promise} A promise that is resolved when the overlay has been
* registered.
*/
async register(
name,
element,
callerCloseMethod = null,
canForceClose = false
) {
let container;
if (!name || !element || !(container = element.parentNode)) {
async register(dialog, canForceClose = false) {
if (typeof dialog !== "object") {
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.");
}
this.#overlays[name] = {
element,
container,
callerCloseMethod,
canForceClose,
};
this.#overlays.set(dialog, { canForceClose });
// NOTE
// if (
// typeof PDFJSDev !== "undefined" &&
// PDFJSDev.test("GENERIC && !SKIP_BABEL") &&
// !dialog.showModal
// ) {
// const dialogPolyfill = require("dialog-polyfill/dist/dialog-polyfill.js");
// dialogPolyfill.registerDialog(dialog);
//
// if (!this._dialogPolyfillCSS) {
// this._dialogPolyfillCSS = true;
//
// const style = document.createElement("style");
// style.textContent = PDFJSDev.eval("DIALOG_POLYFILL_CSS");
//
// document.head.prepend(style);
// }
// }
dialog.addEventListener("cancel", evt => {
this.#active = null;
});
}
/**
* @param {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
* unregistered.
*/
async unregister(name) {
if (!this.#overlays[name]) {
async unregister(dialog) {
if (!this.#overlays.has(dialog)) {
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.");
}
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
* opened.
*/
async open(name) {
if (!this.#overlays[name]) {
async open(dialog) {
if (!this.#overlays.has(dialog)) {
throw new Error("The overlay does not exist.");
} else if (this.#active) {
if (this.#active === name) {
if (this.#active === dialog) {
throw new Error("The overlay is already active.");
} else if (this.#overlays[name].canForceClose) {
this.#closeThroughCaller();
} else if (this.#overlays.get(dialog).canForceClose) {
await this.close();
} else {
throw new Error("Another overlay is currently active.");
}
}
this.#active = name;
this.#overlays[this.#active].element.classList.remove("fn__hidden");
this.#overlays[this.#active].container.classList.remove("fn__hidden");
this.#keyDownBound = this.#keyDown.bind(this);
window.addEventListener("keydown", this.#keyDownBound);
this.#active = dialog;
dialog.showModal();
}
/**
* @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
* closed.
*/
async close(name) {
if (!this.#overlays[name]) {
async close(dialog = this.#active) {
if (!this.#overlays.has(dialog)) {
throw new Error("The overlay does not exist.");
} else if (!this.#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.");
}
this.#overlays[this.#active].container.classList.add("fn__hidden");
this.#overlays[this.#active].element.classList.add("fn__hidden");
dialog.close();
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.
*/
import { PasswordResponses } from "./pdfjs";
import { createPromiseCapability, PasswordResponses } from "./pdfjs";
/**
* @typedef {Object} PasswordPromptOptions
* @property {string} overlayName - Name of the overlay for the overlay manager.
* @property {HTMLDivElement} container - Div container for the overlay.
* @property {HTMLDialogElement} dialog - The overlay's DOM element.
* @property {HTMLParagraphElement} label - Label containing instructions for
* entering the password.
* @property {HTMLInputElement} input - Input field for entering the password.
@ -29,6 +28,12 @@ import { PasswordResponses } from "./pdfjs";
*/
class PasswordPrompt {
#activeCapability = null;
#updateCallback = null;
#reason = null;
/**
* @param {PasswordPromptOptions} options
* @param {OverlayManager} overlayManager - Manager for the viewer overlays.
@ -37,8 +42,7 @@ class PasswordPrompt {
* an <iframe> or an <object>. The default value is `false`.
*/
constructor(options, overlayManager, l10n, isViewerEmbedded = false) {
this.overlayName = options.overlayName;
this.container = options.container;
this.dialog = options.dialog;
this.label = options.label;
this.input = options.input;
this.submitButton = options.submitButton;
@ -47,61 +51,80 @@ class PasswordPrompt {
this.l10n = l10n;
this._isViewerEmbedded = isViewerEmbedded;
this.updateCallback = null;
this.reason = null;
// Attach the event listeners.
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 => {
if (e.keyCode === /* Enter = */ 13) {
this.#verify();
}
});
this.overlayManager.register(
this.overlayName,
this.container,
this.#cancel.bind(this),
true
);
this.overlayManager.register(this.dialog, /* canForceClose = */ true);
this.dialog.addEventListener("close", this.#cancel.bind(this));
}
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 =
this.reason === PasswordResponses.INCORRECT_PASSWORD;
this.#reason === PasswordResponses.INCORRECT_PASSWORD;
if (!this._isViewerEmbedded || passwordIncorrect) {
this.input.focus();
}
// NOTE
this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect
? 'invalid'
: 'label'}`]
}
async close() {
await this.overlayManager.close(this.overlayName);
this.input.value = "";
if (this.overlayManager.active === this.dialog) {
this.overlayManager.close(this.dialog);
}
}
#verify() {
const password = this.input.value;
if (password?.length > 0) {
this.close();
this.updateCallback(password);
this.#invokeCallback(password);
}
}
#cancel() {
this.close();
this.updateCallback(new Error("PasswordPrompt cancelled."));
this.#invokeCallback(new Error("PasswordPrompt cancelled."));
this.#activeCapability.resolve();
}
setUpdateCallback(updateCallback, reason) {
this.updateCallback = updateCallback;
this.reason = reason;
#invokeCallback(password) {
if (!this.#updateCallback) {
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 { BaseTreeViewer } from "./base_tree_viewer.js";
import { waitOnEventOrTimeout } from "./event_utils.js";
/**
* @typedef {Object} PDFAttachmentViewerOptions
@ -38,7 +39,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
this.eventBus._on(
"fileattachmentannotation",
this._appendAttachment.bind(this)
this.#appendAttachment.bind(this)
);
}
@ -51,36 +52,33 @@ class PDFAttachmentViewer extends BaseTreeViewer {
// replaced is when appending FileAttachment annotations.
this._renderedCapability = createPromiseCapability();
}
if (this._pendingDispatchEvent) {
clearTimeout(this._pendingDispatchEvent);
}
this._pendingDispatchEvent = null;
this._pendingDispatchEvent = false;
}
/**
* @private
*/
_dispatchEvent(attachmentsCount) {
async _dispatchEvent(attachmentsCount) {
this._renderedCapability.resolve();
if (this._pendingDispatchEvent) {
clearTimeout(this._pendingDispatchEvent);
this._pendingDispatchEvent = null;
}
if (attachmentsCount === 0) {
if (attachmentsCount === 0 && !this._pendingDispatchEvent) {
// Delay the event when no "regular" attachments exist, to allow time for
// parsing of any FileAttachment annotations that may be present on the
// *initially* rendered page; this reduces the likelihood of temporarily
// disabling the attachmentsView when the `PDFSidebar` handles the event.
this._pendingDispatchEvent = setTimeout(() => {
this.eventBus.dispatch("attachmentsloaded", {
source: this,
attachmentsCount: 0,
this._pendingDispatchEvent = true;
await waitOnEventOrTimeout({
target: this.eventBus,
name: "annotationlayerrendered",
delay: 1000,
});
this._pendingDispatchEvent = null;
});
return;
if (!this._pendingDispatchEvent) {
return; // There was already another `_dispatchEvent`-call`.
}
}
this._pendingDispatchEvent = false;
this.eventBus.dispatch("attachmentsloaded", {
source: this,
@ -129,9 +127,9 @@ class PDFAttachmentViewer extends BaseTreeViewer {
this._bindLink(element, { content, filename });
element.textContent = this._normalizeTextContent(filename);
div.appendChild(element);
div.append(element);
fragment.appendChild(div);
fragment.append(div);
attachmentsCount++;
}
@ -140,27 +138,22 @@ class PDFAttachmentViewer extends BaseTreeViewer {
/**
* Used to append FileAttachment annotations to the sidebar.
* @private
*/
_appendAttachment({ id, filename, content }) {
#appendAttachment({ filename, content }) {
const renderedPromise = this._renderedCapability.promise;
renderedPromise.then(() => {
if (renderedPromise !== this._renderedCapability.promise) {
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) {
if (id === name) {
if (filename === name) {
return; // Ignore the new attachment if it already exists.
}
}
}
attachments[id] = {
attachments[filename] = {
filename,
content,
};

View file

@ -13,6 +13,7 @@
* limitations under the License.
*/
import { AnnotationEditorType } from "./pdfjs";
import { GrabToPan } from "./grab_to_pan.js";
import { PresentationModeState } from "./ui_utils.js";
@ -40,7 +41,7 @@ class PDFCursorTools {
this.eventBus = eventBus;
this.active = CursorTool.SELECT;
this.activeBeforePresentationMode = null;
this.previouslyActive = null;
this.handTool = new GrabToPan({
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,
* must be one of the values in {CursorTool}.
*/
switchTool(tool) {
if (this.activeBeforePresentationMode !== null) {
return; // Cursor tools cannot be used in Presentation Mode.
if (this.previouslyActive !== null) {
// Cursor tools cannot be used in PresentationMode/AnnotationEditor.
return;
}
if (tool === this.active) {
return; // The requested tool is already active.
@ -121,22 +122,54 @@ class PDFCursorTools {
this.switchTool(evt.tool);
});
this.eventBus._on("presentationmodechanged", evt => {
switch (evt.state) {
case PresentationModeState.FULLSCREEN: {
let annotationEditorMode = AnnotationEditorType.NONE,
presentationModeState = PresentationModeState.NORMAL;
const disableActive = () => {
const previouslyActive = this.active;
this.switchTool(CursorTool.SELECT);
this.activeBeforePresentationMode = previouslyActive;
break;
}
case PresentationModeState.NORMAL: {
const previouslyActive = this.activeBeforePresentationMode;
this.previouslyActive ??= previouslyActive; // Keep track of the first one.
};
const enableActive = () => {
const previouslyActive = this.previouslyActive;
this.activeBeforePresentationMode = null;
if (
previouslyActive !== null &&
annotationEditorMode === AnnotationEditorType.NONE &&
presentationModeState === PresentationModeState.NORMAL
) {
this.previouslyActive = null;
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.
*/
import {
createPromiseCapability,
getPdfFilenameFromUrl,
PDFDateString,
} from "./pdfjs";
import { createPromiseCapability, PDFDateString } from "./pdfjs";
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
const DEFAULT_FIELD_CONTENT = "-";
@ -45,9 +41,8 @@ function getPageName(size, isPortrait, pageNames) {
/**
* @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 {HTMLDivElement} container - Div container for 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 {EventBus} eventBus - The application event bus.
* @param {IL10n} l10n - Localization service.
* @param {function} fileNameLookup - The function that is used to lookup
* the document fileName.
*/
constructor(
{ overlayName, fields, container, closeButton },
{ dialog, fields, closeButton },
overlayManager,
eventBus,
l10n
l10n,
fileNameLookup
) {
this.overlayName = overlayName;
this.dialog = dialog;
this.fields = fields;
this.container = container;
this.overlayManager = overlayManager;
this.l10n = l10n;
this._fileNameLookup = fileNameLookup;
this.#reset();
// Bind the event listener for the Close button.
closeButton.addEventListener("click", this.close.bind(this));
this.overlayManager.register(
this.overlayName,
this.container,
this.close.bind(this)
);
this.overlayManager.register(this.dialog);
eventBus._on("pagechanging", evt => {
this._currentPageNumber = evt.pageNumber;
@ -90,6 +84,10 @@ class PDFDocumentProperties {
});
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() {
await Promise.all([
this.overlayManager.open(this.overlayName),
this.overlayManager.open(this.dialog),
this._dataAvailableCapability.promise,
]);
const currentPageNumber = this._currentPageNumber;
@ -118,7 +116,7 @@ class PDFDocumentProperties {
const {
info,
/* metadata, */
contentDispositionFilename,
/* contentDispositionFilename, */
contentLength,
} = await this.pdfDocument.getMetadata();
@ -130,7 +128,7 @@ class PDFDocumentProperties {
pageSize,
isLinearized,
] = await Promise.all([
contentDispositionFilename || getPdfFilenameFromUrl(this.url),
this._fileNameLookup(),
this.#parseFileSize(contentLength),
this.#parseDate(info.CreationDate),
this.#parseDate(info.ModDate),
@ -176,20 +174,18 @@ class PDFDocumentProperties {
/**
* Close the document properties overlay.
*/
close() {
this.overlayManager.close(this.overlayName);
async close() {
this.overlayManager.close(this.dialog);
}
/**
* Set a reference to the PDF document and the URL in order
* to populate the overlay fields with the document properties.
* Note that the overlay will contain no information if this method
* is not called.
* Set a reference to the PDF document in order to populate the dialog fields
* with the document properties. Note that the dialog will contain no
* information if this method is not called.
*
* @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) {
this.#reset();
this.#updateUI(true);
@ -198,14 +194,12 @@ class PDFDocumentProperties {
return;
}
this.pdfDocument = pdfDocument;
this.url = url;
this._dataAvailableCapability.resolve();
}
#reset() {
this.pdfDocument = null;
this.url = null;
this.#fieldData = null;
this._dataAvailableCapability = createPromiseCapability();
@ -225,7 +219,7 @@ class PDFDocumentProperties {
}
return;
}
if (this.overlayManager.active !== this.overlayName) {
if (this.overlayManager.active !== this.dialog) {
// Don't bother updating the dialog if has already been closed,
// since it will be updated the next time `this.open` is called.
return;
@ -243,6 +237,7 @@ class PDFDocumentProperties {
if (!kb) {
return undefined;
}
// NOTE
if (mb >= 1) {
return `${mb >= 1 && (+mb.toPrecision(
3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes)`
@ -315,6 +310,7 @@ class PDFDocumentProperties {
}
}
// NOTE
const [{ width, height }, unit, name, orientation] = await Promise.all([
this._isNonMetricLocale ? sizeInches : sizeMillimeters,
this._isNonMetricLocale
@ -337,10 +333,12 @@ class PDFDocumentProperties {
if (!dateObject) {
return undefined;
}
// NOTE
return `${dateObject.toLocaleDateString()}, ${dateObject.toLocaleTimeString()}`
}
#parseLinearization(isLinearized) {
// NOTE
return isLinearized ? 'Yes' : 'No'
}
}

View file

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

View file

@ -13,6 +13,10 @@
* 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 { createPromiseCapability } from "./pdfjs";
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_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) {
// 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)
// 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.
const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join("");
normalizationRegex = new RegExp(
`([${replace}])|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(\\n)`,
const regexp = `([${replace}])|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(\\p{Ideographic}\\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"
);
} else {
normalizationRegex = withSyllablesRegExp = new RegExp(
regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`,
"gum"
);
}
}
// The goal of this function is to normalize the string and
@ -126,14 +173,14 @@ function normalize(text) {
// Collect diacritics length and positions.
const rawDiacriticsPositions = [];
let m;
while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) {
rawDiacriticsPositions.push([m[0].length, m.index]);
}
let normalized = text.normalize("NFD");
const positions = [[0, 0]];
let k = 0;
let rawDiacriticsIndex = 0;
let syllableIndex = 0;
let shift = 0;
let shiftOrigin = 0;
let eol = 0;
@ -141,7 +188,7 @@ function normalize(text) {
normalized = normalized.replace(
normalizationRegex,
(match, p1, p2, p3, p4, i) => {
(match, p1, p2, p3, p4, p5, p6, i) => {
i -= shiftOrigin;
if (p1) {
// Maybe fractions or quotations mark...
@ -161,12 +208,12 @@ function normalize(text) {
// Diacritics.
hasDiacritics = true;
let jj = len;
if (i + eol === rawDiacriticsPositions[k]?.[1]) {
jj -= rawDiacriticsPositions[k][0];
++k;
if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) {
jj -= rawDiacriticsPositions[rawDiacriticsIndex][0];
++rawDiacriticsIndex;
}
for (let j = 1; j < jj + 1; j++) {
for (let j = 1; j <= jj; j++) {
// i is the position of the first diacritic
// so (i - 1) is the position for the letter before.
positions.push([i - 1 - shift + j, shift - j]);
@ -200,7 +247,16 @@ function normalize(text) {
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
// "foo bar".
positions.push([i - shift + 1, shift - 1]);
@ -209,6 +265,21 @@ function normalize(text) {
eol += 1;
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]);

View file

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

View file

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

View file

@ -13,6 +13,9 @@
* limitations under the License.
*/
/** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
import { parseQueryString, removeNullCharacters } from "./ui_utils.js";
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 {Object} pageRef - reference to the page.
@ -673,6 +718,11 @@ class SimpleLinkService {
*/
executeNamedAction(action) {}
/**
* @param {Object} action
*/
executeSetOCGState(action) {}
/**
* @param {number} pageNum - page number.
* @param {Object} pageRef - reference to the page.

View file

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

View file

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

View file

@ -19,8 +19,8 @@ import {
ScrollMode,
SpreadMode,
} 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 ACTIVE_SELECTOR = "pdfPresentationMode";
const CONTROLS_SELECTOR = "pdfPresentationModeControls";
@ -42,6 +42,10 @@ const SWIPE_ANGLE_THRESHOLD = Math.PI / 6;
*/
class PDFPresentationMode {
#state = PresentationModeState.UNKNOWN;
#args = null;
/**
* @param {PDFPresentationModeOptions} options
*/
@ -50,8 +54,6 @@ class PDFPresentationMode {
this.pdfViewer = pdfViewer;
this.eventBus = eventBus;
this.active = false;
this.args = null;
this.contextMenuOpen = false;
this.mouseScrollTimeStamp = 0;
this.mouseScrollDelta = 0;
@ -60,37 +62,63 @@ class PDFPresentationMode {
/**
* 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() {
if (
this.switchInProgress ||
this.active ||
!this.pdfViewer.pagesCount ||
!this.container.requestFullscreen
) {
async request() {
const { container, pdfViewer } = this;
if (this.active || !pdfViewer.pagesCount || !container.requestFullscreen) {
return false;
}
this.#addFullscreenChangeListeners();
this.#setSwitchInProgress();
this.#notifyStateChange();
this.#notifyStateChange(PresentationModeState.CHANGING);
this.container.requestFullscreen();
const promise = container.requestFullscreen();
this.args = {
pageNumber: this.pdfViewer.currentPageNumber,
scaleValue: this.pdfViewer.currentScaleValue,
scrollMode: this.pdfViewer.scrollMode,
spreadMode: this.pdfViewer.spreadMode,
this.#args = {
pageNumber: pdfViewer.currentPageNumber,
scaleValue: pdfViewer.currentScaleValue,
scrollMode: pdfViewer.scrollMode,
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;
} catch (reason) {
this.#removeFullscreenChangeListeners();
this.#notifyStateChange(PresentationModeState.NORMAL);
}
return false;
}
get active() {
return (
this.#state === PresentationModeState.CHANGING ||
this.#state === PresentationModeState.FULLSCREEN
);
}
#mouseWheel(evt) {
if (!this.active) {
return;
}
evt.preventDefault();
const delta = normalizeWheelEventDelta(evt);
@ -126,57 +154,29 @@ class PDFPresentationMode {
}
}
#notifyStateChange() {
let state = PresentationModeState.NORMAL;
if (this.switchInProgress) {
state = PresentationModeState.CHANGING;
} else if (this.active) {
state = PresentationModeState.FULLSCREEN;
}
this.eventBus.dispatch("presentationmodechanged", {
source: this,
state,
});
}
#notifyStateChange(state) {
this.#state = 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;
}
this.eventBus.dispatch("presentationmodechanged", { source: this, state });
}
#enter() {
this.active = true;
this.#resetSwitchInProgress();
this.#notifyStateChange();
this.#notifyStateChange(PresentationModeState.FULLSCREEN);
this.container.classList.add(ACTIVE_SELECTOR);
// Ensure that the correct page is scrolled into view when entering
// Presentation Mode, by waiting until fullscreen mode in enabled.
setTimeout(() => {
this.pdfViewer.scrollMode = ScrollMode.PAGE;
if (this.#args.spreadMode !== null) {
this.pdfViewer.spreadMode = SpreadMode.NONE;
this.pdfViewer.currentPageNumber = this.args.pageNumber;
}
this.pdfViewer.currentPageNumber = this.#args.pageNumber;
this.pdfViewer.currentScaleValue = "page-fit";
if (this.#args.annotationEditorMode !== null) {
this.pdfViewer.annotationEditorMode = AnnotationEditorType.NONE;
}
}, 0);
this.#addWindowListeners();
@ -196,15 +196,20 @@ class PDFPresentationMode {
// Ensure that the correct page is scrolled into view when exiting
// Presentation Mode, by waiting until fullscreen mode is disabled.
setTimeout(() => {
this.active = false;
this.#removeFullscreenChangeListeners();
this.#notifyStateChange();
this.#notifyStateChange(PresentationModeState.NORMAL);
this.pdfViewer.scrollMode = this.args.scrollMode;
this.pdfViewer.spreadMode = this.args.spreadMode;
this.pdfViewer.currentScaleValue = this.args.scaleValue;
this.pdfViewer.scrollMode = this.#args.scrollMode;
if (this.#args.spreadMode !== null) {
this.pdfViewer.spreadMode = this.#args.spreadMode;
}
this.pdfViewer.currentScaleValue = this.#args.scaleValue;
this.pdfViewer.currentPageNumber = pageNumber;
this.args = null;
if (this.#args.annotationEditorMode !== null) {
this.pdfViewer.annotationEditorMode = this.#args.annotationEditorMode;
}
this.#args = null;
}, 0);
this.#removeWindowListeners();

View file

@ -13,6 +13,11 @@
* 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 { RenderingStates } from "./ui_utils.js";

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,13 @@
* 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 {
getVisibleElements,
isValidRotation,
@ -33,6 +40,9 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {IL10n} l10n - Localization service.
* @property {Object} [pageColors] - Overwrites background and foreground colors
* with user defined ones in order to improve readability in high contrast
* mode.
*/
/**
@ -42,20 +52,39 @@ class PDFThumbnailViewer {
/**
* @param {PDFThumbnailViewerOptions} options
*/
constructor({ container, eventBus, linkService, renderingQueue, l10n }) {
constructor({
container,
eventBus,
linkService,
renderingQueue,
l10n,
pageColors,
}) {
this.container = container;
this.linkService = linkService;
this.renderingQueue = renderingQueue;
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._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() {
for (let i = 0, ii = this._thumbnails.length; i < ii; i++) {
if (
this._thumbnails[i] &&
this._thumbnails[i].renderingState !== RenderingStates.FINISHED
) {
this._thumbnails[i].reset();
for (const thumbnail of this._thumbnails) {
if (thumbnail.renderingState !== RenderingStates.FINISHED) {
thumbnail.reset();
}
}
TempImageFactory.destroyCanvas();
@ -163,8 +189,6 @@ class PDFThumbnailViewer {
this._currentPageNumber = 1;
this._pageLabels = null;
this._pagesRotation = 0;
this._optionalContentConfigPromise = null;
this._setImageDisabled = false;
// Remove the thumbnails from the DOM.
this.container.textContent = "";
@ -188,13 +212,8 @@ class PDFThumbnailViewer {
firstPagePromise
.then(firstPdfPage => {
this._optionalContentConfigPromise = optionalContentConfigPromise;
const pagesCount = pdfDocument.numPages;
const viewport = firstPdfPage.getViewport({ scale: 1 });
const checkSetImageDisabled = () => {
return this._setImageDisabled;
};
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const thumbnail = new PDFThumbnailView({
@ -204,18 +223,15 @@ class PDFThumbnailViewer {
optionalContentConfigPromise,
linkService: this.linkService,
renderingQueue: this.renderingQueue,
checkSetImageDisabled,
l10n: this.l10n,
pageColors: this.pageColors,
});
this._thumbnails.push(thumbnail);
}
// Set the first `pdfPage` immediately, since it's already loaded,
// rather than having to repeat the `PDFDocumentProxy.getPage` call in
// the `this.#ensurePdfPageLoaded` method before rendering can start.
const firstThumbnailView = this._thumbnails[0];
if (firstThumbnailView) {
firstThumbnailView.setPdfPage(firstPdfPage);
}
this._thumbnails[0]?.setPdfPage(firstPdfPage);
// Ensure that the current thumbnail is always highlighted on load.
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
@ -230,10 +246,8 @@ class PDFThumbnailViewer {
* @private
*/
_cancelRendering() {
for (let i = 0, ii = this._thumbnails.length; i < ii; i++) {
if (this._thumbnails[i]) {
this._thumbnails[i].cancelRendering();
}
for (const thumbnail of this._thumbnails) {
thumbnail.cancelRendering();
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -18,5 +18,5 @@
const {addScriptSync} = require('../../protyle/util/addScript')
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"];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,9 @@
* limitations under the License.
*/
import { RenderingStates, ScrollMode, SpreadMode } from './ui_utils.js'
import { AppOptions } from './app_options.js'
import { LinkTarget } from './pdf_link_service.js'
import { PDFViewerApplication } from './app.js'
import { initAnno } from '../anno'
@ -24,18 +26,22 @@ const pdfjsVersion =
const pdfjsBuild =
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.PDFViewerApplicationConstants = AppConstants
window.PDFViewerApplicationOptions = AppOptions
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
var defaultUrl; // eslint-disable-line no-var
(function rewriteUrlClosure () {
// Run this code outside DOMContentLoaded to make sure that the URL
// is rewritten as soon as possible.
const queryString = document.location.search.slice(1)
const m = /(^|&)file=([^&]*)/.exec(queryString)
defaultUrl = m ? decodeURIComponent(m[2]) : ''
const defaultUrl = m ? decodeURIComponent(m[2]) : ''
// Example: chrome-extension://.../http://example.com/file.pdf
const humanReadableUrl = '/' + defaultUrl + location.hash
@ -44,27 +50,34 @@ if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
// eslint-disable-next-line no-undef
chrome.runtime.sendMessage('showPageAction')
}
AppOptions.set('defaultUrl', defaultUrl)
})()
}
function getViewerConfiguration (element) {
let errorWrapper = null
if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) {
errorWrapper = {
container: element.querySelector('#errorWrapper'),
errorMessage: element.querySelector('#errorMessage'),
closeButton: element.querySelector('#errorClose'),
errorMoreInfo: element.querySelector('#errorMoreInfo'),
moreInfoButton: element.querySelector('#errorShowMore'),
lessInfoButton: element.querySelector('#errorShowLess'),
}
}
// NOTE
// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) {
// require('./firefoxcom.js')
// require('./firefox_print_service.js')
// }
// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
// require('./genericcom.js')
// }
// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
// require('./chromecom.js')
// }
// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME || GENERIC')) {
// require('./pdf_print_service.js')
// }
// NOTE
function getViewerConfiguration (element) {
return {
appContainer: element,
mainContainer: element.querySelector('#viewerContainer'),
viewerContainer: element.querySelector('#viewer'),
toolbar: {
// NOTE
rectAnno: element.querySelector('#rectAnno'),
container: element.querySelector('#toolbarViewer'),
numPages: element.querySelector('#numPages'),
@ -76,23 +89,28 @@ function getViewerConfiguration (element) {
zoomIn: element.querySelector('#zoomIn'),
zoomOut: element.querySelector('#zoomOut'),
viewFind: element.querySelector('#viewFind'),
openFile: element.querySelector('#openFile'),
openFile:
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? element.querySelector('#openFile')
: null,
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'),
viewBookmark: element.querySelector('#viewBookmark'),
},
secondaryToolbar: {
toolbar: element.querySelector('#secondaryToolbar'),
toggleButton: element.querySelector('#secondaryToolbarToggle'),
toolbarButtonContainer: element.querySelector(
'#secondaryToolbarButtonContainer'),
presentationModeButton: element.querySelector(
'#secondaryPresentationMode'),
openFileButton: element.querySelector('#secondaryOpenFile'),
presentationModeButton: element.querySelector('#presentationMode'),
openFileButton:
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? element.querySelector('#secondaryOpenFile')
: null,
printButton: element.querySelector('#secondaryPrint'),
downloadButton: element.querySelector('#secondaryDownload'),
viewBookmarkButton: element.querySelector('#secondaryViewBookmark'),
viewBookmarkButton: element.querySelector('#viewBookmark'),
firstPageButton: element.querySelector('#firstPage'),
lastPageButton: element.querySelector('#lastPage'),
pageRotateCwButton: element.querySelector('#pageRotateCw'),
@ -111,7 +129,7 @@ function getViewerConfiguration (element) {
sidebar: {
// Divs (and sidebar button)
outerContainer: element.querySelector('#outerContainer'),
viewerContainer: element.querySelector('#viewerContainer'),
sidebarContainer: element.querySelector('#sidebarContainer'),
toggleButton: element.querySelector('#sidebarToggle'),
// Buttons
thumbnailButton: element.querySelector('#viewThumbnail'),
@ -124,8 +142,7 @@ function getViewerConfiguration (element) {
attachmentsView: element.querySelector('#attachmentsView'),
layersView: element.querySelector('#layersView'),
// View-specific options
outlineOptionsContainer: element.querySelector(
'#outlineOptionsContainer'),
outlineOptionsContainer: element.querySelector('#outlineOptionsContainer'),
currentOutlineItemButton: element.querySelector('#currentOutlineItem'),
},
sidebarResizer: {
@ -146,16 +163,14 @@ function getViewerConfiguration (element) {
findNextButton: element.querySelector('#findNext'),
},
passwordOverlay: {
overlayName: 'passwordOverlay',
container: element.querySelector('#passwordOverlay'),
dialog: element.querySelector('#passwordDialog'),
label: element.querySelector('#passwordText'),
input: element.querySelector('#password'),
submitButton: element.querySelector('#passwordSubmit'),
cancelButton: element.querySelector('#passwordCancel'),
},
documentProperties: {
overlayName: 'documentPropertiesOverlay',
container: element.querySelector('#documentPropertiesOverlay'),
dialog: element.querySelector('#documentPropertiesDialog'),
closeButton: element.querySelector('#documentPropertiesClose'),
fields: {
fileName: element.querySelector('#fileNameField'),
@ -174,9 +189,29 @@ function getViewerConfiguration (element) {
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'),
openFileInputName: 'fileInput',
openFileInput:
typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
? element.querySelector('#fileInput')
: null,
debuggerScriptPath: './debugger.js',
}
}
@ -189,10 +224,6 @@ function webViewerLoad (file, element, pdfPage, annoId) {
if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')) {
config.file = file
} else {
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
AppOptions.set('defaultUrl', defaultUrl)
}
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
// Give custom implementations of the default viewer a simpler way to
// set various `AppOptions`, by dispatching an event once all viewer
@ -221,8 +252,20 @@ function webViewerLoad (file, element, pdfPage, annoId) {
// 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
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.
*/
/** @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";
/**
@ -65,7 +70,7 @@ class XfaLayerBuilder {
// Create an xfa layer div and render the form
const div = document.createElement("div");
this.pageDiv.appendChild(div);
this.pageDiv.append(div);
parameters.div = div;
const result = XfaLayer.render(parameters);
@ -94,7 +99,7 @@ class XfaLayerBuilder {
}
// Create an xfa layer div and render the form
this.div = document.createElement("div");
this.pageDiv.appendChild(this.div);
this.pageDiv.append(this.div);
parameters.div = this.div;
return XfaLayer.render(parameters);
})

View file

@ -544,7 +544,7 @@ export class Wnd {
return;
}
if (model instanceof Asset) {
if (model.pdfObject) {
if (model.pdfObject && model.pdfObject.pdfLoadingTask) {
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