0

PDF Viewer: Add UI to download edited PDF

- Add action menu providing options to download the original or edited
  PDF
- Menu only is shown if there are edits and the "SaveEditedPDFForm"
  feature is enabled.

Bug: 1078543
Change-Id: I561175c7608b747c15acd03123aca66761c8157c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2213240
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: dpapad <dpapad@chromium.org>
Commit-Queue: Rebekah Potter <rbpotter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#777492}
This commit is contained in:
rbpotter
2020-06-11 21:22:02 +00:00
committed by Commit Bot
parent dfc1f2ff8a
commit 5bc2e02253
15 changed files with 270 additions and 51 deletions

@ -51,6 +51,8 @@ void AddStrings(base::Value* dict) {
{"errorDialogTitle", IDS_PDF_ERROR_DIALOG_TITLE},
{"pageReload", IDS_PDF_PAGE_RELOAD_BUTTON},
{"bookmarks", IDS_PDF_BOOKMARKS},
{"downloadEdited", IDS_PDF_DOWNLOAD_EDITED},
{"downloadOriginal", IDS_PDF_DOWNLOAD_ORIGINAL},
{"labelPageNumber", IDS_PDF_LABEL_PAGE_NUMBER},
{"tooltipRotateCW", IDS_PDF_TOOLTIP_ROTATE_CW},
{"tooltipDownload", IDS_PDF_TOOLTIP_DOWNLOAD},

@ -86,6 +86,7 @@ js_library("toolbar_manager") {
js_library("controller") {
deps = [
":annotation_tool",
":constants",
":viewport",
"elements:viewer-pdf-toolbar",
"//ui/webui/resources/js:assert.m",

@ -21,3 +21,14 @@ export const TwoUpViewAction = {
TWO_UP_VIEW_ENABLE: 'two-up-view-enable',
TWO_UP_VIEW_DISABLE: 'two-up-view-disable',
};
/**
* Enumeration of save message request types. Must Match SaveRequestType in
* pdf/out_of_process_instance.h.
* @enum {number}
*/
export const SaveRequestType = {
ANNOTATION: 0,
ORIGINAL: 1,
EDITED: 2,
};

@ -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 {$} from 'chrome://resources/js/util.m.js';
import {SaveRequestType} from './constants.js';
import {PartialPoint, Point, Viewport} from './viewport.js';
/** @typedef {{ type: string }} */
@ -105,12 +106,13 @@ export class ContentController {
/**
* Requests that the current document be saved.
* @param {boolean} requireResult whether a response is required, otherwise
* the controller may save the document to disk internally.
* @param {!SaveRequestType} requestType The type of save request. If
* ANNOTATION, a response is required, otherwise the controller may save
* the document to disk internally.
* @return {Promise<{fileName: string, dataToSave: ArrayBuffer}>}
* @abstract
*/
save(requireResult) {}
save(requestType) {}
/**
* Loads PDF document from `data` activates UI.
@ -186,7 +188,7 @@ export class InkController extends ContentController {
}
/** @override */
save(requireResult) {
save(requestType) {
return this.inkHost_.saveDocument();
}
@ -403,11 +405,15 @@ export class PluginController extends ContentController {
}
/** @override */
save(requireResult) {
save(requestType) {
const resolver = new PromiseResolver();
const newToken = createToken();
this.pendingTokens_.set(newToken, resolver);
this.postMessage_({type: 'save', token: newToken, force: requireResult});
this.postMessage_({
type: 'save',
token: newToken,
saveRequestType: requestType,
});
return resolver.promise;
}
@ -416,6 +422,7 @@ export class PluginController extends ContentController {
const url = URL.createObjectURL(new Blob([data]));
this.plugin_.removeAttribute('headers');
this.plugin_.setAttribute('stream-url', url);
this.plugin_.setAttribute('has-edits', '');
this.plugin_.style.display = 'block';
try {
await this.getLoadedCallback_();

@ -79,6 +79,8 @@ js_library("viewer-pdf-toolbar") {
":viewer-page-selector",
":viewer-toolbar-dropdown",
"..:annotation_tool",
"..:constants",
"//ui/webui/resources/cr_elements/cr_action_menu:cr_action_menu.m",
"//ui/webui/resources/js:assert.m",
"//ui/webui/resources/js:load_time_data.m",
]

@ -190,8 +190,16 @@
title="$i18n{tooltipRotateCW}"></cr-icon-button>
<cr-icon-button id="download" iron-icon="cr:file-download"
on-click="download" aria-label$="$i18n{tooltipDownload}"
on-click="onDownloadClick_" aria-label$="$i18n{tooltipDownload}"
title="$i18n{tooltipDownload}"></cr-icon-button>
<cr-action-menu id="downloadMenu">
<button class="dropdown-item" on-click="onDownloadEditedClick_">
$i18n{downloadEdited}
</button>
<button class="dropdown-item" on-click="onDownloadOriginalClick_">
$i18n{downloadOriginal}
</button>
</cr-action-menu>
<cr-icon-button id="print" iron-icon="cr:print" on-click="print"
hidden="[[!printingEnabled_]]" title="$i18n{tooltipPrint}"

@ -10,15 +10,17 @@ import './icons.js';
import './viewer-bookmark.js';
import './viewer-page-selector.js';
import './viewer-toolbar-dropdown.js';
// <if expr="chromeos">
import './viewer-pen-options.js';
// </if>
import {AnchorAlignment} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Bookmark} from '../bookmark_type.js';
import {SaveRequestType} from '../constants.js';
Polymer({
is: 'viewer-pdf-toolbar',
@ -118,6 +120,18 @@ Polymer({
/** @type {?Object} */
animation_: null,
/** @private {boolean} */
hasEdits_: false,
/** @private {boolean} */
hasAnnotations_: false,
/**
* Whether the PDF Form save feature is enabled.
* @private {boolean}
*/
pdfFormSaveEnabled_: false,
/**
* @param {number} newProgress
* @param {number} oldProgress
@ -174,6 +188,10 @@ Polymer({
}
},
setIsEditing() {
this.hasEdits_ = true;
},
selectPageNumber() {
this.$.pageselector.select();
},
@ -181,7 +199,8 @@ Polymer({
/** @return {boolean} Whether the toolbar should be kept open. */
shouldKeepOpen() {
return this.$.bookmarks.dropdownOpen || this.loadProgress < 100 ||
this.$.pageselector.isActive() || this.annotationMode;
this.$.pageselector.isActive() || this.annotationMode ||
this.$.downloadMenu.open;
},
/** @return {boolean} Whether a dropdown was open and was hidden. */
@ -213,8 +232,33 @@ Polymer({
this.fire('rotate-right');
},
download() {
this.fire('save');
/** @private */
onDownloadClick_() {
if (!this.hasAnnotations_ &&
(!this.hasEdits_ || !this.pdfFormSaveEnabled_)) {
this.fire('save', SaveRequestType.ORIGINAL);
return;
}
this.$.downloadMenu.showAt(this.$.download, {
anchorAlignmentX: AnchorAlignment.CENTER,
anchorAlignmentY: AnchorAlignment.AFTER_END,
noOffset: true,
});
},
/** @private */
onDownloadOriginalClick_() {
this.fire('save', SaveRequestType.ORIGINAL);
this.$.downloadMenu.close();
},
/** @private */
onDownloadEditedClick_() {
this.fire(
'save',
this.hasAnnotations_ ? SaveRequestType.ANNOTATION :
SaveRequestType.EDITED);
this.$.downloadMenu.close();
},
print() {
@ -232,6 +276,7 @@ Polymer({
toggleAnnotation() {
this.annotationMode = !this.annotationMode;
if (this.annotationMode) {
this.hasAnnotations_ = true;
// Select pen tool when entering annotation mode.
this.updateAnnotationTool_(/** @type {!HTMLElement} */ (this.$.pen));
}
@ -300,6 +345,7 @@ Polymer({
this.pdfAnnotationsEnabled_ =
loadTimeData.getBoolean('pdfAnnotationsEnabled');
this.pdfFormSaveEnabled_ = loadTimeData.getBoolean('pdfFormSaveEnabled');
this.printingEnabled_ = loadTimeData.getBoolean('printingEnabled');
},
});

@ -10,7 +10,7 @@ import {$, hasKeyModifiers, isRTL} from 'chrome://resources/js/util.m.js';
import {Bookmark} from './bookmark_type.js';
import {BrowserApi} from './browser_api.js';
import {FittingType, TwoUpViewAction} from './constants.js';
import {FittingType, SaveRequestType, TwoUpViewAction} from './constants.js';
import {ContentController, InkController, MessageData, PluginController, PrintPreviewParams} from './controller.js';
import {FitToChangedEvent} from './elements/viewer-zoom-toolbar.js';
import {PDFMetrics} from './metrics.js';
@ -95,6 +95,12 @@ export class PDFViewer {
/** @private {boolean} */
this.hasEnteredAnnotationMode_ = false;
/** @private {boolean} */
this.annotationMode_ = false;
/** @private {boolean} */
this.hasEdits_ = false;
/** @private {boolean} */
this.hadPassword_ = false;
@ -231,7 +237,7 @@ export class PDFViewer {
if (toolbarEnabled) {
this.toolbar_ = /** @type {!ViewerPdfToolbarElement} */ ($('toolbar'));
this.toolbar_.hidden = false;
this.toolbar_.addEventListener('save', () => this.save_());
this.toolbar_.addEventListener('save', e => this.save_(e.detail));
this.toolbar_.addEventListener('print', () => this.print_());
this.toolbar_.addEventListener(
'undo', () => this.currentController_.undo());
@ -490,6 +496,7 @@ export class PDFViewer {
*/
async annotationModeToggled_(e) {
const annotationMode = e.detail.value;
this.annotationMode_ = annotationMode;
this.zoomToolbar_.annotationMode = annotationMode;
if (annotationMode) {
// Enter annotation mode.
@ -497,8 +504,9 @@ export class PDFViewer {
// TODO(dstockwell): set plugin read-only, begin transition
this.updateProgress_(0);
// TODO(dstockwell): handle save failure
const saveResult = await this.pluginController_.save(true);
// Data always exists when save is called with requireResult = true.
const saveResult =
await this.pluginController_.save(SaveRequestType.ANNOTATION);
// Data always exists when save is called with requestType = ANNOTATION.
const result = /** @type {!RequiredSaveResult} */ (saveResult);
if (result.hasUnsavedChanges) {
assert(!loadTimeData.getBoolean('pdfFormSaveEnabled'));
@ -508,6 +516,7 @@ export class PDFViewer {
// The user aborted entering annotation mode. Revert to the plugin.
this.toolbar_.annotationMode = false;
this.zoomToolbar_.annotationMode = false;
this.annotationMode_ = false;
this.updateProgress_(100);
return;
}
@ -535,8 +544,9 @@ export class PDFViewer {
this.inkController_.unload();
});
// TODO(dstockwell): handle save failure
const saveResult = await this.inkController_.save(true);
// Data always exists when save is called with requireResult = true.
const saveResult =
await this.inkController_.save(SaveRequestType.ANNOTATION);
// Data always exists when save is called with requestType = ANNOTATION.
const result = /** @type {!RequiredSaveResult} */ (saveResult);
await this.pluginController_.load(result.fileName, result.dataToSave);
// Ensure the plugin gets the initial viewport.
@ -1061,6 +1071,11 @@ export class PDFViewer {
this.setDocumentMetadata_(
metadata.title, metadata.bookmarks, metadata.canSerializeDocument);
return;
case 'setIsEditing':
// Editing mode can only be entered once, and cannot be exited.
this.hasEdits_ = true;
this.toolbar_.setIsEditing();
return;
case 'setIsSelecting':
this.setIsSelecting_(
/** @type {{ isSelecting: boolean }} */ (data).isSelecting);
@ -1234,24 +1249,46 @@ export class PDFViewer {
return;
}
this.save_();
let saveMode;
if (this.hasEnteredAnnotationMode_) {
saveMode = SaveRequestType.ANNOTATION;
} else if (
loadTimeData.getBoolean('pdfFormSaveEnabled') && this.hasEdits_) {
saveMode = SaveRequestType.EDITED;
} else {
saveMode = SaveRequestType.ORIGINAL;
}
this.save_(saveMode);
}
/**
* Saves the current PDF document to disk.
* @param {SaveRequestType} requestType The type of save request.
* @private
*/
async save_() {
async save_(requestType) {
PDFMetrics.record(PDFMetrics.UserAction.SAVE);
if (this.hasEnteredAnnotationMode_) {
// If we have entered annotation mode we must require the local
// contents to ensure annotations are saved, unless the user specifically
// requested the original document. Otherwise we would save the cached
// remote copy without annotations.
if (requestType === SaveRequestType.ANNOTATION) {
PDFMetrics.record(PDFMetrics.UserAction.SAVE_WITH_ANNOTATION);
}
// If we have entered annotation mode we must require the local
// contents to ensure annotations are saved. Otherwise we would
// save the cached or remote copy without annotatios.
const requireResult = this.hasEnteredAnnotationMode_;
// Always send requests of type ORIGINAL to the plugin controller, not the
// ink controller. The ink controller always saves the edited document.
// TODO(dstockwell): Report an error to user if this fails.
const result = await this.currentController_.save(requireResult);
let result;
if (requestType !== SaveRequestType.ORIGINAL || !this.annotationMode_) {
result = await this.currentController_.save(requestType);
} else {
// Request type original in annotation mode --> need to exit annotation
// mode before saving. See https://crbug.com/919364.
await this.exitAnnotationMode_();
assert(!this.annotationMode_);
result = await this.currentController_.save(SaveRequestType.ORIGINAL);
}
if (result == null) {
// The content controller handled the save internally.
return;

@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {FittingType} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/constants.js';
import {FittingType, SaveRequestType} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/constants.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {createBookmarksForTest} from './test_util.js';
@ -304,7 +305,80 @@ const tests = [
chrome.test.assertTrue(button.ironIcon.endsWith(fitPageIcon));
chrome.test.succeed();
}
},
/**
* Test that the toolbar shows an option to download the edited PDF if
* available.
*/
function testEditedPdfOption() {
const pdfToolbar = document.createElement('viewer-pdf-toolbar');
document.body.appendChild(pdfToolbar);
const downloadButton = pdfToolbar.$.download;
const actionMenu = pdfToolbar.$.downloadMenu;
chrome.test.assertFalse(actionMenu.open);
loadTimeData.overrideValues({pdfFormSaveEnabled: false});
pdfToolbar.strings = Object.assign({}, pdfToolbar.strings);
let lastRequestType;
let numRequests = 0;
pdfToolbar.addEventListener('save', e => {
lastRequestType = e.detail;
numRequests++;
});
// No edits, and feature is off.
downloadButton.click();
chrome.test.assertFalse(actionMenu.open);
chrome.test.assertEq(SaveRequestType.ORIGINAL, lastRequestType);
chrome.test.assertEq(1, numRequests);
// Reset.
lastRequestType = SaveRequestType.EDITED;
// Still does not show the menu if there are no edits.
loadTimeData.overrideValues({pdfFormSaveEnabled: true});
pdfToolbar.strings = Object.assign({}, pdfToolbar.strings);
downloadButton.click();
chrome.test.assertFalse(actionMenu.open);
chrome.test.assertEq(SaveRequestType.ORIGINAL, lastRequestType);
chrome.test.assertEq(2, numRequests);
// Set editing mode. Now, clicking download opens the menu.
pdfToolbar.setIsEditing();
downloadButton.click();
chrome.test.assertTrue(actionMenu.open);
chrome.test.assertEq(2, numRequests);
// Click on "Edited".
const buttons = pdfToolbar.shadowRoot.querySelectorAll('button');
buttons[0].click();
chrome.test.assertEq(SaveRequestType.EDITED, lastRequestType);
chrome.test.assertFalse(actionMenu.open);
chrome.test.assertEq(3, numRequests);
// Click again.
downloadButton.click();
chrome.test.assertTrue(actionMenu.open);
chrome.test.assertEq(3, numRequests);
// Click on "Original".
buttons[1].click();
chrome.test.assertEq(SaveRequestType.ORIGINAL, lastRequestType);
chrome.test.assertFalse(actionMenu.open);
chrome.test.assertEq(4, numRequests);
// Even if the document has been edited, always download the original if the
// feature flag is off.
loadTimeData.overrideValues({pdfFormSaveEnabled: false});
pdfToolbar.strings = Object.assign({}, pdfToolbar.strings);
downloadButton.click();
chrome.test.assertFalse(actionMenu.open);
chrome.test.assertEq(SaveRequestType.ORIGINAL, lastRequestType);
chrome.test.assertEq(5, numRequests);
chrome.test.succeed();
},
];
chrome.test.runTests(tests);

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<grit-part>
<if expr="enable_plugins">
<message name="IDS_PDF_DOWNLOAD_ORIGINAL" desc="The label for the menu option to download the original, unedited PDF document.">
Original document
</message>
<message name="IDS_PDF_DOWNLOAD_EDITED" desc="The label for the menu option to download the edited PDF document.">
Edited document
</message>
<message name="IDS_PDF_NEED_PASSWORD" desc="A message asking the user for a password to open a PDF file.">
This document is password protected. Please enter a password.
</message>

@ -0,0 +1 @@
275fe039a311149fd0a2bafc5145669d2c58dd50

@ -0,0 +1 @@
dfa4cded7d1e26aa6ab6e624c04df96572a69460

@ -109,7 +109,7 @@ constexpr char kJSPrintType[] = "print";
// Save (Page -> Plugin)
constexpr char kJSSaveType[] = "save";
constexpr char kJSToken[] = "token";
constexpr char kJSForce[] = "force";
constexpr char kJSSaveRequestType[] = "saveRequestType";
// Save Data (Plugin -> Page)
constexpr char kJSSaveDataType[] = "saveData";
constexpr char kJSFileName[] = "fileName";
@ -182,6 +182,9 @@ constexpr char kJSNamedDestinationPageNumber[] = "pageNumber";
constexpr char kJSSetIsSelectingType[] = "setIsSelecting";
constexpr char kJSIsSelecting[] = "isSelecting";
// Editing forms in document (Plugin -> Page)
constexpr char kJSSetIsEditingType[] = "setIsEditing";
// Notify when a form field is focused (Plugin -> Page)
constexpr char kJSFieldFocusType[] = "formFocusChange";
constexpr char kJSFieldFocus[] = "focused";
@ -487,6 +490,7 @@ bool OutOfProcessInstance::Init(uint32_t argc,
text_input_ = std::make_unique<pp::TextInput_Dev>(this);
bool enable_javascript = true;
bool has_edits = false;
const char* stream_url = nullptr;
const char* original_url = nullptr;
const char* top_level_url = nullptr;
@ -509,6 +513,8 @@ bool OutOfProcessInstance::Init(uint32_t argc,
} else if (strcmp(argn[i], "javascript") == 0) {
if (base::FeatureList::IsEnabled(features::kPdfHonorJsContentSettings))
enable_javascript = (strcmp(argv[i], "allow") == 0);
} else if (strcmp(argn[i], "has-edits") == 0) {
has_edits = true;
}
if (!success)
return false;
@ -531,6 +537,7 @@ bool OutOfProcessInstance::Init(uint32_t argc,
LoadUrl(stream_url, /*is_print_preview=*/false);
url_ = original_url;
edit_mode_ = has_edits;
pp::PDF::SetCrashData(GetPluginInstance(), original_url, top_level_url);
return engine_->New(original_url, headers);
}
@ -680,19 +687,27 @@ void OutOfProcessInstance::HandleMessage(const pp::Var& message) {
Print();
} else if (type == kJSSaveType) {
if (!(dict.Get(pp::Var(kJSToken)).is_string() &&
dict.Get(pp::Var(kJSForce)).is_bool())) {
dict.Get(pp::Var(kJSSaveRequestType)).is_int())) {
NOTREACHED();
return;
}
const bool force = dict.Get(pp::Var(kJSForce)).AsBool();
if (force) {
// |force| being true means the user has entered annotation mode. In which
// case, assume the user will make edits and prefer saving using the
// plugin data.
pp::PDF::SetPluginCanSave(this, true);
SaveToBuffer(dict.Get(pp::Var(kJSToken)).AsString());
} else {
SaveToFile(dict.Get(pp::Var(kJSToken)).AsString());
const SaveRequestType request_type = static_cast<SaveRequestType>(
dict.Get(pp::Var(kJSSaveRequestType)).AsInt());
switch (request_type) {
case SaveRequestType::kAnnotation:
// In annotation mode, assume the user will make edits and prefer saving
// using the plugin data.
pp::PDF::SetPluginCanSave(this, true);
SaveToBuffer(dict.Get(pp::Var(kJSToken)).AsString());
break;
case SaveRequestType::kOriginal:
pp::PDF::SetPluginCanSave(this, false);
SaveToFile(dict.Get(pp::Var(kJSToken)).AsString());
pp::PDF::SetPluginCanSave(this, CanSaveEdits());
break;
case SaveRequestType::kEdited:
SaveToBuffer(dict.Get(pp::Var(kJSToken)).AsString());
break;
}
} else if (type == kJSRotateClockwiseType) {
RotateClockwise();
@ -1470,7 +1485,7 @@ void OutOfProcessInstance::GetDocumentPassword(
PostMessage(message);
}
bool OutOfProcessInstance::ShouldSaveEdits() const {
bool OutOfProcessInstance::CanSaveEdits() const {
return edit_mode_ &&
base::FeatureList::IsEnabled(features::kSaveEditedPDFForm);
}
@ -1488,7 +1503,7 @@ void OutOfProcessInstance::SaveToBuffer(const std::string& token) {
edit_mode_ && !base::FeatureList::IsEnabled(features::kSaveEditedPDFForm);
message.Set(kJSHasUnsavedChanges, pp::Var(has_unsaved_changes));
if (ShouldSaveEdits()) {
if (CanSaveEdits()) {
std::vector<uint8_t> data = engine_->GetSaveData();
if (IsSaveDataSizeValid(data.size())) {
pp::VarArrayBuffer buffer(data.size());
@ -1514,14 +1529,9 @@ void OutOfProcessInstance::SaveToBuffer(const std::string& token) {
}
void OutOfProcessInstance::SaveToFile(const std::string& token) {
if (!ShouldSaveEdits()) {
engine_->KillFormFocus();
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
return;
}
SaveToBuffer(token);
engine_->KillFormFocus();
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
}
void OutOfProcessInstance::ConsumeSaveToken(const std::string& token) {
@ -1934,7 +1944,12 @@ void OutOfProcessInstance::IsSelectingChanged(bool is_selecting) {
void OutOfProcessInstance::IsEditModeChanged(bool is_edit_mode) {
edit_mode_ = is_edit_mode;
pp::PDF::SetPluginCanSave(this, ShouldSaveEdits());
pp::PDF::SetPluginCanSave(this, CanSaveEdits());
if (CanSaveEdits()) {
pp::VarDictionary message;
message.Set(kType, kJSSetIsEditingType);
PostMessage(message);
}
}
float OutOfProcessInstance::GetToolbarHeightInScreenCoords() {

@ -186,7 +186,7 @@ class OutOfProcessInstance : public pp::Instance,
// frame's origin.
pp::URLLoader CreateURLLoaderInternal();
bool ShouldSaveEdits() const;
bool CanSaveEdits() const;
void SaveToFile(const std::string& token);
void SaveToBuffer(const std::string& token);
void ConsumeSaveToken(const std::string& token);
@ -212,6 +212,13 @@ class OutOfProcessInstance : public pp::Instance,
LOAD_STATE_FAILED,
};
// Must match SaveRequestType in chrome/browser/resources/pdf/constants.js.
enum class SaveRequestType {
kAnnotation = 0,
kOriginal = 1,
kEdited = 2,
};
// Set new zoom scale.
void SetZoom(double scale);

@ -14,6 +14,7 @@
* minY: (number|undefined),
* maxX: (number|undefined),
* maxY: (number|undefined),
* noOffset: (boolean|undefined),
* }}
*/
let ShowAtConfig;
@ -344,7 +345,7 @@ Polymer({
const rect = this.anchorElement_.getBoundingClientRect();
let height = rect.height;
if (opt_config &&
if (opt_config && !opt_config.noOffset &&
opt_config.anchorAlignmentY === AnchorAlignment.AFTER_END) {
// When an action menu is positioned after the end of an element, the
// action menu can appear too far away from the anchor element, typically