diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2022-02-04 17:20:24 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2022-02-12 08:15:25 +0000 |
commit | 8fa0776f1f79e91fc9c0b9c1ba11a0a29c05196b (patch) | |
tree | 788d8d7549712682703a0310ca4a0f0860d4802b /chromium/chrome/browser/resources/pdf | |
parent | 606d85f2a5386472314d39923da28c70c60dc8e7 (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')
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; } |