summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/pdf
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2022-02-04 17:20:24 +0100
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2022-02-12 08:15:25 +0000
commit8fa0776f1f79e91fc9c0b9c1ba11a0a29c05196b (patch)
tree788d8d7549712682703a0310ca4a0f0860d4802b /chromium/chrome/browser/resources/pdf
parent606d85f2a5386472314d39923da28c70c60dc8e7 (diff)
BASELINE: Update Chromium to 98.0.4758.90
Change-Id: Ib7c41539bf8a8e0376bd639f27d68294de90f3c8 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/chrome/browser/resources/pdf')
-rw-r--r--chromium/chrome/browser/resources/pdf/BUILD.gn3
-rw-r--r--chromium/chrome/browser/resources/pdf/controller.js31
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-download-controls.html6
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js3
-rw-r--r--chromium/chrome/browser/resources/pdf/gesture_detector.js25
-rw-r--r--chromium/chrome/browser/resources/pdf/ink/drawing_canvas_externs.js19
-rw-r--r--chromium/chrome/browser/resources/pdf/ink/ink_api.js8
-rw-r--r--chromium/chrome/browser/resources/pdf/ink_controller.js13
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_internal_plugin_wrapper.js151
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_scripting_api.d.ts26
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_scripting_api.js1
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_viewer.js36
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_viewer_base.js56
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js28
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf_viewer_wrapper.js8
-rw-r--r--chromium/chrome/browser/resources/pdf/viewport.js528
-rw-r--r--chromium/chrome/browser/resources/pdf/viewport_scroller.js2
17 files changed, 713 insertions, 231 deletions
diff --git a/chromium/chrome/browser/resources/pdf/BUILD.gn b/chromium/chrome/browser/resources/pdf/BUILD.gn
index 1cfb3345c07..99aecf0e84e 100644
--- a/chromium/chrome/browser/resources/pdf/BUILD.gn
+++ b/chromium/chrome/browser/resources/pdf/BUILD.gn
@@ -251,6 +251,7 @@ js_library("viewport") {
deps = [
":constants",
":gesture_detector",
+ ":internal_plugin",
":zoom_manager",
"//ui/webui/resources/js:assert.m",
"//ui/webui/resources/js:event_tracker.m",
@@ -300,6 +301,7 @@ js_library("local_storage_proxy") {
js_library("controller") {
deps = [
+ ":gesture_detector",
":internal_plugin",
":viewport",
"//ui/webui/resources/js:assert.m",
@@ -342,7 +344,6 @@ js_library("pdf_viewer") {
":browser_api",
":constants",
":controller",
- ":gesture_detector",
":ink_controller",
":local_storage_proxy",
":metrics",
diff --git a/chromium/chrome/browser/resources/pdf/controller.js b/chromium/chrome/browser/resources/pdf/controller.js
index 3847e19c227..d61834fdfb3 100644
--- a/chromium/chrome/browser/resources/pdf/controller.js
+++ b/chromium/chrome/browser/resources/pdf/controller.js
@@ -8,6 +8,7 @@ import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
import {NamedDestinationMessageData, Point, SaveRequestType} from './constants.js';
+import {Gesture} from './gesture_detector.js';
import {UnseasonedPdfPluginElement} from './internal_plugin.js';
import {PartialPoint, PinchPhase, Viewport} from './viewport.js';
@@ -58,9 +59,8 @@ let ThumbnailMessageData;
*/
function createToken() {
const randomBytes = new Uint8Array(16);
- return window.crypto.getRandomValues(randomBytes)
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
+ window.crypto.getRandomValues(randomBytes);
+ return Array.from(randomBytes, b => b.toString(16).padStart(2, '0')).join('');
}
/** @interface */
@@ -199,6 +199,7 @@ export class PluginController {
this.pendingTokens_ = new Map();
this.requestResolverMap_ = new Map();
+ this.viewport_.setContent(this.plugin_);
this.plugin_.addEventListener(
'message', e => this.handlePluginMessage_(e), false);
if (!this.plugin_.postMessage) {
@@ -214,6 +215,8 @@ export class PluginController {
(message, transfer) => {
this.unseasonedDelayedMessages_.push({message, transfer});
};
+
+ this.viewport_.setRemoteContent(this.unseasonedPlugin_);
}
}
@@ -262,6 +265,12 @@ export class PluginController {
* @param {number} y
*/
updateScroll(x, y) {
+ if (this.unseasonedPlugin_) {
+ // Ignore "local" scroll events in unseasoned mode, as these are synthetic
+ // events generated by the remote scrolling implementation.
+ return;
+ }
+
this.postMessage_({type: 'updateScroll', x, y});
}
@@ -491,14 +500,16 @@ export class PluginController {
let url;
if (this.unseasonedPlugin_) {
assert(this.unseasonedPlugin_ === this.plugin_);
+ this.viewport_.setRemoteContent(this.unseasonedPlugin_);
this.unseasonedPlugin_.postMessage(
{type: 'loadArray', dataToLoad: data}, [data]);
} else {
+ this.viewport_.setContent(this.plugin_);
url = URL.createObjectURL(new Blob([data]));
this.plugin_.setAttribute('src', url);
+ this.plugin_.setAttribute('has-edits', '');
}
- this.plugin_.setAttribute('has-edits', '');
this.plugin_.style.display = 'block';
try {
await this.getLoadedCallback_();
@@ -557,6 +568,10 @@ export class PluginController {
}
switch (messageData.type) {
+ case 'gesture':
+ this.viewport_.dispatchGesture(
+ /** @type {{ gesture: !Gesture }} */ (messageData).gesture);
+ break;
case 'goToPage':
this.viewport_.goToPage(
/** @type {{type: string, page: number}} */ (messageData).page);
@@ -567,6 +582,14 @@ export class PluginController {
case 'scrollBy':
this.viewport_.scrollBy(/** @type {!Point} */ (messageData));
break;
+ case 'syncScrollFromRemote':
+ this.viewport_.syncScrollFromRemote(
+ /** @type {!Point} */ (messageData));
+ break;
+ case 'ackScrollToRemote':
+ this.viewport_.ackScrollToRemote(
+ /** @type {!Point} */ (messageData));
+ break;
case 'saveData':
this.saveData_(/** @type {!SaveDataMessageData} */ (messageData));
break;
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-download-controls.html b/chromium/chrome/browser/resources/pdf/elements/viewer-download-controls.html
index 7a3a0bae310..4e7ab345818 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-download-controls.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-download-controls.html
@@ -18,10 +18,12 @@
aria-haspopup$="[[downloadHasPopup_]]"
title="$i18n{tooltipDownload}"></cr-icon-button>
<cr-action-menu id="menu" on-open-changed="onOpenChanged_">
- <button class="dropdown-item" on-click="onDownloadEditedClick_">
+ <button id="download-edited" class="dropdown-item"
+ on-click="onDownloadEditedClick_">
$i18n{downloadEdited}
</button>
- <button class="dropdown-item" on-click="onDownloadOriginalClick_">
+ <button id="download-original" class="dropdown-item"
+ on-click="onDownloadOriginalClick_">
$i18n{downloadOriginal}
</button>
</cr-action-menu>
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
index 63599821e8c..cefaf68388a 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
@@ -307,7 +307,8 @@ class ViewerInkHostElement extends PolymerElement {
*/
async saveDocument() {
if (this.state_ === State.ACTIVE) {
- this.buffer_ = await this.ink_.getPDFDestructive().buffer;
+ const pdf = await this.ink_.getPDFDestructive();
+ this.buffer_ = await pdf.buffer;
this.state_ = State.IDLE;
}
return {
diff --git a/chromium/chrome/browser/resources/pdf/gesture_detector.js b/chromium/chrome/browser/resources/pdf/gesture_detector.js
index 1682f8ed362..d9ba6cbc333 100644
--- a/chromium/chrome/browser/resources/pdf/gesture_detector.js
+++ b/chromium/chrome/browser/resources/pdf/gesture_detector.js
@@ -27,22 +27,25 @@ export let PinchEventDetail;
// touches form gestures (e.g. pinching).
export class GestureDetector {
/**
- * @param {!EventTarget|!Element} element The element to monitor for touch
- * gestures.
+ * @param {!Element} element The element to monitor for touch gestures.
*/
constructor(element) {
- element.addEventListener(
+ /** @private {!Element} */
+ this.element_ = element;
+
+ this.element_.addEventListener(
'touchstart',
/** @type {function(!Event)} */ (this.onTouchStart_.bind(this)),
{passive: true});
const boundOnTouch =
/** @type {function(!Event)} */ (this.onTouch_.bind(this));
- element.addEventListener('touchmove', boundOnTouch, {passive: true});
- element.addEventListener('touchend', boundOnTouch, {passive: true});
- element.addEventListener('touchcancel', boundOnTouch, {passive: true});
+ this.element_.addEventListener('touchmove', boundOnTouch, {passive: true});
+ this.element_.addEventListener('touchend', boundOnTouch, {passive: true});
+ this.element_.addEventListener(
+ 'touchcancel', boundOnTouch, {passive: true});
- element.addEventListener(
+ this.element_.addEventListener(
'wheel',
/** @type {function(!Event)} */ (this.onWheel_.bind(this)),
{passive: false});
@@ -62,6 +65,7 @@ export class GestureDetector {
* @private {?number}
*/
this.accumulatedWheelScale_ = null;
+
/**
* A timeout ID from setTimeout used for sending the pinchend event when
* handling ctrl-wheels.
@@ -94,6 +98,13 @@ export class GestureDetector {
* @private
*/
notify_(type, detail) {
+ // Adjust center into element-relative coordinates.
+ const clientRect = this.element_.getBoundingClientRect();
+ detail.center = {
+ x: detail.center.x - clientRect.x,
+ y: detail.center.y - clientRect.y,
+ };
+
this.eventTarget_.dispatchEvent(new CustomEvent(type, {detail}));
}
diff --git a/chromium/chrome/browser/resources/pdf/ink/drawing_canvas_externs.js b/chromium/chrome/browser/resources/pdf/ink/drawing_canvas_externs.js
index 525eb6c35d2..3c3a75ccd6e 100644
--- a/chromium/chrome/browser/resources/pdf/ink/drawing_canvas_externs.js
+++ b/chromium/chrome/browser/resources/pdf/ink/drawing_canvas_externs.js
@@ -7,6 +7,10 @@
*
* This file defines types and an interface, drawings.Canvas, that are safe for
* export and satisfy the usage in the Chrome PDF annotation mode.
+ *
+ * See
+ * https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/resources/pdf/ink/ink_api.js
+ * for usage.
*/
/** @const Namespace */
@@ -99,9 +103,9 @@ drawings.Canvas = class {
* Returns a copy of the currently-edited PDF with the Ink annotations
* serialized into it.
*
- * @return {!Uint8Array}
+ * @return {!Promise<!Uint8Array>}
*/
- getPDF() {}
+ async getPDF() {}
/**
* Returns the currently-edited PDF with the Ink annotations serialized into
@@ -109,9 +113,9 @@ drawings.Canvas = class {
* should not be issued any further strokes or functions calls until setPDF is
* called again.
*
- * @return {!Uint8Array}
+ * @return {!Promise<!Uint8Array>}
*/
- getPDFDestructive() {}
+ async getPDFDestructive() {}
/**
* Set the camera to the provided box in PDF coordinates.
@@ -143,6 +147,13 @@ drawings.Canvas = class {
flush() {}
/**
+ * Returns a Promise that is resolved when no new frames will be requested.
+ *
+ * @return {!Promise<undefined>}
+ */
+ waitForZeroFps() {}
+
+ /**
* Set the out of bounds color drawn around the PDF and between pages.
*
* @param {string} color
diff --git a/chromium/chrome/browser/resources/pdf/ink/ink_api.js b/chromium/chrome/browser/resources/pdf/ink/ink_api.js
index 5263fb4f921..0f499c95706 100644
--- a/chromium/chrome/browser/resources/pdf/ink/ink_api.js
+++ b/chromium/chrome/browser/resources/pdf/ink/ink_api.js
@@ -29,16 +29,16 @@ class InkAPI {
}
/**
- * @return {!Uint8Array}
+ * @return {!Promise<!Uint8Array>}
*/
- getPDF() {
+ async getPDF() {
return this.canvas_.getPDF();
}
/**
- * @return {!Uint8Array}
+ * @return {!Promise<!Uint8Array>}
*/
- getPDFDestructive() {
+ async getPDFDestructive() {
return this.canvas_.getPDFDestructive();
}
diff --git a/chromium/chrome/browser/resources/pdf/ink_controller.js b/chromium/chrome/browser/resources/pdf/ink_controller.js
index 95b8a5b8f83..9e580f5778e 100644
--- a/chromium/chrome/browser/resources/pdf/ink_controller.js
+++ b/chromium/chrome/browser/resources/pdf/ink_controller.js
@@ -54,9 +54,6 @@ export class InkController {
/** @private {!Viewport} */
this.viewport_;
- /** @private {!HTMLDivElement} */
- this.contentElement_;
-
/** @private {?ViewerInkHostElement} */
this.inkHost_ = null;
@@ -66,11 +63,9 @@ export class InkController {
/**
* @param {!Viewport} viewport
- * @param {!HTMLDivElement} contentElement
*/
- init(viewport, contentElement) {
+ init(viewport) {
this.viewport_ = viewport;
- this.contentElement_ = contentElement;
}
/**
@@ -78,9 +73,9 @@ export class InkController {
* @override
*/
get isActive() {
- // Check whether `contentElement_` is defined as a signal that `init()` was
+ // Check whether `viewport_` is defined as a signal that `init()` was
// called.
- return !!this.contentElement_ && this.isActive_;
+ return !!this.viewport_ && this.isActive_;
}
/**
@@ -158,7 +153,7 @@ export class InkController {
load(filename, data) {
if (!this.inkHost_) {
const inkHost = document.createElement('viewer-ink-host');
- this.contentElement_.appendChild(inkHost);
+ this.viewport_.setContent(inkHost);
this.inkHost_ = /** @type {!ViewerInkHostElement} */ (inkHost);
this.inkHost_.viewport = this.viewport_;
inkHost.addEventListener('stroke-added', e => {
diff --git a/chromium/chrome/browser/resources/pdf/pdf_internal_plugin_wrapper.js b/chromium/chrome/browser/resources/pdf/pdf_internal_plugin_wrapper.js
index 76f74a4d173..adcff18098d 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_internal_plugin_wrapper.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_internal_plugin_wrapper.js
@@ -6,19 +6,9 @@ import {GestureDetector, PinchEventDetail} from './gesture_detector.js';
const channel = new MessageChannel();
+const sizer = document.querySelector('#sizer');
const plugin =
/** @type {!HTMLEmbedElement} */ (document.querySelector('embed'));
-plugin.addEventListener('message', e => channel.port1.postMessage(e.data));
-channel.port1.onmessage = e => {
- if (e.data.type === 'loadArray') {
- if (plugin.src.startsWith('blob:')) {
- URL.revokeObjectURL(plugin.src);
- }
- plugin.src = URL.createObjectURL(new Blob([e.data.dataToLoad]));
- } else {
- plugin.postMessage(e.data);
- }
-};
const srcUrl = new URL(plugin.getAttribute('src'));
let parentOrigin = srcUrl.origin;
@@ -26,9 +16,85 @@ if (parentOrigin === 'chrome-untrusted://print') {
// Within Print Preview, the source origin differs from the parent origin.
parentOrigin = 'chrome://print';
}
+
+// Plugin-to-parent message handlers. All messages are passed through, but some
+// messages may affect this frame, too.
+let isFormFieldFocused = false;
+plugin.addEventListener('message', e => {
+ switch (e.data.type) {
+ case 'formFocusChange':
+ // TODO(crbug.com/1279516): Ideally, the plugin would just consume
+ // interesting keyboard events first.
+ isFormFieldFocused = /** @type {{focused:boolean}} */ (e.data).focused;
+ break;
+ }
+
+ channel.port1.postMessage(e.data);
+});
+
+// Parent-to-plugin message handlers. Most messages are passed through, but some
+// messages (with handlers that `return` immediately) are meant only for this
+// frame, not the plugin.
+channel.port1.onmessage = e => {
+ switch (e.data.type) {
+ case 'loadArray':
+ if (plugin.src.startsWith('blob:')) {
+ URL.revokeObjectURL(plugin.src);
+ }
+ plugin.src = URL.createObjectURL(new Blob([e.data.dataToLoad]));
+ plugin.setAttribute('has-edits', '');
+ return;
+
+ case 'syncScrollToRemote':
+ window.scrollTo(e.data.x, e.data.y);
+ channel.port1.postMessage({
+ type: 'ackScrollToRemote',
+ x: window.scrollX,
+ y: window.scrollY,
+ });
+ return;
+
+ case 'updateSize':
+ sizer.style.width = `${e.data.width}px`;
+ sizer.style.height = `${e.data.height}px`;
+ return;
+
+ case 'viewport':
+ // Snoop on "viewport" message to support real RTL scrolling in Print
+ // Preview.
+ // TODO(crbug.com/1158670): Support real RTL scrolling in the PDF viewer.
+ if (parentOrigin === 'chrome://print' && e.data.layoutOptions) {
+ switch (e.data.layoutOptions.direction) {
+ case 1:
+ document.dir = 'rtl';
+ break;
+ case 2:
+ document.dir = 'ltr';
+ break;
+ default:
+ document.dir = '';
+ break;
+ }
+ }
+ break;
+ }
+
+ plugin.postMessage(e.data);
+};
+
+// Entangle parent-child message channel.
window.parent.postMessage(
{type: 'connect', token: srcUrl.href}, parentOrigin, [channel.port2]);
+// Forward "scroll" events back to the parent frame's `Viewport`.
+window.addEventListener('scroll', () => {
+ channel.port1.postMessage({
+ type: 'syncScrollFromRemote',
+ x: window.scrollX,
+ y: window.scrollY,
+ });
+});
+
/**
* Relays gesture events to the parent frame.
* @param {!Event} e The gesture event.
@@ -51,14 +117,42 @@ for (const type of ['pinchstart', 'pinchupdate', 'pinchend']) {
document.addEventListener('keydown', e => {
// Only forward potential shortcut keys.
- if (!e.ctrlKey && !e.metaKey && e.key !== ' ') {
- return;
- }
+ switch (e.key) {
+ case ' ':
+ // Preventing Space happens in the "keypress" event handler.
+ break;
+ case 'PageDown':
+ case 'PageUp':
+ // Always prevent PageDown/PageUp.
+ e.preventDefault();
+ break;
+
+ case 'ArrowDown':
+ case 'ArrowLeft':
+ case 'ArrowRight':
+ case 'ArrowUp':
+ // Don't prevent arrow navigation in form fields, or if modified.
+ if (!isFormFieldFocused && !hasKeyModifiers(e)) {
+ e.preventDefault();
+ }
+ break;
- // Take over Ctrl+A, but not other shortcuts, such as zoom or print.
- if (e.key === 'a') {
- e.preventDefault();
+ case 'Escape':
+ case 'Tab':
+ // Print Preview is interested in Escape and Tab.
+ break;
+
+ default:
+ if (e.ctrlKey || e.metaKey) {
+ // Take over Ctrl+A, but not other shortcuts, such as zoom or print.
+ if (e.key === 'a') {
+ e.preventDefault();
+ }
+ break;
+ }
+ return;
}
+
channel.port1.postMessage({
type: 'sendKeyEvent',
keyEvent: {
@@ -72,3 +166,26 @@ document.addEventListener('keydown', e => {
},
});
});
+
+// Suppress extra scroll by preventing the default "keypress" handler for Space.
+// TODO(crbug.com/1279429): Ideally would prevent "keydown" instead, but this
+// doesn't work when a plugin element has focus.
+document.addEventListener('keypress', e => {
+ switch (e.key) {
+ case ' ':
+ // Don't prevent Space in form fields.
+ if (!isFormFieldFocused) {
+ e.preventDefault();
+ }
+ break;
+ }
+});
+
+// TODO(crbug.com/1252096): Load from chrome://resources/js/util.m.js instead.
+/**
+ * @param {!Event} e
+ * @return {boolean}
+ */
+function hasKeyModifiers(e) {
+ return !!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey);
+}
diff --git a/chromium/chrome/browser/resources/pdf/pdf_scripting_api.d.ts b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.d.ts
new file mode 100644
index 00000000000..df2284113f7
--- /dev/null
+++ b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.d.ts
@@ -0,0 +1,26 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Minimal TypeScript definitions for Print Preview.
+// TODO(rbpotter): Remove this file once pdf_scripting_api.js is migrated to
+// TypeScript.
+
+export interface PDFPlugin extends HTMLIFrameElement {
+ darkModeChanged(darkMode: boolean): void;
+ hideToolbar(): void;
+ loadPreviewPage(url: string, index: number): void;
+ resetPrintPreviewMode(
+ url: string, color: boolean, pages: number[], modifiable: boolean): void;
+ scrollPosition(x: number, y: number): void;
+ sendKeyEvent(e: KeyboardEvent): void;
+ setKeyEventCallback(callback: (e: KeyboardEvent) => void): void;
+ setLoadCompleteCallback(callback: (success: boolean) => void): void;
+ setViewportChangedCallback(
+ callback:
+ (pageX: number, pageY: number, pageWidth: number,
+ viewportWidth: number, viewportHeight: number) => void): void;
+}
+
+export function PDFCreateOutOfProcessPlugin(
+ src: string, baseUrl: string): PDFPlugin;
diff --git a/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js
index 0cb40b79e5b..7887f4200d6 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js
@@ -19,7 +19,6 @@ export function DeserializeKeyEvent(dict) {
altKey: dict.altKey,
metaKey: dict.metaKey,
});
- e.fromScriptingAPI = true;
return e;
}
diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer.js b/chromium/chrome/browser/resources/pdf/pdf_viewer.js
index 64308b73b70..c8990a298ba 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_viewer.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_viewer.js
@@ -24,7 +24,6 @@ import {PluginController} from './controller.js';
import {ViewerErrorDialogElement} from './elements/viewer-error-dialog.js';
import {ViewerPdfSidenavElement} from './elements/viewer-pdf-sidenav.js';
import {ViewerToolbarElement} from './elements/viewer-toolbar.js';
-import {Gesture} from './gesture_detector.js';
// <if expr="enable_ink">
import {InkController, InkControllerEventType} from './ink_controller.js';
//</if>
@@ -303,16 +302,6 @@ export class PDFViewerElement extends PDFViewerBaseElement {
// </if>
}
- /** @override */
- getContent() {
- return /** @type {!HTMLDivElement} */ (this.$$('#content'));
- }
-
- /** @override */
- getSizer() {
- return /** @type {!HTMLDivElement} */ (this.$$('#sizer'));
- }
-
/**
* @return {!ViewerToolbarElement}
* @private
@@ -328,14 +317,16 @@ export class PDFViewerElement extends PDFViewerBaseElement {
/** @param {!BrowserApi} browserApi */
init(browserApi) {
- super.init(browserApi);
+ super.init(
+ browserApi, /** @type {!HTMLElement} */ (this.$$('#scroller')),
+ /** @type {!HTMLDivElement} */ (this.$$('#sizer')),
+ /** @type {!HTMLDivElement} */ (this.$$('#content')));
this.pluginController_ = PluginController.getInstance();
// <if expr="enable_ink">
this.inkController_ = InkController.getInstance();
- this.inkController_.init(
- this.viewport, /** @type {!HTMLDivElement} */ (this.getContent()));
+ this.inkController_.init(this.viewport);
this.tracker.add(
this.inkController_.getEventTarget(),
InkControllerEventType.HAS_UNSAVED_CHANGES,
@@ -653,7 +644,6 @@ export class PDFViewerElement extends PDFViewerBaseElement {
}
}
- /** @return {!Viewport} The viewport. Used for testing. */
/** @return {!Array<!Bookmark>} The bookmarks. Used for testing. */
get bookmarks() {
return this.bookmarks_;
@@ -840,13 +830,11 @@ export class PDFViewerElement extends PDFViewerBaseElement {
this.documentHasFocus_ =
/** @type {{ hasFocus: boolean }} */ (data).hasFocus;
return;
- case 'gesture':
- this.viewport.dispatchGesture(
- /** @type {{ gesture: !Gesture }} */ (data).gesture);
- return;
case 'sendKeyEvent':
- this.handleKeyEvent(/** @type {!KeyboardEvent} */ (DeserializeKeyEvent(
- /** @type {{ keyEvent: Object }} */ (data).keyEvent)));
+ const keyEvent = DeserializeKeyEvent(
+ /** @type {{ keyEvent: Object }} */ (data).keyEvent);
+ keyEvent.fromPlugin = true;
+ this.handleKeyEvent(keyEvent);
return;
}
assertNotReached('Unknown message type received: ' + data.type);
@@ -1121,7 +1109,8 @@ export class PDFViewerElement extends PDFViewerBaseElement {
if (!fileName.toLowerCase().endsWith('.pdf')) {
fileName = fileName + '.pdf';
}
-
+ // Create blob before callback to avoid race condition.
+ const blob = new Blob([result.dataToSave], {type: 'application/pdf'});
chrome.fileSystem.chooseEntry(
{
type: 'saveFile',
@@ -1138,8 +1127,7 @@ export class PDFViewerElement extends PDFViewerBaseElement {
return;
}
entry.createWriter(writer => {
- writer.write(
- new Blob([result.dataToSave], {type: 'application/pdf'}));
+ writer.write(blob);
// Unblock closing the window now that the user has saved
// successfully.
chrome.mimeHandlerPrivate.setShowBeforeUnloadDialog(false);
diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js
index f0f8eb281f7..15b33cc9fbe 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js
@@ -117,18 +117,6 @@ export class PDFViewerBaseElement extends PolymerElement {
}
/**
- * @return {!HTMLDivElement}
- * @protected
- */
- getContent() {}
-
- /**
- * @return {!HTMLDivElement}
- * @protected
- */
- getSizer() {}
-
- /**
* @param {!FittingType} view
* @protected
*/
@@ -149,17 +137,26 @@ export class PDFViewerBaseElement extends PolymerElement {
return this.shadowRoot.querySelector(query);
}
+ /**
+ * Whether to enable the new UI.
+ * @return {boolean}
+ * @protected
+ */
+ isNewUiEnabled() {
+ return true;
+ }
+
/** @return {number} */
getBackgroundColor() {
return -1;
}
/**
- * @param {boolean} isPrintPreview Is the plugin for Print Preview.
+ * Creates the plugin element.
* @return {!HTMLEmbedElement} The plugin
* @private
*/
- createPlugin_(isPrintPreview) {
+ createPlugin_() {
// Create the plugin object dynamically. The plugin element is sized to
// fill the entire window and is set to be fixed positioning, acting as a
// viewport. The plugin renders into this viewport according to the scroll
@@ -189,7 +186,7 @@ export class PDFViewerBaseElement extends PolymerElement {
plugin.toggleAttribute('full-frame', true);
}
- if (!isPrintPreview) {
+ if (this.isNewUiEnabled()) {
plugin.toggleAttribute('pdf-viewer-update-enabled', true);
}
@@ -208,8 +205,14 @@ export class PDFViewerBaseElement extends PolymerElement {
return plugin;
}
- /** @param {!BrowserApi} browserApi */
- init(browserApi) {
+ /**
+ * Initializes the PDF viewer.
+ * @param {!BrowserApi} browserApi The interface with the browser.
+ * @param {!HTMLElement} scroller The viewport's scroller element.
+ * @param {!HTMLDivElement} sizer The viewport's sizer element.
+ * @param {!HTMLDivElement} content The viewport's content element.
+ */
+ init(browserApi, scroller, sizer, content) {
this.browserApi = browserApi;
this.originalUrl = this.browserApi.getStreamInfo().originalUrl;
@@ -220,13 +223,6 @@ export class PDFViewerBaseElement extends PolymerElement {
return PluginController.getInstance().getNamedDestination(destination);
});
- // Determine the scrolling container.
- const isPrintPreview =
- document.documentElement.hasAttribute('is-print-preview');
- const scrollContainer = isPrintPreview ?
- document.documentElement :
- /** @type {!HTMLElement} */ (this.getSizer().offsetParent);
-
// Create the viewport.
const defaultZoom =
this.browserApi.getZoomBehavior() === ZoomBehavior.MANAGE ?
@@ -234,8 +230,7 @@ export class PDFViewerBaseElement extends PolymerElement {
1.0;
this.viewport_ = new Viewport(
- scrollContainer, this.getSizer(), this.getContent(),
- getScrollbarWidth(), defaultZoom);
+ scroller, sizer, content, getScrollbarWidth(), defaultZoom);
this.viewport_.setViewportChangedCallback(() => this.viewportChanged_());
this.viewport_.setBeforeZoomCallback(
() => this.currentController.beforeZoom());
@@ -255,8 +250,7 @@ export class PDFViewerBaseElement extends PolymerElement {
}, false);
// Create the plugin.
- this.plugin_ = this.createPlugin_(isPrintPreview);
- this.getContent().appendChild(this.plugin_);
+ this.plugin_ = this.createPlugin_();
const pluginController = PluginController.getInstance();
pluginController.init(
@@ -306,13 +300,13 @@ export class PDFViewerBaseElement extends PolymerElement {
if (progress === -1) {
// Document load failed.
this.showErrorDialog = true;
- this.getSizer().style.display = 'none';
+ this.viewport_.setContent(null);
this.setLoadState(LoadState.FAILED);
this.sendDocumentLoadedMessage();
} else if (progress === 100) {
// Document load complete.
if (this.lastViewportPosition) {
- this.viewport_.position = this.lastViewportPosition;
+ this.viewport_.setPosition(this.lastViewportPosition);
}
this.paramsParser.getViewportFromUrlParams(this.originalUrl)
.then(params => this.handleURLParams_(params));
@@ -544,7 +538,7 @@ export class PDFViewerBaseElement extends PolymerElement {
} else if (params.view === FittingType.FIT_TO_HEIGHT) {
currentViewportPosition.x += zoomedPositionShift;
}
- this.viewport_.position = currentViewportPosition;
+ this.viewport_.setPosition(currentViewportPosition);
}
this.isUserInitiatedEvent = true;
}
diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js
index 0898e201b01..28ce7cf1362 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js
@@ -55,13 +55,8 @@ class PDFViewerPPElement extends PDFViewerBaseElement {
}
/** @override */
- getContent() {
- return /** @type {!HTMLDivElement} */ (this.$$('#content'));
- }
-
- /** @override */
- getSizer() {
- return /** @type {!HTMLDivElement} */ (this.$$('#sizer'));
+ isNewUiEnabled() {
+ return false;
}
/** @override */
@@ -79,7 +74,10 @@ class PDFViewerPPElement extends PDFViewerBaseElement {
/** @param {!BrowserApi} browserApi */
init(browserApi) {
- super.init(browserApi);
+ super.init(
+ browserApi, document.documentElement,
+ /** @type {!HTMLDivElement} */ (this.$$('#sizer')),
+ /** @type {!HTMLDivElement} */ (this.$$('#content')));
/** @private {?PluginController} */
this.pluginController_ = PluginController.getInstance();
@@ -242,8 +240,10 @@ class PDFViewerPPElement extends PDFViewerBaseElement {
this.pluginController_.resetPrintPreviewMode(messageData);
return true;
case 'sendKeyEvent':
- this.handleKeyEvent(/** @type {!KeyboardEvent} */ (DeserializeKeyEvent(
- /** @type {{ keyEvent: Object }} */ (message.data).keyEvent)));
+ const keyEvent = DeserializeKeyEvent(
+ /** @type {{ keyEvent: Object }} */ (message.data).keyEvent);
+ keyEvent.fromScriptingAPI = true;
+ this.handleKeyEvent(keyEvent);
return true;
case 'hideToolbar':
this.toolbarManager_.resetKeyboardNavigationAndHideToolbar();
@@ -257,7 +257,7 @@ class PDFViewerPPElement extends PDFViewerBaseElement {
messageData = /** @type {{ x: number, y: number }} */ (message.data);
position.y += messageData.y;
position.x += messageData.x;
- this.viewport.position = position;
+ this.viewport.setPosition(position);
return true;
}
@@ -305,6 +305,12 @@ class PDFViewerPPElement extends PDFViewerBaseElement {
case 'documentFocusChanged':
// TODO(crbug.com/1069370): Draw a focus rect around plugin.
return;
+ case 'sendKeyEvent':
+ const keyEvent = DeserializeKeyEvent(
+ /** @type {{ keyEvent: Object }} */ (data).keyEvent);
+ keyEvent.fromPlugin = true;
+ this.handleKeyEvent(keyEvent);
+ return;
case 'beep':
case 'formFocusChange':
case 'getPassword':
diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_wrapper.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_wrapper.js
index befb05fd348..ba26e58ec4e 100644
--- a/chromium/chrome/browser/resources/pdf/pdf_viewer_wrapper.js
+++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_wrapper.js
@@ -15,8 +15,12 @@ export {ViewerPdfSidenavElement} from './elements/viewer-pdf-sidenav.js';
export {ViewerPropertiesDialogElement} from './elements/viewer-properties-dialog.js';
export {ViewerThumbnailBarElement} from './elements/viewer-thumbnail-bar.js';
export {PAINTED_ATTRIBUTE, ViewerThumbnailElement} from './elements/viewer-thumbnail.js';
+// <if expr="enable_ink">
+export {ViewerToolbarDropdownElement} from './elements/viewer-toolbar-dropdown.js';
+// </if>
export {ViewerToolbarElement} from './elements/viewer-toolbar.js';
export {GestureDetector, PinchEventDetail} from './gesture_detector.js';
+export {UnseasonedPdfPluginElement} from './internal_plugin.js';
export {record, recordFitTo, resetForTesting, UserAction} from './metrics.js';
export {NavigatorDelegate, PdfNavigator, WindowOpenDisposition} from './navigator.js';
export {OpenPdfParamsParser} from './open_pdf_params_parser.js';
@@ -25,7 +29,3 @@ export {getFilenameFromURL, PDFViewerElement} from './pdf_viewer.js';
export {shouldIgnoreKeyEvents} from './pdf_viewer_utils.js';
export {LayoutOptions, PAGE_SHADOW, Viewport} from './viewport.js';
export {ZoomManager} from './zoom_manager.js';
-
-// <if expr="enable_ink">
-export {ViewerToolbarDropdownElement} from './elements/viewer-toolbar-dropdown.js';
-// </if>
diff --git a/chromium/chrome/browser/resources/pdf/viewport.js b/chromium/chrome/browser/resources/pdf/viewport.js
index 896defafa3d..7954418d1e1 100644
--- a/chromium/chrome/browser/resources/pdf/viewport.js
+++ b/chromium/chrome/browser/resources/pdf/viewport.js
@@ -8,6 +8,7 @@ import {$, hasKeyModifiers, isRTL} from 'chrome://resources/js/util.m.js';
import {FittingType, Point} from './constants.js';
import {Gesture, GestureDetector, PinchEventDetail} from './gesture_detector.js';
+import {UnseasonedPdfPluginElement} from './internal_plugin.js';
import {InactiveZoomManager, ZoomManager} from './zoom_manager.js';
/**
@@ -65,29 +66,29 @@ function vectorDelta(p1, p2) {
return {x: p2.x - p1.x, y: p2.y - p1.y};
}
+// TODO(crbug.com/1276456): Would Viewport be better as a Polymer element?
export class Viewport {
/**
- * @param {!HTMLElement} scrollParent
+ * @param {!HTMLElement} container The element which contains the scrollable
+ * content.
* @param {!HTMLDivElement} sizer The element which represents the size of the
- * document in the viewport
+ * scrollable content in the viewport
* @param {!HTMLDivElement} content The element which is the parent of the
* plugin in the viewer.
* @param {number} scrollbarWidth The width of scrollbars on the page
* @param {number} defaultZoom The default zoom level.
*/
- constructor(scrollParent, sizer, content, scrollbarWidth, defaultZoom) {
+ constructor(container, sizer, content, scrollbarWidth, defaultZoom) {
/** @private {!HTMLElement} */
- this.window_ = scrollParent;
-
- /** @private {!HTMLDivElement} */
- this.sizer_ = sizer;
-
- /** @private {!HTMLDivElement} */
- this.content_ = content;
+ this.window_ = container;
/** @private {number} */
this.scrollbarWidth_ = scrollbarWidth;
+ /** @private {!ScrollContent} */
+ this.scrollContent_ =
+ new ScrollContent(this.window_, sizer, content, this.scrollbarWidth_);
+
/** @private {number} */
this.defaultZoom_ = defaultZoom;
@@ -153,7 +154,7 @@ export class Viewport {
this.tracker_ = new EventTracker();
/** @private {!GestureDetector} */
- this.gestureDetector_ = new GestureDetector(this.content_);
+ this.gestureDetector_ = new GestureDetector(content);
/** @private {boolean} */
this.sentPinchEvent_ = false;
@@ -179,6 +180,7 @@ export class Viewport {
// Necessary check since during testing a fake DOM element is used.
!(this.window_ instanceof HTMLElement)) {
window.addEventListener('scroll', this.updateViewport_.bind(this));
+ this.scrollContent_.setEventTarget(window);
// The following line is only used in tests, since they expect
// |scrollCallback| to be called on the mock |window_| object (legacy).
this.window_.scrollCallback = this.updateViewport_.bind(this);
@@ -189,6 +191,7 @@ export class Viewport {
} else {
// Standard PDF viewer
this.window_.addEventListener('scroll', this.updateViewport_.bind(this));
+ this.scrollContent_.setEventTarget(this.window_);
const resizeObserver = new ResizeObserver(_ => this.resizeWrapper_());
const target = this.window_.parentElement;
assert(target.id === 'main');
@@ -199,6 +202,39 @@ export class Viewport {
'change-zoom', e => this.setZoom(e.detail.zoom));
}
+ /**
+ * Sets the contents of the viewport, scrolling within the viewport's window.
+ * @param {?Node} content The new viewport contents, or null to clear the
+ * viewport.
+ */
+ setContent(content) {
+ this.scrollContent_.setContent(content);
+ }
+
+ /**
+ * Sets the contents of the viewport, scrolling within the content's window.
+ * @param {!UnseasonedPdfPluginElement} content The new viewport contents.
+ */
+ setRemoteContent(content) {
+ this.scrollContent_.setRemoteContent(content);
+ }
+
+ /**
+ * Synchronizes scroll position from remote content.
+ * @param {!Point} position
+ */
+ syncScrollFromRemote(position) {
+ this.scrollContent_.syncScrollFromRemote(position);
+ }
+
+ /**
+ * Receives acknowledgment of scroll position synchronized to remote content.
+ * @param {!Point} position
+ */
+ ackScrollToRemote(position) {
+ this.scrollContent_.ackScrollToRemote(position);
+ }
+
/** @param {function():void} viewportChangedCallback */
setViewportChangedCallback(viewportChangedCallback) {
this.viewportChangedCallback_ = viewportChangedCallback;
@@ -394,26 +430,12 @@ export class Viewport {
contentSizeChanged_() {
const zoomedDimensions = this.getZoomedDocumentDimensions_(this.getZoom());
if (zoomedDimensions) {
- this.sizer_.style.width = zoomedDimensions.width + 'px';
- this.sizer_.style.height = zoomedDimensions.height + 'px';
+ this.scrollContent_.setSize(
+ zoomedDimensions.width, zoomedDimensions.height);
}
}
/**
- * @param {!Point} coordinateInFrame
- * @return {!Point} Coordinate converted to plugin coordinates.
- * @private
- */
- frameToPluginCoordinate_(coordinateInFrame) {
- const containerRect =
- this.content_.querySelector('#plugin').getBoundingClientRect();
- return {
- x: coordinateInFrame.x - containerRect.left,
- y: coordinateInFrame.y - containerRect.top
- };
- }
-
- /**
* Called when the viewport should be updated.
* @private
*/
@@ -459,18 +481,21 @@ export class Viewport {
/** @return {!Point} The scroll position of the viewport. */
get position() {
- return {x: this.window_.scrollLeft, y: this.window_.scrollTop};
+ return {
+ x: this.scrollContent_.scrollLeft,
+ y: this.scrollContent_.scrollTop,
+ };
}
/**
* Scroll the viewport to the specified position.
* @param {!Point} position The position to scroll to.
*/
- set position(position) {
- this.window_.scrollTo(position.x, position.y);
+ setPosition(position) {
+ this.scrollContent_.scrollTo(position.x, position.y);
}
- /** @return {!Size} the size of the viewport excluding scrollbars. */
+ /** @return {!Size} The size of the viewport. */
get size() {
return {
width: this.window_.offsetWidth,
@@ -478,6 +503,14 @@ export class Viewport {
};
}
+ /**
+ * Exposes the current content size for testing.
+ * @return {!Size}
+ */
+ get contentSizeForTesting() {
+ return this.scrollContent_.sizeForTesting;
+ }
+
/** @return {number} The current zoom. */
getZoom() {
return this.zoomManager_.applyBrowserZoom(this.internalZoom_);
@@ -561,10 +594,10 @@ export class Viewport {
this.contentSizeChanged_();
// Scroll to the scaled scroll position.
zoom = this.getZoom();
- this.position = {
+ this.setPosition({
x: currentScrollPos.x * zoom,
- y: currentScrollPos.y * zoom
- };
+ y: currentScrollPos.y * zoom,
+ });
}
/**
@@ -595,7 +628,7 @@ export class Viewport {
this.contentSizeChanged_();
// Scroll to the scaled scroll position.
- this.position = {x: currentScrollPos.x, y: currentScrollPos.y};
+ this.setPosition(currentScrollPos);
}
/**
@@ -639,10 +672,10 @@ export class Viewport {
this.contentSizeChanged_();
const newZoom = this.getZoom();
// Scroll to the scaled scroll position.
- this.position = {
+ this.setPosition({
x: currentScrollPos.x * newZoom,
- y: currentScrollPos.y * newZoom
- };
+ y: currentScrollPos.y * newZoom,
+ });
this.updateViewport_();
});
}
@@ -944,10 +977,10 @@ export class Viewport {
};
this.setZoomInternal_(this.computeFittingZoom_(dimensions, false, true));
if (scrollToTopOfPage) {
- this.position = {
+ this.setPosition({
x: 0,
y: this.pageDimensions_[page].y * this.getZoom(),
- };
+ });
}
this.updateViewport_();
});
@@ -979,10 +1012,10 @@ export class Viewport {
};
this.setZoomInternal_(this.computeFittingZoom_(dimensions, true, true));
if (scrollToTopOfPage) {
- this.position = {
+ this.setPosition({
x: 0,
y: this.pageDimensions_[page].y * this.getZoom(),
- };
+ });
}
this.updateViewport_();
});
@@ -1050,47 +1083,25 @@ export class Viewport {
// Avoid scrolling if the space key is down while a form field is focused
// on since the user might be typing space into the field.
if (formFieldFocused && e.key === ' ') {
+ this.window_.dispatchEvent(new CustomEvent('scroll-avoided-for-testing'));
return;
}
- const direction =
- e.key === 'PageUp' || (e.key === ' ' && e.shiftKey) ? -1 : 1;
+ const isDown = e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey);
// Go to the previous/next page if we are fit-to-page or fit-to-height.
if (this.isPagedMode_()) {
- direction === 1 ? this.goToNextPage() : this.goToPreviousPage();
+ isDown ? this.goToNextPage() : this.goToPreviousPage();
// Since we do the movement of the page.
e.preventDefault();
- } else if (
- /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
- .fromScriptingAPI) {
- this.position = {
+ } else if (isCrossFrameKeyEvent(e)) {
+ const scrollOffset = (isDown ? 1 : -1) * this.size.height;
+ this.setPosition({
x: this.position.x,
- y: this.position.y + direction * this.size.height,
- };
- }
- }
-
- /**
- * @param {!KeyboardEvent} e
- * @param {boolean} formFieldFocused
- * @private
- */
- arrowLeftHandler_(e, formFieldFocused) {
- if (hasKeyModifiers(e)) {
- return;
+ y: this.position.y + scrollOffset,
+ });
}
- // Go to the previous page if there are no horizontal scrollbars and
- // no form field is focused.
- if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) {
- this.goToPreviousPage();
- // Since we do the movement of the page.
- e.preventDefault();
- } else if (
- /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
- .fromScriptingAPI) {
- this.position.x -= SCROLL_INCREMENT;
- }
+ this.window_.dispatchEvent(new CustomEvent('scroll-proceeded-for-testing'));
}
/**
@@ -1098,21 +1109,23 @@ export class Viewport {
* @param {boolean} formFieldFocused
* @private
*/
- arrowRightHandler_(e, formFieldFocused) {
- if (hasKeyModifiers(e)) {
+ arrowLeftRightHandler_(e, formFieldFocused) {
+ if (formFieldFocused || hasKeyModifiers(e)) {
return;
}
- // Go to the next page if there are no horizontal scrollbars and no
- // form field is focused.
- if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) {
- this.goToNextPage();
+ // Go to the previous/next page if there are no horizontal scrollbars.
+ const isRight = e.key === 'ArrowRight';
+ if (!this.documentHasScrollbars().horizontal) {
+ isRight ? this.goToNextPage() : this.goToPreviousPage();
// Since we do the movement of the page.
e.preventDefault();
- } else if (
- /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
- .fromScriptingAPI) {
- this.position.x += SCROLL_INCREMENT;
+ } else if (isCrossFrameKeyEvent(e)) {
+ const scrollOffset = (isRight ? 1 : -1) * SCROLL_INCREMENT;
+ this.setPosition({
+ x: this.position.x + scrollOffset,
+ y: this.position.y,
+ });
}
}
@@ -1122,20 +1135,21 @@ export class Viewport {
* @private
*/
arrowUpDownHandler_(e, formFieldFocused) {
- if (hasKeyModifiers(e)) {
+ if (formFieldFocused || hasKeyModifiers(e)) {
return;
}
- // Go to the previous/next page if Presentation mode is on and no form field
- // is focused.
- if (!(document.fullscreenElement === null || formFieldFocused)) {
- e.key === 'ArrowDown' ? this.goToNextPage() : this.goToPreviousPage();
+ // Go to the previous/next page if Presentation mode is on.
+ const isDown = e.key === 'ArrowDown';
+ if (document.fullscreenElement !== null) {
+ isDown ? this.goToNextPage() : this.goToPreviousPage();
e.preventDefault();
- } else if (
- /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e)
- .fromScriptingAPI) {
- const direction = e.key === 'ArrowDown' ? 1 : -1;
- this.position.y += direction * SCROLL_INCREMENT;
+ } else if (isCrossFrameKeyEvent(e)) {
+ const scrollOffset = (isDown ? 1 : -1) * SCROLL_INCREMENT;
+ this.setPosition({
+ x: this.position.x,
+ y: this.position.y + scrollOffset,
+ });
}
}
@@ -1154,15 +1168,13 @@ export class Viewport {
this.pageUpDownSpaceHandler_(e, formFieldFocused);
return true;
case 'ArrowLeft':
- this.arrowLeftHandler_(e, formFieldFocused);
+ case 'ArrowRight':
+ this.arrowLeftRightHandler_(e, formFieldFocused);
return true;
case 'ArrowDown':
case 'ArrowUp':
this.arrowUpDownHandler_(e, formFieldFocused);
return true;
- case 'ArrowRight':
- this.arrowRightHandler_(e, formFieldFocused);
- return true;
default:
return false;
}
@@ -1232,10 +1244,10 @@ export class Viewport {
y = currentCoords.y;
}
- this.position = {
+ this.setPosition({
x: (dimensions.x + x) * this.getZoom(),
- y: (dimensions.y + y) * this.getZoom()
- };
+ y: (dimensions.y + y) * this.getZoom(),
+ });
this.updateViewport_();
});
}
@@ -1265,7 +1277,7 @@ export class Viewport {
this.setZoomInternal_(Math.min(
this.defaultZoom_,
this.computeFittingZoom_(this.documentDimensions_, true, false)));
- this.position = {x: 0, y: 0};
+ this.setPosition({x: 0, y: 0});
}
this.contentSizeChanged_();
this.resize_();
@@ -1319,8 +1331,8 @@ export class Viewport {
spaceOnLeft = Math.max(spaceOnLeft, 0);
return {
- x: x * zoom + spaceOnLeft - this.window_.scrollLeft,
- y: insetDimensions.y * zoom - this.window_.scrollTop,
+ x: x * zoom + spaceOnLeft - this.scrollContent_.scrollLeft,
+ y: insetDimensions.y * zoom - this.scrollContent_.scrollTop,
width: insetDimensions.width * zoom,
height: insetDimensions.height * zoom
};
@@ -1384,7 +1396,7 @@ export class Viewport {
}
if (changed) {
- this.position = newPosition;
+ this.setPosition(newPosition);
}
}
@@ -1441,8 +1453,7 @@ export class Viewport {
this.documentNeedsScrollbars(this.zoomManager_.applyBrowserZoom(
this.clampZoom_(this.internalZoom_ * scaleDelta)));
- const centerInPlugin = this.frameToPluginCoordinate_(center);
- this.pinchCenter_ = centerInPlugin;
+ this.pinchCenter_ = center;
// If there's no horizontal scrolling, keep the content centered so
// the user can't zoom in on the non-content area.
@@ -1462,7 +1473,7 @@ export class Viewport {
this.fittingType_ = FittingType.NONE;
- this.setPinchZoomInternal_(scaleDelta, centerInPlugin);
+ this.setPinchZoomInternal_(scaleDelta, center);
this.updateViewport_();
this.prevScale_ = /** @type {number} */ (startScaleRatio);
});
@@ -1482,7 +1493,7 @@ export class Viewport {
const {center, startScaleRatio} = e.detail;
this.pinchPhase_ = PinchPhase.END;
const scaleDelta = startScaleRatio / this.prevScale_;
- this.pinchCenter_ = this.frameToPluginCoordinate_(center);
+ this.pinchCenter_ = center;
this.setPinchZoomInternal_(scaleDelta, this.pinchCenter_);
this.updateViewport_();
@@ -1511,8 +1522,7 @@ export class Viewport {
window.requestAnimationFrame(() => {
this.pinchPhase_ = PinchPhase.START;
this.prevScale_ = 1;
- this.oldCenterInContent_ =
- this.pluginToContent_(this.frameToPluginCoordinate_(e.detail.center));
+ this.oldCenterInContent_ = this.pluginToContent_(e.detail.center);
const needsScrollbars = this.documentNeedsScrollbars(this.getZoom());
this.keepContentCentered_ = !needsScrollbars.horizontal;
@@ -1551,6 +1561,25 @@ export const PinchPhase = {
const SCROLL_INCREMENT = 40;
/**
+ * Returns whether a keyboard event came from another frame.
+ * @param {!KeyboardEvent} keyEvent
+ * @return {boolean}
+ */
+function isCrossFrameKeyEvent(keyEvent) {
+ // TODO(crbug.com/1279516): Consider moving these properties to a custom
+ // KeyboardEvent subtype, if it doesn't become obsolete entirely.
+ const custom =
+ /**
+ * @type {!{
+ * fromPlugin: (boolean|undefined),
+ * fromScriptingAPI: (boolean|undefined),
+ * }}
+ */
+ (keyEvent);
+ return !!custom.fromPlugin || !!custom.fromScriptingAPI;
+}
+
+/**
* The width of the page shadow around pages in pixels.
* @type {!{top: number, bottom: number, left: number, right: number}}
*/
@@ -1560,3 +1589,282 @@ export const PAGE_SHADOW = {
left: 5,
right: 5
};
+
+/**
+ * A wrapper around the viewport's scrollable content. This abstraction isolates
+ * details concerning internal vs. external scrolling behavior.
+ */
+class ScrollContent {
+ /**
+ * @param {!Element} container The element which contains the scrollable
+ * content.
+ * @param {!Element} sizer The element which represents the size of the
+ * scrollable content.
+ * @param {!Element} content The element which is the parent of the scrollable
+ * content.
+ * @param {number} scrollbarWidth The width of any scrollbars.
+ */
+ constructor(container, sizer, content, scrollbarWidth) {
+ /** @private @const {!Element} */
+ this.container_ = container;
+
+ /** @private @const {!Element} */
+ this.sizer_ = sizer;
+
+ /** @private {?EventTarget} */
+ this.target_ = null;
+
+ /** @private @const {!Element} */
+ this.content_ = content;
+
+ /** @private @const {number} */
+ this.scrollbarWidth_ = scrollbarWidth;
+
+ /** @private {?UnseasonedPdfPluginElement} */
+ this.unseasonedPlugin_ = null;
+
+ /** @private {number} */
+ this.width_ = 0;
+
+ /** @private {number} */
+ this.height_ = 0;
+
+ /** @private {number} */
+ this.scrollLeft_ = 0;
+
+ /** @private {number} */
+ this.scrollTop_ = 0;
+
+ /** @private {number} */
+ this.unackedScrollsToRemote_ = 0;
+ }
+
+ /**
+ * Sets the target for dispatching "scroll" events.
+ * @param {!EventTarget} target
+ */
+ setEventTarget(target) {
+ this.target_ = target;
+ }
+
+ /**
+ * Dispatches a "scroll" event.
+ */
+ dispatchScroll_() {
+ this.target_ && this.target_.dispatchEvent(new Event('scroll'));
+ }
+
+ /**
+ * Sets the contents, switching to scrolling locally.
+ * @param {?Node} content The new contents, or null to clear.
+ */
+ setContent(content) {
+ if (content === null) {
+ this.sizer_.style.display = 'none';
+ return;
+ }
+ this.attachContent_(content);
+
+ // Switch to local content.
+ this.sizer_.style.display = 'block';
+ if (!this.unseasonedPlugin_) {
+ return;
+ }
+ this.unseasonedPlugin_ = null;
+
+ // Synchronize remote state to local.
+ this.updateSize_();
+ this.scrollTo(this.scrollLeft_, this.scrollTop_);
+ }
+
+ /**
+ * Sets the contents, switching to scrolling remotely.
+ * @param {!UnseasonedPdfPluginElement} content The new contents.
+ */
+ setRemoteContent(content) {
+ this.attachContent_(content);
+
+ // Switch to remote content.
+ const previousScrollLeft = this.scrollLeft;
+ const previousScrollTop = this.scrollTop;
+ this.sizer_.style.display = 'none';
+ assert(!this.unseasonedPlugin_);
+ this.unseasonedPlugin_ = content;
+
+ // Synchronize local state to remote.
+ this.updateSize_();
+ this.scrollTo(previousScrollLeft, previousScrollTop);
+ }
+
+ /**
+ * Attaches the contents to the DOM.
+ * @param {!Node} content The new contents.
+ * @private
+ */
+ attachContent_(content) {
+ // We don't actually replace the content in the DOM, as the controller
+ // implementations take care of "removal" in controller-specific ways:
+ //
+ // 1. Plugin content gets added once, then hidden and revealed using CSS.
+ // 2. Ink content gets removed directly from the DOM on unload.
+ if (!content.parentNode) {
+ this.content_.appendChild(content);
+ }
+ assert(content.parentNode === this.content_);
+ }
+
+ /**
+ * Synchronizes scroll position from remote content.
+ * @param {!Point} position
+ */
+ syncScrollFromRemote(position) {
+ if (this.unackedScrollsToRemote_ > 0) {
+ // Don't overwrite scroll position while scrolls-to-remote are pending.
+ // TODO(crbug.com/1246398): Don't need this if we make this synchronous
+ // again, by moving more logic to the plugin frame.
+ return;
+ }
+
+ if (this.scrollLeft_ === position.x && this.scrollTop_ === position.y) {
+ // Don't trigger scroll event if scroll position hasn't changed.
+ return;
+ }
+
+ this.scrollLeft_ = position.x;
+ this.scrollTop_ = position.y;
+ this.dispatchScroll_();
+ }
+
+ /**
+ * Receives acknowledgment of scroll position synchronized to remote content.
+ * @param {!Point} position
+ */
+ ackScrollToRemote(position) {
+ assert(this.unackedScrollsToRemote_ > 0);
+
+ if (--this.unackedScrollsToRemote_ === 0) {
+ // Accept remote adjustment when there are no pending scrolls-to-remote.
+ this.scrollLeft_ = position.x;
+ this.scrollTop_ = position.y;
+ }
+
+ this.dispatchScroll_();
+ }
+
+ /**
+ * Exposes the current content size for testing.
+ * @return {!Size}
+ */
+ get sizeForTesting() {
+ return {
+ width: this.width_,
+ height: this.height_,
+ };
+ }
+
+ /**
+ * Sets the content size.
+ * @param {number} width
+ * @param {number} height
+ */
+ setSize(width, height) {
+ this.width_ = width;
+ this.height_ = height;
+ this.updateSize_();
+ }
+
+ /** @private */
+ updateSize_() {
+ if (this.unseasonedPlugin_) {
+ this.unseasonedPlugin_.postMessage({
+ type: 'updateSize',
+ width: this.width_,
+ height: this.height_,
+ });
+ } else {
+ this.sizer_.style.width = `${this.width_}px`;
+ this.sizer_.style.height = `${this.height_}px`;
+ }
+ }
+
+ /**
+ * Gets the scroll offset from the left edge.
+ * @return {number}
+ */
+ get scrollLeft() {
+ return this.unseasonedPlugin_ ? this.scrollLeft_ :
+ this.container_.scrollLeft;
+ }
+
+ /**
+ * Gets the scroll offset from the top edge.
+ * @return {number}
+ */
+ get scrollTop() {
+ return this.unseasonedPlugin_ ? this.scrollTop_ : this.container_.scrollTop;
+ }
+
+ /**
+ * Scrolls to the given coordinates.
+ * @param {number} x
+ * @param {number} y
+ */
+ scrollTo(x, y) {
+ if (this.unseasonedPlugin_) {
+ // TODO(crbug.com/1277228): Can get NaN if zoom calculations divide by 0.
+ x = Number.isNaN(x) ? 0 : x;
+ y = Number.isNaN(y) ? 0 : y;
+
+ // Clamp coordinates to scroll limits. Note that the order of min() and
+ // max() operations is significant, as each "maximum" can be negative.
+ const maxX = this.maxScroll_(
+ this.width_, this.container_.clientWidth,
+ this.height_ > this.container_.clientHeight);
+ const maxY = this.maxScroll_(
+ this.height_, this.container_.clientHeight,
+ this.width_ > this.container_.clientWidth);
+
+ if (this.container_.dir === 'rtl') {
+ // Right-to-left.
+ x = Math.min(Math.max(-maxX, x), 0);
+ } else {
+ // Left-to-right.
+ x = Math.max(0, Math.min(x, maxX));
+ }
+ y = Math.max(0, Math.min(y, maxY));
+
+ // To match the DOM's scrollTo() behavior, update the scroll position
+ // immediately, but fire the scroll event later (when the remote side
+ // triggers `ackScrollToRemote()`).
+ this.scrollLeft_ = x;
+ this.scrollTop_ = y;
+
+ ++this.unackedScrollsToRemote_;
+ this.unseasonedPlugin_.postMessage({
+ type: 'syncScrollToRemote',
+ x: this.scrollLeft_,
+ y: this.scrollTop_,
+ });
+ } else {
+ this.container_.scrollTo(x, y);
+ }
+ }
+
+ /**
+ * Computes maximum scroll position.
+ * @param {number} maxContent The maximum content dimension.
+ * @param {number} maxContainer The maximum container dimension.
+ * @param {boolean} hasScrollbar Whether to compensate for a scrollbar.
+ * @return {number}
+ * @private
+ */
+ maxScroll_(maxContent, maxContainer, hasScrollbar) {
+ if (hasScrollbar) {
+ maxContainer -= this.scrollbarWidth_;
+ }
+
+ // This may return a negative value, which is fine because scroll positions
+ // are clamped to a minimum of 0.
+ return maxContent - maxContainer;
+ }
+}
diff --git a/chromium/chrome/browser/resources/pdf/viewport_scroller.js b/chromium/chrome/browser/resources/pdf/viewport_scroller.js
index 8d0186ce3d9..321a61f8e8a 100644
--- a/chromium/chrome/browser/resources/pdf/viewport_scroller.js
+++ b/chromium/chrome/browser/resources/pdf/viewport_scroller.js
@@ -56,7 +56,7 @@ export class ViewportScroller {
ViewportScroller.DRAG_TIMER_INTERVAL_MS_;
position.y += (this.scrollVelocity_.y * timeAdjustment);
position.x += (this.scrollVelocity_.x * timeAdjustment);
- this.viewport_.position = position;
+ this.viewport_.setPosition(position);
this.lastFrameTime_ = currentFrameTime;
}