mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 22:58:07 +02:00
[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
19 changed files with 897 additions and 56 deletions
|
@ -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,12 +145,24 @@ class AnnotationEditorLayer {
|
|||
*/
|
||||
updateMode(mode = this.#uiManager.getMode()) {
|
||||
this.#cleanup();
|
||||
if (mode === AnnotationEditorType.INK) {
|
||||
// We always want to an ink editor ready to draw in.
|
||||
this.addInkEditorIfNeeded(false);
|
||||
this.disableClick();
|
||||
} else {
|
||||
this.enableClick();
|
||||
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();
|
||||
break;
|
||||
case AnnotationEditorType.HIGHLIGHT:
|
||||
this.enableTextSelection();
|
||||
this.togglePointerEvents(false);
|
||||
this.disableClick();
|
||||
break;
|
||||
default:
|
||||
this.disableTextSelection();
|
||||
this.togglePointerEvents(true);
|
||||
this.enableClick();
|
||||
}
|
||||
|
||||
if (mode !== AnnotationEditorType.NONE) {
|
||||
|
@ -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;
|
||||
|
||||
this.#setUpDragSession(event);
|
||||
}
|
||||
|
||||
#setUpDragSession(event) {
|
||||
if (!this._isDraggable) {
|
||||
if (this._isDraggable) {
|
||||
this.#setUpDragSession(event);
|
||||
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,7 +1217,9 @@ class AnnotationEditorUIManager {
|
|||
}
|
||||
|
||||
addNewEditorFromKeyboard() {
|
||||
this.currentLayer.addNewEditor();
|
||||
if (this.currentLayer.canCreateNewEmptyEditor()) {
|
||||
this.currentLayer.addNewEditor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue