[Editor] Add a new editor to highlight some text in a pdf (bug 1866119)
This patch is first big step for the new highlight feature. Few patches will follow in order to conform to the specs UX/UI gave us.
This commit is contained in:
parent
4bf7ff2027
commit
1ea6293923
@ -81,6 +81,10 @@
|
||||
"description": "Whether to allow execution of active content (JavaScript) by PDF files.",
|
||||
"default": false
|
||||
},
|
||||
"enableHighlightEditor": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"disableRange": {
|
||||
"title": "Disable range requests",
|
||||
"description": "Whether to disable range requests (not recommended).",
|
||||
|
@ -21,10 +21,12 @@
|
||||
/** @typedef {import("../../../web/interfaces").IL10n} IL10n */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
|
||||
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
|
||||
|
||||
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { FreeTextEditor } from "./freetext.js";
|
||||
import { HighlightEditor } from "./highlight.js";
|
||||
import { InkEditor } from "./ink.js";
|
||||
import { setLayerDimensions } from "../display_utils.js";
|
||||
import { StampEditor } from "./stamp.js";
|
||||
@ -39,6 +41,8 @@ import { StampEditor } from "./stamp.js";
|
||||
* @property {number} pageIndex
|
||||
* @property {IL10n} l10n
|
||||
* @property {AnnotationLayer} [annotationLayer]
|
||||
* @property {HTMLDivElement} [textLayer]
|
||||
* @property {DrawLayer} drawLayer
|
||||
* @property {PageViewport} viewport
|
||||
*/
|
||||
|
||||
@ -59,10 +63,14 @@ class AnnotationEditorLayer {
|
||||
|
||||
#boundPointerup = this.pointerup.bind(this);
|
||||
|
||||
#boundPointerUpAfterSelection = this.pointerUpAfterSelection.bind(this);
|
||||
|
||||
#boundPointerdown = this.pointerdown.bind(this);
|
||||
|
||||
#editorFocusTimeoutId = null;
|
||||
|
||||
#boundSelectionStart = this.selectionStart.bind(this);
|
||||
|
||||
#editors = new Map();
|
||||
|
||||
#hadPointerDown = false;
|
||||
@ -71,12 +79,14 @@ class AnnotationEditorLayer {
|
||||
|
||||
#isDisabling = false;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#uiManager;
|
||||
|
||||
static _initialized = false;
|
||||
|
||||
static #editorTypes = new Map(
|
||||
[FreeTextEditor, InkEditor, StampEditor].map(type => [
|
||||
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
|
||||
type._editorType,
|
||||
type,
|
||||
])
|
||||
@ -91,6 +101,8 @@ class AnnotationEditorLayer {
|
||||
div,
|
||||
accessibilityManager,
|
||||
annotationLayer,
|
||||
drawLayer,
|
||||
textLayer,
|
||||
viewport,
|
||||
l10n,
|
||||
}) {
|
||||
@ -109,6 +121,8 @@ class AnnotationEditorLayer {
|
||||
this.#accessibilityManager = accessibilityManager;
|
||||
this.#annotationLayer = annotationLayer;
|
||||
this.viewport = viewport;
|
||||
this.#textLayer = textLayer;
|
||||
this.drawLayer = drawLayer;
|
||||
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
@ -131,11 +145,23 @@ class AnnotationEditorLayer {
|
||||
*/
|
||||
updateMode(mode = this.#uiManager.getMode()) {
|
||||
this.#cleanup();
|
||||
if (mode === AnnotationEditorType.INK) {
|
||||
// We always want to an ink editor ready to draw in.
|
||||
switch (mode) {
|
||||
case AnnotationEditorType.INK:
|
||||
// We always want to have an ink editor ready to draw in.
|
||||
this.addInkEditorIfNeeded(false);
|
||||
|
||||
this.disableTextSelection();
|
||||
this.togglePointerEvents(true);
|
||||
this.disableClick();
|
||||
} else {
|
||||
break;
|
||||
case AnnotationEditorType.HIGHLIGHT:
|
||||
this.enableTextSelection();
|
||||
this.togglePointerEvents(false);
|
||||
this.disableClick();
|
||||
break;
|
||||
default:
|
||||
this.disableTextSelection();
|
||||
this.togglePointerEvents(true);
|
||||
this.enableClick();
|
||||
}
|
||||
|
||||
@ -272,6 +298,7 @@ class AnnotationEditorLayer {
|
||||
for (const editorType of AnnotationEditorLayer.#editorTypes.values()) {
|
||||
classList.remove(`${editorType._type}Editing`);
|
||||
}
|
||||
this.disableTextSelection();
|
||||
|
||||
this.#isDisabling = false;
|
||||
}
|
||||
@ -293,6 +320,18 @@ class AnnotationEditorLayer {
|
||||
this.#uiManager.setActiveEditor(editor);
|
||||
}
|
||||
|
||||
enableTextSelection() {
|
||||
if (this.#textLayer?.div) {
|
||||
document.addEventListener("selectstart", this.#boundSelectionStart);
|
||||
}
|
||||
}
|
||||
|
||||
disableTextSelection() {
|
||||
if (this.#textLayer?.div) {
|
||||
document.removeEventListener("selectstart", this.#boundSelectionStart);
|
||||
}
|
||||
}
|
||||
|
||||
enableClick() {
|
||||
this.div.addEventListener("pointerdown", this.#boundPointerdown);
|
||||
this.div.addEventListener("pointerup", this.#boundPointerup);
|
||||
@ -458,18 +497,24 @@ class AnnotationEditorLayer {
|
||||
return this.#uiManager.getId();
|
||||
}
|
||||
|
||||
get #currentEditorType() {
|
||||
return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new editor
|
||||
* @param {Object} params
|
||||
* @returns {AnnotationEditor}
|
||||
*/
|
||||
#createNewEditor(params) {
|
||||
const editorType = AnnotationEditorLayer.#editorTypes.get(
|
||||
this.#uiManager.getMode()
|
||||
);
|
||||
const editorType = this.#currentEditorType;
|
||||
return editorType ? new editorType.prototype.constructor(params) : null;
|
||||
}
|
||||
|
||||
canCreateNewEmptyEditor() {
|
||||
return this.#currentEditorType?.canCreateNewEmptyEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste some content into a new editor.
|
||||
* @param {number} mode
|
||||
@ -512,9 +557,10 @@ class AnnotationEditorLayer {
|
||||
* Create and add a new editor.
|
||||
* @param {PointerEvent} event
|
||||
* @param {boolean} isCentered
|
||||
* @param [Object] data
|
||||
* @returns {AnnotationEditor}
|
||||
*/
|
||||
#createAndAddNewEditor(event, isCentered) {
|
||||
#createAndAddNewEditor(event, isCentered, data = {}) {
|
||||
const id = this.getNextId();
|
||||
const editor = this.#createNewEditor({
|
||||
parent: this,
|
||||
@ -523,6 +569,7 @@ class AnnotationEditorLayer {
|
||||
y: event.offsetY,
|
||||
uiManager: this.#uiManager,
|
||||
isCentered,
|
||||
...data,
|
||||
});
|
||||
if (editor) {
|
||||
this.add(editor);
|
||||
@ -589,6 +636,98 @@ class AnnotationEditorLayer {
|
||||
this.#uiManager.unselect(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectionChange callback.
|
||||
* @param {Event} _event
|
||||
*/
|
||||
selectionStart(_event) {
|
||||
this.#textLayer?.div.addEventListener(
|
||||
"pointerup",
|
||||
this.#boundPointerUpAfterSelection,
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user releases the mouse button after having selected
|
||||
* some text.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
pointerUpAfterSelection(event) {
|
||||
const selection = document.getSelection();
|
||||
if (selection.rangeCount === 0) {
|
||||
return;
|
||||
}
|
||||
const range = selection.getRangeAt(0);
|
||||
if (range.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#textLayer?.div.contains(range.commonAncestorContainer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
x: layerX,
|
||||
y: layerY,
|
||||
width: parentWidth,
|
||||
height: parentHeight,
|
||||
} = this.#textLayer.div.getBoundingClientRect();
|
||||
const bboxes = range.getClientRects();
|
||||
|
||||
// We must rotate the boxes because we want to have them in the non-rotated
|
||||
// page coordinates.
|
||||
let rotator;
|
||||
switch (this.viewport.rotation) {
|
||||
case 90:
|
||||
rotator = (x, y, w, h) => ({
|
||||
x: (y - layerY) / parentHeight,
|
||||
y: 1 - (x + w - layerX) / parentWidth,
|
||||
width: h / parentHeight,
|
||||
height: w / parentWidth,
|
||||
});
|
||||
break;
|
||||
case 180:
|
||||
rotator = (x, y, w, h) => ({
|
||||
x: 1 - (x + w - layerX) / parentWidth,
|
||||
y: 1 - (y + h - layerY) / parentHeight,
|
||||
width: w / parentWidth,
|
||||
height: h / parentHeight,
|
||||
});
|
||||
break;
|
||||
case 270:
|
||||
rotator = (x, y, w, h) => ({
|
||||
x: 1 - (y + h - layerY) / parentHeight,
|
||||
y: (x - layerX) / parentWidth,
|
||||
width: h / parentHeight,
|
||||
height: w / parentWidth,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
rotator = (x, y, w, h) => ({
|
||||
x: (x - layerX) / parentWidth,
|
||||
y: (y - layerY) / parentHeight,
|
||||
width: w / parentWidth,
|
||||
height: h / parentHeight,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
const boxes = [];
|
||||
for (const { x, y, width, height } of bboxes) {
|
||||
if (width === 0 || height === 0) {
|
||||
continue;
|
||||
}
|
||||
boxes.push(rotator(x, y, width, height));
|
||||
}
|
||||
if (boxes.length !== 0) {
|
||||
this.#createAndAddNewEditor(event, false, {
|
||||
boxes,
|
||||
});
|
||||
}
|
||||
selection.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointerup callback.
|
||||
* @param {PointerEvent} event
|
||||
@ -631,6 +770,9 @@ class AnnotationEditorLayer {
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
pointerdown(event) {
|
||||
if (this.#uiManager.getMode() === AnnotationEditorType.HIGHLIGHT) {
|
||||
this.enableTextSelection();
|
||||
}
|
||||
if (this.#hadPointerDown) {
|
||||
// It's possible to have a second pointerdown event before a pointerup one
|
||||
// when the user puts a finger on a touchscreen and then add a second one
|
||||
@ -734,8 +876,15 @@ class AnnotationEditorLayer {
|
||||
// the viewport.
|
||||
this.#uiManager.commitOrRemove();
|
||||
|
||||
const oldRotation = this.viewport.rotation;
|
||||
const rotation = viewport.rotation;
|
||||
this.viewport = viewport;
|
||||
setLayerDimensions(this.div, { rotation: viewport.rotation });
|
||||
setLayerDimensions(this.div, { rotation });
|
||||
if (oldRotation !== rotation) {
|
||||
for (const editor of this.#editors.values()) {
|
||||
editor.rotate(rotation);
|
||||
}
|
||||
}
|
||||
this.updateMode();
|
||||
}
|
||||
|
||||
|
@ -507,7 +507,11 @@ class AnnotationEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fixAndSetPosition() {
|
||||
/**
|
||||
* Fix the position of the editor in order to keep it inside its parent page.
|
||||
* @param {number} [rotation] - the rotation of the page.
|
||||
*/
|
||||
fixAndSetPosition(rotation = this.rotation) {
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
let { x, y, width, height } = this;
|
||||
width *= pageWidth;
|
||||
@ -515,7 +519,7 @@ class AnnotationEditor {
|
||||
x *= pageWidth;
|
||||
y *= pageHeight;
|
||||
|
||||
switch (this.rotation) {
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
x = Math.max(0, Math.min(pageWidth - width, x));
|
||||
y = Math.max(0, Math.min(pageHeight - height, y));
|
||||
@ -1125,14 +1129,28 @@ class AnnotationEditor {
|
||||
|
||||
this.#hasBeenClicked = true;
|
||||
|
||||
if (this._isDraggable) {
|
||||
this.#setUpDragSession(event);
|
||||
}
|
||||
|
||||
#setUpDragSession(event) {
|
||||
if (!this._isDraggable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#selectOnPointerEvent(event);
|
||||
}
|
||||
|
||||
#selectOnPointerEvent(event) {
|
||||
const { isMac } = FeatureTest.platform;
|
||||
if (
|
||||
(event.ctrlKey && !isMac) ||
|
||||
event.shiftKey ||
|
||||
(event.metaKey && isMac)
|
||||
) {
|
||||
this.parent.toggleSelected(this);
|
||||
} else {
|
||||
this.parent.setSelected(this);
|
||||
}
|
||||
}
|
||||
|
||||
#setUpDragSession(event) {
|
||||
const isSelected = this._uiManager.isSelected(this);
|
||||
this._uiManager.setUpDragSession();
|
||||
|
||||
@ -1163,16 +1181,7 @@ class AnnotationEditor {
|
||||
|
||||
this.#hasBeenClicked = false;
|
||||
if (!this._uiManager.endDragSession()) {
|
||||
const { isMac } = FeatureTest.platform;
|
||||
if (
|
||||
(event.ctrlKey && !isMac) ||
|
||||
event.shiftKey ||
|
||||
(event.metaKey && isMac)
|
||||
) {
|
||||
this.parent.toggleSelected(this);
|
||||
} else {
|
||||
this.parent.setSelected(this);
|
||||
}
|
||||
this.#selectOnPointerEvent(event);
|
||||
}
|
||||
};
|
||||
window.addEventListener("pointerup", pointerUpCallback);
|
||||
@ -1204,8 +1213,11 @@ class AnnotationEditor {
|
||||
|
||||
/**
|
||||
* Convert the current rect into a page one.
|
||||
* @param {number} tx - x-translation in screen coordinates.
|
||||
* @param {number} ty - y-translation in screen coordinates.
|
||||
* @param {number} [rotation] - the rotation of the page.
|
||||
*/
|
||||
getRect(tx, ty) {
|
||||
getRect(tx, ty, rotation = this.rotation) {
|
||||
const scale = this.parentScale;
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
const [pageX, pageY] = this.pageTranslation;
|
||||
@ -1216,7 +1228,7 @@ class AnnotationEditor {
|
||||
const width = this.width * pageWidth;
|
||||
const height = this.height * pageHeight;
|
||||
|
||||
switch (this.rotation) {
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
return [
|
||||
x + shiftX + pageX,
|
||||
@ -1332,6 +1344,12 @@ class AnnotationEditor {
|
||||
this.div?.addEventListener("focusout", this.#boundFocusout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the editor.
|
||||
* @param {number} angle
|
||||
*/
|
||||
rotate(_angle) {}
|
||||
|
||||
/**
|
||||
* Serialize the editor.
|
||||
* The result of the serialization will be used to construct a
|
||||
@ -1426,6 +1444,10 @@ class AnnotationEditor {
|
||||
}
|
||||
}
|
||||
|
||||
get toolbarPosition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* onkeydown callback.
|
||||
* @param {KeyboardEvent} event
|
||||
@ -1669,6 +1691,10 @@ class AnnotationEditor {
|
||||
static get MIN_SIZE() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
static canCreateNewEmptyEditor() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// This class is used to fake an editor which has been deleted.
|
||||
|
454
src/display/editor/highlight.js
Normal file
454
src/display/editor/highlight.js
Normal file
@ -0,0 +1,454 @@
|
||||
/* Copyright 2022 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnnotationEditorParamsType,
|
||||
AnnotationEditorType,
|
||||
Util,
|
||||
} from "../../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor.js";
|
||||
import { bindEvents } from "./tools.js";
|
||||
import { Outliner } from "./outliner.js";
|
||||
|
||||
/**
|
||||
* Basic draw editor in order to generate an Highlight annotation.
|
||||
*/
|
||||
class HighlightEditor extends AnnotationEditor {
|
||||
#boxes;
|
||||
|
||||
#clipPathId = null;
|
||||
|
||||
#color;
|
||||
|
||||
#focusOutlines = null;
|
||||
|
||||
#highlightDiv = null;
|
||||
|
||||
#highlightOutlines = null;
|
||||
|
||||
#id = null;
|
||||
|
||||
#lastPoint = null;
|
||||
|
||||
#opacity;
|
||||
|
||||
#outlineId = null;
|
||||
|
||||
static _defaultColor = "#FFF066";
|
||||
|
||||
static _defaultOpacity = 0.4;
|
||||
|
||||
static _l10nPromise;
|
||||
|
||||
static _type = "highlight";
|
||||
|
||||
static _editorType = AnnotationEditorType.HIGHLIGHT;
|
||||
|
||||
constructor(params) {
|
||||
super({ ...params, name: "highlightEditor" });
|
||||
this.#color = params.color || HighlightEditor._defaultColor;
|
||||
this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
|
||||
this.#boxes = params.boxes || null;
|
||||
this._isDraggable = false;
|
||||
|
||||
this.#createOutlines();
|
||||
this.#addToDrawLayer();
|
||||
this.rotate(this.rotation);
|
||||
}
|
||||
|
||||
#createOutlines() {
|
||||
const outliner = new Outliner(this.#boxes, /* borderWidth = */ 0.001);
|
||||
this.#highlightOutlines = outliner.getOutlines();
|
||||
({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
} = this.#highlightOutlines.box);
|
||||
|
||||
const outlinerForOutline = new Outliner(
|
||||
this.#boxes,
|
||||
/* borderWidth = */ 0.0025,
|
||||
/* innerMargin = */ 0.001,
|
||||
this._uiManager.direction === "ltr"
|
||||
);
|
||||
this.#focusOutlines = outlinerForOutline.getOutlines();
|
||||
|
||||
// The last point is in the pages coordinate system.
|
||||
const { lastPoint } = this.#focusOutlines.box;
|
||||
this.#lastPoint = [
|
||||
(lastPoint[0] - this.x) / this.width,
|
||||
(lastPoint[1] - this.y) / this.height,
|
||||
];
|
||||
}
|
||||
|
||||
static initialize(l10n) {
|
||||
AnnotationEditor.initialize(l10n);
|
||||
}
|
||||
|
||||
static updateDefaultParams(type, value) {
|
||||
switch (type) {
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
|
||||
HighlightEditor._defaultColor = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
|
||||
HighlightEditor._defaultOpacity = value / 100;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get toolbarPosition() {
|
||||
return this.#lastPoint;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
updateParams(type, value) {
|
||||
switch (type) {
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
|
||||
this.#updateColor(value);
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
|
||||
this.#updateOpacity(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static get defaultPropertiesToUpdate() {
|
||||
return [
|
||||
[
|
||||
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
|
||||
HighlightEditor._defaultColor,
|
||||
],
|
||||
[
|
||||
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
|
||||
Math.round(HighlightEditor._defaultOpacity * 100),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get propertiesToUpdate() {
|
||||
return [
|
||||
[
|
||||
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
|
||||
this.#color || HighlightEditor._defaultColor,
|
||||
],
|
||||
[
|
||||
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
|
||||
Math.round(100 * (this.#opacity ?? HighlightEditor._defaultOpacity)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the color and make this action undoable.
|
||||
* @param {string} color
|
||||
*/
|
||||
#updateColor(color) {
|
||||
const savedColor = this.color;
|
||||
this.addCommands({
|
||||
cmd: () => {
|
||||
this.#color = color;
|
||||
this.parent.drawLayer.changeColor(this.#id, color);
|
||||
},
|
||||
undo: () => {
|
||||
this.#color = savedColor;
|
||||
this.parent.drawLayer.changeColor(this.#id, savedColor);
|
||||
},
|
||||
mustExec: true,
|
||||
type: AnnotationEditorParamsType.HIGHLIGHT_COLOR,
|
||||
overwriteIfSameType: true,
|
||||
keepUndo: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the opacity and make this action undoable.
|
||||
* @param {number} opacity
|
||||
*/
|
||||
#updateOpacity(opacity) {
|
||||
opacity /= 100;
|
||||
const savedOpacity = this.#opacity;
|
||||
this.addCommands({
|
||||
cmd: () => {
|
||||
this.#opacity = opacity;
|
||||
this.parent.drawLayer.changeOpacity(this.#id, opacity);
|
||||
},
|
||||
undo: () => {
|
||||
this.#opacity = savedOpacity;
|
||||
this.parent.drawLayer.changeOpacity(this.#id, savedOpacity);
|
||||
},
|
||||
mustExec: true,
|
||||
type: AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
|
||||
overwriteIfSameType: true,
|
||||
keepUndo: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
fixAndSetPosition() {
|
||||
return super.fixAndSetPosition(0);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getRect(tx, ty) {
|
||||
return super.getRect(tx, ty, 0);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
onceAdded() {
|
||||
this.parent.addUndoableEditor(this);
|
||||
this.div.focus();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
remove() {
|
||||
super.remove();
|
||||
this.#cleanDrawLayer();
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
rebuild() {
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
super.rebuild();
|
||||
if (this.div === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#addToDrawLayer();
|
||||
|
||||
if (!this.isAttachedToDOM) {
|
||||
// At some point this editor was removed and we're rebuilting it,
|
||||
// hence we must add it to its parent.
|
||||
this.parent.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
if (this.parent && !parent) {
|
||||
this.#cleanDrawLayer();
|
||||
} else if (parent) {
|
||||
this.#addToDrawLayer(parent);
|
||||
}
|
||||
super.setParent(parent);
|
||||
}
|
||||
|
||||
#cleanDrawLayer() {
|
||||
if (this.#id === null || !this.parent) {
|
||||
return;
|
||||
}
|
||||
this.parent.drawLayer.remove(this.#id);
|
||||
this.#id = null;
|
||||
this.parent.drawLayer.remove(this.#outlineId);
|
||||
this.#outlineId = null;
|
||||
}
|
||||
|
||||
#addToDrawLayer(parent = this.parent) {
|
||||
if (this.#id !== null) {
|
||||
return;
|
||||
}
|
||||
({ id: this.#id, clipPathId: this.#clipPathId } =
|
||||
parent.drawLayer.highlight(
|
||||
this.#highlightOutlines,
|
||||
this.#color,
|
||||
this.#opacity
|
||||
));
|
||||
if (this.#highlightDiv) {
|
||||
this.#highlightDiv.style.clipPath = this.#clipPathId;
|
||||
}
|
||||
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
|
||||
}
|
||||
|
||||
static #rotateBbox({ x, y, width, height }, angle) {
|
||||
switch (angle) {
|
||||
case 90:
|
||||
return {
|
||||
x: 1 - y - height,
|
||||
y: x,
|
||||
width: height,
|
||||
height: width,
|
||||
};
|
||||
case 180:
|
||||
return {
|
||||
x: 1 - x - width,
|
||||
y: 1 - y - height,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
case 270:
|
||||
return {
|
||||
x: y,
|
||||
y: 1 - x - width,
|
||||
width: height,
|
||||
height: width,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
rotate(angle) {
|
||||
const { drawLayer } = this.parent;
|
||||
drawLayer.rotate(this.#id, angle);
|
||||
drawLayer.rotate(this.#outlineId, angle);
|
||||
drawLayer.updateBox(this.#id, HighlightEditor.#rotateBbox(this, angle));
|
||||
drawLayer.updateBox(
|
||||
this.#outlineId,
|
||||
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
render() {
|
||||
if (this.div) {
|
||||
return this.div;
|
||||
}
|
||||
|
||||
const div = super.render();
|
||||
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
|
||||
div.append(highlightDiv);
|
||||
highlightDiv.className = "internal";
|
||||
highlightDiv.style.clipPath = this.#clipPathId;
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setDims(this.width * parentWidth, this.height * parentHeight);
|
||||
|
||||
bindEvents(this, this.#highlightDiv, ["pointerover", "pointerleave"]);
|
||||
this.enableEditing();
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
pointerover() {
|
||||
this.parent.drawLayer.addClass(this.#outlineId, "hovered");
|
||||
}
|
||||
|
||||
pointerleave() {
|
||||
this.parent.drawLayer.removeClass(this.#outlineId, "hovered");
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
select() {
|
||||
super.select();
|
||||
this.parent?.drawLayer.removeClass(this.#outlineId, "hovered");
|
||||
this.parent?.drawLayer.addClass(this.#outlineId, "selected");
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
unselect() {
|
||||
super.unselect();
|
||||
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
|
||||
}
|
||||
|
||||
#serializeBoxes() {
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
const boxes = this.#boxes;
|
||||
const quadPoints = new Array(boxes.length * 8);
|
||||
let i = 0;
|
||||
for (const { x, y, width, height } of boxes) {
|
||||
const sx = x * pageWidth;
|
||||
const sy = (1 - y - height) * pageHeight;
|
||||
// The specifications say that the rectangle should start from the bottom
|
||||
// left corner and go counter-clockwise.
|
||||
// But when opening the file in Adobe Acrobat it appears that this isn't
|
||||
// correct hence the 4th and 6th numbers are just swapped.
|
||||
quadPoints[i] = quadPoints[i + 4] = sx;
|
||||
quadPoints[i + 1] = quadPoints[i + 3] = sy;
|
||||
quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth;
|
||||
quadPoints[i + 5] = quadPoints[i + 7] = sy + height * pageHeight;
|
||||
i += 8;
|
||||
}
|
||||
return quadPoints;
|
||||
}
|
||||
|
||||
#serializeOutlines() {
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
const width = this.width * pageWidth;
|
||||
const height = this.height * pageHeight;
|
||||
const tx = this.x * pageWidth;
|
||||
const ty = (1 - this.y - this.height) * pageHeight;
|
||||
const outlines = [];
|
||||
for (const outline of this.#highlightOutlines.outlines) {
|
||||
const points = new Array(outline.length);
|
||||
for (let i = 0; i < outline.length; i += 2) {
|
||||
points[i] = tx + outline[i] * width;
|
||||
points[i + 1] = ty + (1 - outline[i + 1]) * height;
|
||||
}
|
||||
outlines.push(points);
|
||||
}
|
||||
return outlines;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static deserialize(data, parent, uiManager) {
|
||||
const editor = super.deserialize(data, parent, uiManager);
|
||||
|
||||
const { rect, color, quadPoints } = data;
|
||||
editor.#color = Util.makeHexColor(...color);
|
||||
editor.#opacity = data.opacity;
|
||||
|
||||
const [pageWidth, pageHeight] = editor.pageDimensions;
|
||||
editor.width = (rect[2] - rect[0]) / pageWidth;
|
||||
editor.height = (rect[3] - rect[1]) / pageHeight;
|
||||
const boxes = (editor.#boxes = []);
|
||||
for (let i = 0; i < quadPoints.length; i += 8) {
|
||||
boxes.push({
|
||||
x: quadPoints[4] / pageWidth,
|
||||
y: 1 - quadPoints[i + 5] / pageHeight,
|
||||
width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth,
|
||||
height: (quadPoints[i + 5] - quadPoints[i + 1]) / pageHeight,
|
||||
});
|
||||
}
|
||||
editor.#createOutlines();
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
serialize(isForCopying = false) {
|
||||
// It doesn't make sense to copy/paste a highlight annotation.
|
||||
if (this.isEmpty() || isForCopying) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = this.getRect(0, 0);
|
||||
const color = AnnotationEditor._colorManager.convert(this.#color);
|
||||
|
||||
return {
|
||||
annotationType: AnnotationEditorType.HIGHLIGHT,
|
||||
color,
|
||||
opacity: this.#opacity,
|
||||
quadPoints: this.#serializeBoxes(),
|
||||
outlines: this.#serializeOutlines(),
|
||||
pageIndex: this.pageIndex,
|
||||
rect,
|
||||
rotation: 0,
|
||||
structTreeParentId: this._structTreeParentId,
|
||||
};
|
||||
}
|
||||
|
||||
static canCreateNewEmptyEditor() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { HighlightEditor };
|
@ -36,6 +36,19 @@ class EditorToolbar {
|
||||
buttons.className = "buttons";
|
||||
editToolbar.append(buttons);
|
||||
|
||||
const position = this.#editor.toolbarPosition;
|
||||
if (position) {
|
||||
const { style } = editToolbar;
|
||||
const x =
|
||||
this.#editor._uiManager.direction === "ltr"
|
||||
? 1 - position[0]
|
||||
: position[0];
|
||||
style.insetInlineEnd = `${100 * x}%`;
|
||||
style.top = `calc(${
|
||||
100 * position[1]
|
||||
}% + var(--editor-toolbar-vert-offset))`;
|
||||
}
|
||||
|
||||
this.#addDeleteButton();
|
||||
|
||||
return editToolbar;
|
||||
|
@ -1217,8 +1217,10 @@ class AnnotationEditorUIManager {
|
||||
}
|
||||
|
||||
addNewEditorFromKeyboard() {
|
||||
if (this.currentLayer.canCreateNewEmptyEditor()) {
|
||||
this.currentLayer.addNewEditor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the toolbar if it's required to reflect the tool currently used.
|
||||
|
@ -86,6 +86,8 @@ const AnnotationEditorParamsType = {
|
||||
INK_COLOR: 21,
|
||||
INK_THICKNESS: 22,
|
||||
INK_OPACITY: 23,
|
||||
HIGHLIGHT_COLOR: 31,
|
||||
HIGHLIGHT_OPACITY: 32,
|
||||
};
|
||||
|
||||
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
||||
|
@ -13,6 +13,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import url(draw_layer_builder.css);
|
||||
|
||||
:root {
|
||||
--outline-width: 2px;
|
||||
--outline-color: #0060df;
|
||||
@ -188,7 +190,10 @@
|
||||
border: var(--focus-outline-around);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotationEditorLayer
|
||||
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor) {
|
||||
.editToolbar {
|
||||
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
|
||||
--editor-toolbar-bg-color: #f0f0f4;
|
||||
@ -198,6 +203,7 @@
|
||||
--editor-toolbar-active-bg-color: #cfcfd8;
|
||||
--editor-toolbar-focus-outline-color: #0060df;
|
||||
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
|
||||
--editor-toolbar-vert-offset: 6px;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--editor-toolbar-bg-color: #2b2a33;
|
||||
@ -225,10 +231,11 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
pointer-events: auto;
|
||||
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
inset-block-start: calc(100% + 6px);
|
||||
inset-block-start: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: var(--editor-toolbar-bg-color);
|
||||
@ -316,7 +323,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .freeTextEditor .overlay.enabled {
|
||||
.annotationEditorLayer freeTextEditor .overlay.enabled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -513,12 +520,12 @@
|
||||
rotate: 270deg;
|
||||
|
||||
&:dir(ltr) {
|
||||
inset-inline-start: calc(100% + 6px);
|
||||
inset-inline-start: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
inset-block-start: 0;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
inset-inline-end: calc(100% + 6px);
|
||||
inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
inset-block-end: 0;
|
||||
inset-block-start: unset;
|
||||
}
|
||||
@ -547,7 +554,7 @@
|
||||
.editToolbar {
|
||||
rotate: 180deg;
|
||||
inset-inline-start: 0;
|
||||
inset-block-end: calc(100% + 6px);
|
||||
inset-block-end: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
inset-block-start: unset;
|
||||
}
|
||||
}
|
||||
@ -585,13 +592,13 @@
|
||||
rotate: 90deg;
|
||||
|
||||
&:dir(ltr) {
|
||||
inset-inline-end: calc(100% + 6px);
|
||||
inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
inset-block-end: 0;
|
||||
inset-block-start: unset;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
inset-inline-start: calc(100% + 6px);
|
||||
inset-inline-start: calc(100% + var(--editor-toolbar-vert-offset));
|
||||
inset-block-start: 0;
|
||||
}
|
||||
}
|
||||
@ -1000,3 +1007,62 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotationEditorLayer {
|
||||
&[data-main-rotation="0"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
rotate: 0deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="90"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
rotate: 270deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="180"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
rotate: 180deg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-main-rotation="270"] {
|
||||
.highlightEditor > .editToolbar {
|
||||
rotate: 90deg;
|
||||
}
|
||||
}
|
||||
|
||||
.highlightEditor {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
transform-origin: 0 0;
|
||||
cursor: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
transform: none;
|
||||
|
||||
.internal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.selectedEditor {
|
||||
.internal {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.editToolbar {
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,11 +35,17 @@ import { NullL10n } from "web-l10n_utils";
|
||||
* @property {IL10n} [l10n]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationLayer} [annotationLayer]
|
||||
* @property {TextLayer} [textLayer]
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
*/
|
||||
|
||||
class AnnotationEditorLayerBuilder {
|
||||
#annotationLayer = null;
|
||||
|
||||
#drawLayer = null;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#uiManager;
|
||||
|
||||
/**
|
||||
@ -55,6 +61,8 @@ class AnnotationEditorLayerBuilder {
|
||||
this._cancelled = false;
|
||||
this.#uiManager = options.uiManager;
|
||||
this.#annotationLayer = options.annotationLayer || null;
|
||||
this.#textLayer = options.textLayer || null;
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,6 +101,8 @@ class AnnotationEditorLayerBuilder {
|
||||
l10n: this.l10n,
|
||||
viewport: clonedViewport,
|
||||
annotationLayer: this.#annotationLayer,
|
||||
textLayer: this.#textLayer,
|
||||
drawLayer: this.#drawLayer,
|
||||
});
|
||||
|
||||
const parameters = {
|
||||
|
@ -28,6 +28,8 @@ class AnnotationEditorParams {
|
||||
#bindListeners({
|
||||
editorFreeTextFontSize,
|
||||
editorFreeTextColor,
|
||||
editorHighlightColor,
|
||||
editorHighlightOpacity,
|
||||
editorInkColor,
|
||||
editorInkThickness,
|
||||
editorInkOpacity,
|
||||
@ -46,6 +48,12 @@ class AnnotationEditorParams {
|
||||
editorFreeTextColor.addEventListener("input", function () {
|
||||
dispatchEvent("FREETEXT_COLOR", this.value);
|
||||
});
|
||||
editorHighlightColor.addEventListener("input", function () {
|
||||
dispatchEvent("HIGHLIGHT_COLOR", this.value);
|
||||
});
|
||||
editorHighlightOpacity.addEventListener("input", function () {
|
||||
dispatchEvent("HIGHLIGHT_OPACITY", this.valueAsNumber);
|
||||
});
|
||||
editorInkColor.addEventListener("input", function () {
|
||||
dispatchEvent("INK_COLOR", this.value);
|
||||
});
|
||||
@ -68,6 +76,12 @@ class AnnotationEditorParams {
|
||||
case AnnotationEditorParamsType.FREETEXT_COLOR:
|
||||
editorFreeTextColor.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
|
||||
editorHighlightColor.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
|
||||
editorHighlightOpacity.value = value;
|
||||
break;
|
||||
case AnnotationEditorParamsType.INK_COLOR:
|
||||
editorInkColor.value = value;
|
||||
break;
|
||||
|
@ -486,6 +486,11 @@ const PDFViewerApplication = {
|
||||
appConfig.toolbar?.editorStampButton?.classList.add("hidden");
|
||||
}
|
||||
|
||||
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
|
||||
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
|
||||
editorHighlightButton.hidden = false;
|
||||
}
|
||||
|
||||
this.annotationEditorParams = new AnnotationEditorParams(
|
||||
appConfig.annotationEditorParams,
|
||||
eventBus
|
||||
|
@ -125,6 +125,14 @@ const defaultOptions = {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableHighlightEditor: {
|
||||
// We'll probably want to make some experiments before enabling this
|
||||
// in Firefox release, but it has to be temporary.
|
||||
// TODO: remove it when unnecessary.
|
||||
/** @type {boolean} */
|
||||
value: typeof PDFJSDev === "undefined",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enablePermissions: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
|
@ -13,13 +13,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @typedef {import("../src/display/draw_layer.js").DrawLayer} DrawLayer */
|
||||
|
||||
import { DrawLayer } from "pdfjs-lib";
|
||||
|
||||
/**
|
||||
* @typedef {Object} DrawLayerBuilderOptions
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
* @property {number} pageIndex
|
||||
*/
|
||||
|
||||
class DrawLayerBuilder {
|
||||
@ -53,6 +51,14 @@ class DrawLayerBuilder {
|
||||
this.#drawLayer.destroy();
|
||||
this.#drawLayer = null;
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
this.#drawLayer?.setParent(parent);
|
||||
}
|
||||
|
||||
getDrawLayer() {
|
||||
return this.#drawLayer;
|
||||
}
|
||||
}
|
||||
|
||||
export { DrawLayerBuilder };
|
||||
|
6
web/images/toolbarButton-editorHighlight.svg
Normal file
6
web/images/toolbarButton-editorHighlight.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.10918 11.66C7.24918 11.8 7.43918 11.88 7.63918 11.88C7.83918 11.88 8.02918 11.8 8.16918 11.66L14.9192 4.91C15.2692 4.57 15.4592 4.11 15.4592 3.62C15.4592 3.13 15.2692 2.67 14.9192 2.33L13.1292 0.54C12.7892 0.19 12.3292 0 11.8392 0C11.3492 0 10.8892 0.2 10.5492 0.54L3.79918 7.29C3.50918 7.58 3.50918 8.06 3.79918 8.35L4.38988 8.9407L1.40918 11.93H5.64918L6.51419 11.065L7.10918 11.66ZM7.63918 10.07L5.38918 7.82V7.81L7.8648 5.33438L10.1198 7.58938L7.63918 10.07ZM11.1805 6.52872L13.8592 3.85C13.9892 3.72 13.9892 3.52 13.8592 3.39L12.0692 1.6C11.9892 1.52 11.8892 1.5 11.8392 1.5C11.8392 1.5 11.6892 1.51 11.6092 1.59L8.92546 4.27372L11.1805 6.52872Z" fill="#000"/>
|
||||
<path d="M0.40918 14H15.4092V16H0.40918V14Z" fill="#000"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 910 B |
@ -42,6 +42,7 @@ import {
|
||||
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||
import { compatibilityParams } from "./app_options.js";
|
||||
import { DrawLayerBuilder } from "./draw_layer_builder.js";
|
||||
import { NullL10n } from "web-l10n_utils";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
||||
@ -177,6 +178,7 @@ class PDFPageView {
|
||||
this.zoomLayer = null;
|
||||
this.xfaLayer = null;
|
||||
this.structTreeLayer = null;
|
||||
this.drawLayer = null;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "page";
|
||||
@ -354,6 +356,14 @@ class PDFPageView {
|
||||
}
|
||||
}
|
||||
|
||||
async #renderDrawLayer() {
|
||||
try {
|
||||
await this.drawLayer.render("display");
|
||||
} catch (ex) {
|
||||
console.error(`#renderDrawLayer: "${ex}".`);
|
||||
}
|
||||
}
|
||||
|
||||
async #renderXfaLayer() {
|
||||
let error = null;
|
||||
try {
|
||||
@ -718,6 +728,10 @@ class PDFPageView {
|
||||
this.annotationEditorLayer &&
|
||||
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
|
||||
) {
|
||||
if (this.drawLayer) {
|
||||
this.drawLayer.cancel();
|
||||
this.drawLayer = null;
|
||||
}
|
||||
this.annotationEditorLayer.cancel();
|
||||
this.annotationEditorLayer = null;
|
||||
}
|
||||
@ -770,6 +784,9 @@ class PDFPageView {
|
||||
this.#renderAnnotationLayer();
|
||||
}
|
||||
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
|
||||
if (this.drawLayer) {
|
||||
this.#renderDrawLayer();
|
||||
}
|
||||
this.#renderAnnotationEditorLayer();
|
||||
}
|
||||
if (redrawXfaLayer && this.xfaLayer) {
|
||||
@ -1001,12 +1018,19 @@ class PDFPageView {
|
||||
await this.#renderAnnotationLayer();
|
||||
}
|
||||
|
||||
if (!this.annotationEditorLayer) {
|
||||
const { annotationEditorUIManager } = this.#layerProperties;
|
||||
|
||||
if (!annotationEditorUIManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawLayer ||= new DrawLayerBuilder({
|
||||
pageIndex: this.id,
|
||||
});
|
||||
await this.#renderDrawLayer();
|
||||
this.drawLayer.setParent(canvasWrapper);
|
||||
|
||||
if (!this.annotationEditorLayer) {
|
||||
this.annotationEditorLayer = new AnnotationEditorLayerBuilder({
|
||||
uiManager: annotationEditorUIManager,
|
||||
pageDiv: div,
|
||||
@ -1014,6 +1038,8 @@ class PDFPageView {
|
||||
l10n,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
drawLayer: this.drawLayer.getDrawLayer(),
|
||||
});
|
||||
}
|
||||
this.#renderAnnotationEditorLayer();
|
||||
|
@ -69,6 +69,18 @@ class Toolbar {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorHighlightButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
eventDetails: {
|
||||
get mode() {
|
||||
const { classList } = options.editorHighlightButton;
|
||||
return classList.contains("toggled")
|
||||
? AnnotationEditorType.NONE
|
||||
: AnnotationEditorType.HIGHLIGHT;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
element: options.editorInkButton,
|
||||
eventName: "switchannotationeditormode",
|
||||
@ -202,6 +214,8 @@ class Toolbar {
|
||||
#bindEditorToolsListener({
|
||||
editorFreeTextButton,
|
||||
editorFreeTextParamsToolbar,
|
||||
editorHighlightButton,
|
||||
editorHighlightParamsToolbar,
|
||||
editorInkButton,
|
||||
editorInkParamsToolbar,
|
||||
editorStampButton,
|
||||
@ -213,6 +227,11 @@ class Toolbar {
|
||||
mode === AnnotationEditorType.FREETEXT,
|
||||
editorFreeTextParamsToolbar
|
||||
);
|
||||
toggleCheckedBtn(
|
||||
editorHighlightButton,
|
||||
mode === AnnotationEditorType.HIGHLIGHT,
|
||||
editorHighlightParamsToolbar
|
||||
);
|
||||
toggleCheckedBtn(
|
||||
editorInkButton,
|
||||
mode === AnnotationEditorType.INK,
|
||||
@ -226,6 +245,7 @@ class Toolbar {
|
||||
|
||||
const isDisable = mode === AnnotationEditorType.DISABLE;
|
||||
editorFreeTextButton.disabled = isDisable;
|
||||
editorHighlightButton.disabled = isDisable;
|
||||
editorInkButton.disabled = isDisable;
|
||||
editorStampButton.disabled = isDisable;
|
||||
};
|
||||
|
@ -81,6 +81,7 @@
|
||||
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
|
||||
--treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
|
||||
--toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg);
|
||||
--toolbarButton-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg);
|
||||
--toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg);
|
||||
--toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg);
|
||||
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
|
||||
@ -584,6 +585,10 @@ body {
|
||||
inset-inline-end: calc(var(--editor-toolbar-base-offset) + 56px);
|
||||
}
|
||||
|
||||
#editorHighlightParamsToolbar {
|
||||
inset-inline-end: calc(var(--editor-toolbar-base-offset) + 84px);
|
||||
}
|
||||
|
||||
#editorStampAddImage::before {
|
||||
mask-image: var(--editorParams-stampAddImage-icon);
|
||||
}
|
||||
@ -903,6 +908,10 @@ body {
|
||||
mask-image: var(--toolbarButton-editorFreeText-icon);
|
||||
}
|
||||
|
||||
#editorHighlight::before {
|
||||
mask-image: var(--toolbarButton-editorHighlight-icon);
|
||||
}
|
||||
|
||||
#editorInk::before {
|
||||
mask-image: var(--toolbarButton-editorInk-icon);
|
||||
}
|
||||
|
@ -171,15 +171,28 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
</div> <!-- findbar -->
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorHighlightColor" class="editorParamsLabel" data-l10n-id="editor_highlight_color">Color</label>
|
||||
<input type="color" value="#FFFF00" id="editorHighlightColor" class="editorParamsColor" tabindex="100">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorHighlightOpacity" class="editorParamsLabel" data-l10n-id="editor_highlight_opacity">Opacity</label>
|
||||
<input type="range" id="editorHighlightOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="101">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
|
||||
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="100">
|
||||
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="102">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorFreeTextFontSize" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
|
||||
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100" step="1" tabindex="101">
|
||||
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100" step="1" tabindex="103">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -188,22 +201,22 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
|
||||
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="102">
|
||||
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="104">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkThickness" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
|
||||
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="103">
|
||||
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="105">
|
||||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
|
||||
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="104">
|
||||
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="106">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
|
||||
<div class="editorParamsToolbarContainer">
|
||||
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="105" data-l10n-id="pdfjs-editor-stamp-add-image-button">
|
||||
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="107" data-l10n-id="pdfjs-editor-stamp-add-image-button">
|
||||
<span data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -334,13 +347,15 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
<div id="toolbarViewerRight">
|
||||
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
|
||||
<button id="editorFreeText" class="toolbarButton" disabled="disabled" title="Text" role="radio" aria-checked="false" aria-controls="editorFreeTextParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-free-text-button">
|
||||
<button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight" role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-highlight">
|
||||
<span data-l10n-id="pdfjs-editor-highlight-label">Highlight</span>
|
||||
<button id="editorFreeText" class="toolbarButton" disabled="disabled" title="Text" role="radio" aria-checked="false" aria-controls="editorFreeTextParamsToolbar" tabindex="32" data-l10n-id="pdfjs-editor-free-text-button">
|
||||
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
|
||||
</button>
|
||||
<button id="editorInk" class="toolbarButton" disabled="disabled" title="Draw" role="radio" aria-checked="false" aria-controls="editorInkParamsToolbar" tabindex="32" data-l10n-id="pdfjs-editor-ink-button">
|
||||
<button id="editorInk" class="toolbarButton" disabled="disabled" title="Draw" role="radio" aria-checked="false" aria-controls="editorInkParamsToolbar" tabindex="33" data-l10n-id="pdfjs-editor-ink-button">
|
||||
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
|
||||
</button>
|
||||
<button id="editorStamp" class="toolbarButton" disabled="disabled" title="Add or edit images" role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="33" data-l10n-id="pdfjs-editor-stamp-button">
|
||||
<button id="editorStamp" class="toolbarButton" disabled="disabled" title="Add or edit images" role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="34" data-l10n-id="pdfjs-editor-stamp-button">
|
||||
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -57,6 +57,10 @@ function getViewerConfiguration() {
|
||||
editorFreeTextParamsToolbar: document.getElementById(
|
||||
"editorFreeTextParamsToolbar"
|
||||
),
|
||||
editorHighlightButton: document.getElementById("editorHighlight"),
|
||||
editorHighlightParamsToolbar: document.getElementById(
|
||||
"editorHighlightParamsToolbar"
|
||||
),
|
||||
editorInkButton: document.getElementById("editorInk"),
|
||||
editorInkParamsToolbar: document.getElementById("editorInkParamsToolbar"),
|
||||
editorStampButton: document.getElementById("editorStamp"),
|
||||
@ -164,6 +168,8 @@ function getViewerConfiguration() {
|
||||
annotationEditorParams: {
|
||||
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
|
||||
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
|
||||
editorHighlightColor: document.getElementById("editorHighlightColor"),
|
||||
editorHighlightOpacity: document.getElementById("editorHighlightOpacity"),
|
||||
editorInkColor: document.getElementById("editorInkColor"),
|
||||
editorInkThickness: document.getElementById("editorInkThickness"),
|
||||
editorInkOpacity: document.getElementById("editorInkOpacity"),
|
||||
|
Loading…
Reference in New Issue
Block a user