Set aria-label to the highlighted text, this way the focus frame is exactly set on the highlight element and the higlighted text is read.
828 lines
22 KiB
JavaScript
828 lines
22 KiB
JavaScript
/* 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,
|
|
shadow,
|
|
Util,
|
|
} from "../../shared/util.js";
|
|
import { bindEvents, KeyboardManager } from "./tools.js";
|
|
import { FreeOutliner, Outliner } from "./outliner.js";
|
|
import { AnnotationEditor } from "./editor.js";
|
|
import { ColorPicker } from "./color_picker.js";
|
|
import { noContextMenu } from "../display_utils.js";
|
|
|
|
/**
|
|
* Basic draw editor in order to generate an Highlight annotation.
|
|
*/
|
|
class HighlightEditor extends AnnotationEditor {
|
|
#anchorNode = null;
|
|
|
|
#anchorOffset = 0;
|
|
|
|
#boxes;
|
|
|
|
#clipPathId = null;
|
|
|
|
#colorPicker = null;
|
|
|
|
#focusOutlines = null;
|
|
|
|
#focusNode = null;
|
|
|
|
#focusOffset = 0;
|
|
|
|
#highlightDiv = null;
|
|
|
|
#highlightOutlines = null;
|
|
|
|
#id = null;
|
|
|
|
#isFreeHighlight = false;
|
|
|
|
#boundKeydown = this.#keydown.bind(this);
|
|
|
|
#lastPoint = null;
|
|
|
|
#opacity;
|
|
|
|
#outlineId = null;
|
|
|
|
#text = "";
|
|
|
|
#thickness;
|
|
|
|
#methodOfCreation = "";
|
|
|
|
static _defaultColor = null;
|
|
|
|
static _defaultOpacity = 1;
|
|
|
|
static _defaultThickness = 12;
|
|
|
|
static _l10nPromise;
|
|
|
|
static _type = "highlight";
|
|
|
|
static _editorType = AnnotationEditorType.HIGHLIGHT;
|
|
|
|
static _freeHighlightId = -1;
|
|
|
|
static _freeHighlight = null;
|
|
|
|
static _freeHighlightClipId = "";
|
|
|
|
static get _keyboardManager() {
|
|
const proto = HighlightEditor.prototype;
|
|
return shadow(
|
|
this,
|
|
"_keyboardManager",
|
|
new KeyboardManager([
|
|
[["ArrowLeft", "mac+ArrowLeft"], proto._moveCaret, { args: [0] }],
|
|
[["ArrowRight", "mac+ArrowRight"], proto._moveCaret, { args: [1] }],
|
|
[["ArrowUp", "mac+ArrowUp"], proto._moveCaret, { args: [2] }],
|
|
[["ArrowDown", "mac+ArrowDown"], proto._moveCaret, { args: [3] }],
|
|
])
|
|
);
|
|
}
|
|
|
|
constructor(params) {
|
|
super({ ...params, name: "highlightEditor" });
|
|
this.color = params.color || HighlightEditor._defaultColor;
|
|
this.#thickness = params.thickness || HighlightEditor._defaultThickness;
|
|
this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
|
|
this.#boxes = params.boxes || null;
|
|
this.#methodOfCreation = params.methodOfCreation || "";
|
|
this.#text = params.text || "";
|
|
this._isDraggable = false;
|
|
|
|
if (params.highlightId > -1) {
|
|
this.#isFreeHighlight = true;
|
|
this.#createFreeOutlines(params);
|
|
this.#addToDrawLayer();
|
|
} else {
|
|
this.#anchorNode = params.anchorNode;
|
|
this.#anchorOffset = params.anchorOffset;
|
|
this.#focusNode = params.focusNode;
|
|
this.#focusOffset = params.focusOffset;
|
|
this.#createOutlines();
|
|
this.#addToDrawLayer();
|
|
this.rotate(this.rotation);
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
get telemetryInitialData() {
|
|
return {
|
|
action: "added",
|
|
type: this.#isFreeHighlight ? "free_highlight" : "highlight",
|
|
color: this._uiManager.highlightColorNames.get(this.color),
|
|
thickness: this.#thickness,
|
|
methodOfCreation: this.#methodOfCreation,
|
|
};
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
get telemetryFinalData() {
|
|
return {
|
|
type: "highlight",
|
|
color: this._uiManager.highlightColorNames.get(this.color),
|
|
};
|
|
}
|
|
|
|
static computeTelemetryFinalData(data) {
|
|
// We want to know how many colors have been used.
|
|
return { numberOfColors: data.get("color").size };
|
|
}
|
|
|
|
#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,
|
|
];
|
|
}
|
|
|
|
#createFreeOutlines({ highlightOutlines, highlightId, clipPathId }) {
|
|
this.#highlightOutlines = highlightOutlines;
|
|
const extraThickness = 1.5;
|
|
this.#focusOutlines = highlightOutlines.getNewOutline(
|
|
/* Slightly bigger than the highlight in order to have a little
|
|
space between the highlight and the outline. */
|
|
this.#thickness / 2 + extraThickness,
|
|
/* innerMargin = */ 0.0025
|
|
);
|
|
|
|
if (highlightId >= 0) {
|
|
this.#id = highlightId;
|
|
this.#clipPathId = clipPathId;
|
|
// We need to redraw the highlight because we change the coordinates to be
|
|
// in the box coordinate system.
|
|
this.parent.drawLayer.finalizeLine(highlightId, highlightOutlines);
|
|
this.#outlineId = this.parent.drawLayer.highlightOutline(
|
|
this.#focusOutlines
|
|
);
|
|
} else if (this.parent) {
|
|
const angle = this.parent.viewport.rotation;
|
|
this.parent.drawLayer.updateLine(this.#id, highlightOutlines);
|
|
this.parent.drawLayer.updateBox(
|
|
this.#id,
|
|
HighlightEditor.#rotateBbox(
|
|
this.#highlightOutlines.box,
|
|
(angle - this.rotation + 360) % 360
|
|
)
|
|
);
|
|
|
|
this.parent.drawLayer.updateLine(this.#outlineId, this.#focusOutlines);
|
|
this.parent.drawLayer.updateBox(
|
|
this.#outlineId,
|
|
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
|
|
);
|
|
}
|
|
const { x, y, width, height } = highlightOutlines.box;
|
|
switch (this.rotation) {
|
|
case 0:
|
|
this.x = x;
|
|
this.y = y;
|
|
this.width = width;
|
|
this.height = height;
|
|
break;
|
|
case 90: {
|
|
const [pageWidth, pageHeight] = this.parentDimensions;
|
|
this.x = y;
|
|
this.y = 1 - x;
|
|
this.width = (width * pageHeight) / pageWidth;
|
|
this.height = (height * pageWidth) / pageHeight;
|
|
break;
|
|
}
|
|
case 180:
|
|
this.x = 1 - x;
|
|
this.y = 1 - y;
|
|
this.width = width;
|
|
this.height = height;
|
|
break;
|
|
case 270: {
|
|
const [pageWidth, pageHeight] = this.parentDimensions;
|
|
this.x = 1 - y;
|
|
this.y = x;
|
|
this.width = (width * pageHeight) / pageWidth;
|
|
this.height = (height * pageWidth) / pageHeight;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const { lastPoint } = this.#focusOutlines.box;
|
|
this.#lastPoint = [(lastPoint[0] - x) / width, (lastPoint[1] - y) / height];
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
static initialize(l10n, uiManager) {
|
|
AnnotationEditor.initialize(l10n, uiManager);
|
|
HighlightEditor._defaultColor ||=
|
|
uiManager.highlightColors?.values().next().value || "#fff066";
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
static updateDefaultParams(type, value) {
|
|
switch (type) {
|
|
case AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR:
|
|
HighlightEditor._defaultColor = value;
|
|
break;
|
|
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
|
|
HighlightEditor._defaultThickness = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
translateInPage(x, y) {}
|
|
|
|
/** @inheritdoc */
|
|
get toolbarPosition() {
|
|
return this.#lastPoint;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
updateParams(type, value) {
|
|
switch (type) {
|
|
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
|
|
this.#updateColor(value);
|
|
break;
|
|
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
|
|
this.#updateThickness(value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static get defaultPropertiesToUpdate() {
|
|
return [
|
|
[
|
|
AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR,
|
|
HighlightEditor._defaultColor,
|
|
],
|
|
[
|
|
AnnotationEditorParamsType.HIGHLIGHT_THICKNESS,
|
|
HighlightEditor._defaultThickness,
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
get propertiesToUpdate() {
|
|
return [
|
|
[
|
|
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
|
|
this.color || HighlightEditor._defaultColor,
|
|
],
|
|
[
|
|
AnnotationEditorParamsType.HIGHLIGHT_THICKNESS,
|
|
this.#thickness || HighlightEditor._defaultThickness,
|
|
],
|
|
[AnnotationEditorParamsType.HIGHLIGHT_FREE, this.#isFreeHighlight],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Update the color and make this action undoable.
|
|
* @param {string} color
|
|
*/
|
|
#updateColor(color) {
|
|
const setColor = col => {
|
|
this.color = col;
|
|
this.parent?.drawLayer.changeColor(this.#id, col);
|
|
this.#colorPicker?.updateColor(col);
|
|
};
|
|
const savedColor = this.color;
|
|
this.addCommands({
|
|
cmd: setColor.bind(this, color),
|
|
undo: setColor.bind(this, savedColor),
|
|
post: this._uiManager.updateUI.bind(this._uiManager, this),
|
|
mustExec: true,
|
|
type: AnnotationEditorParamsType.HIGHLIGHT_COLOR,
|
|
overwriteIfSameType: true,
|
|
keepUndo: true,
|
|
});
|
|
|
|
this._reportTelemetry(
|
|
{
|
|
action: "color_changed",
|
|
color: this._uiManager.highlightColorNames.get(color),
|
|
},
|
|
/* mustWait = */ true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update the thickness and make this action undoable.
|
|
* @param {number} thickness
|
|
*/
|
|
#updateThickness(thickness) {
|
|
const savedThickness = this.#thickness;
|
|
const setThickness = th => {
|
|
this.#thickness = th;
|
|
this.#changeThickness(th);
|
|
};
|
|
this.addCommands({
|
|
cmd: setThickness.bind(this, thickness),
|
|
undo: setThickness.bind(this, savedThickness),
|
|
post: this._uiManager.updateUI.bind(this._uiManager, this),
|
|
mustExec: true,
|
|
type: AnnotationEditorParamsType.INK_THICKNESS,
|
|
overwriteIfSameType: true,
|
|
keepUndo: true,
|
|
});
|
|
this._reportTelemetry(
|
|
{ action: "thickness_changed", thickness },
|
|
/* mustWait = */ true
|
|
);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
async addEditToolbar() {
|
|
const toolbar = await super.addEditToolbar();
|
|
if (!toolbar) {
|
|
return null;
|
|
}
|
|
if (this._uiManager.highlightColors) {
|
|
this.#colorPicker = new ColorPicker({ editor: this });
|
|
toolbar.addColorPicker(this.#colorPicker);
|
|
}
|
|
return toolbar;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
disableEditing() {
|
|
super.disableEditing();
|
|
this.div.classList.toggle("disabled", true);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
enableEditing() {
|
|
super.enableEditing();
|
|
this.div.classList.toggle("disabled", false);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
fixAndSetPosition() {
|
|
return super.fixAndSetPosition(this.#getRotation());
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getBaseTranslation() {
|
|
// The editor itself doesn't have any CSS border (we're drawing one
|
|
// ourselves in using SVG).
|
|
return [0, 0];
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
getRect(tx, ty) {
|
|
return super.getRect(tx, ty, this.#getRotation());
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
onceAdded() {
|
|
this.parent.addUndoableEditor(this);
|
|
this.div.focus();
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
remove() {
|
|
this.#cleanDrawLayer();
|
|
this._reportTelemetry({
|
|
action: "deleted",
|
|
});
|
|
super.remove();
|
|
}
|
|
|
|
/** @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 rebuilding it,
|
|
// hence we must add it to its parent.
|
|
this.parent.add(this);
|
|
}
|
|
}
|
|
|
|
setParent(parent) {
|
|
let mustBeSelected = false;
|
|
if (this.parent && !parent) {
|
|
this.#cleanDrawLayer();
|
|
} else if (parent) {
|
|
this.#addToDrawLayer(parent);
|
|
// If mustBeSelected is true it means that this editor was selected
|
|
// when its parent has been destroyed, hence we must select it again.
|
|
mustBeSelected =
|
|
!this.parent && this.div?.classList.contains("selectedEditor");
|
|
}
|
|
super.setParent(parent);
|
|
this.show(this._isVisible);
|
|
if (mustBeSelected) {
|
|
// We select it after the parent has been set.
|
|
this.select();
|
|
}
|
|
}
|
|
|
|
#changeThickness(thickness) {
|
|
if (!this.#isFreeHighlight) {
|
|
return;
|
|
}
|
|
this.#createFreeOutlines({
|
|
highlightOutlines: this.#highlightOutlines.getNewOutline(thickness / 2),
|
|
});
|
|
this.fixAndSetPosition();
|
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
|
this.setDims(this.width * parentWidth, this.height * parentHeight);
|
|
}
|
|
|
|
#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
|
|
));
|
|
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
|
|
if (this.#highlightDiv) {
|
|
this.#highlightDiv.style.clipPath = this.#clipPathId;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// We need to rotate the svgs because of the coordinates system.
|
|
const { drawLayer } = this.parent;
|
|
let box;
|
|
if (this.#isFreeHighlight) {
|
|
angle = (angle - this.rotation + 360) % 360;
|
|
box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle);
|
|
} else {
|
|
// An highlight annotation is always drawn horizontally.
|
|
box = HighlightEditor.#rotateBbox(this, angle);
|
|
}
|
|
drawLayer.rotate(this.#id, angle);
|
|
drawLayer.rotate(this.#outlineId, angle);
|
|
drawLayer.updateBox(this.#id, box);
|
|
drawLayer.updateBox(
|
|
this.#outlineId,
|
|
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
|
|
);
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
render() {
|
|
if (this.div) {
|
|
return this.div;
|
|
}
|
|
|
|
const div = super.render();
|
|
if (this.#text) {
|
|
div.setAttribute("aria-label", this.#text);
|
|
div.setAttribute("role", "mark");
|
|
}
|
|
if (this.#isFreeHighlight) {
|
|
div.classList.add("free");
|
|
} else {
|
|
this.div.addEventListener("keydown", this.#boundKeydown);
|
|
}
|
|
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
|
|
div.append(highlightDiv);
|
|
highlightDiv.setAttribute("aria-hidden", "true");
|
|
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");
|
|
}
|
|
|
|
#keydown(event) {
|
|
HighlightEditor._keyboardManager.exec(this, event);
|
|
}
|
|
|
|
_moveCaret(direction) {
|
|
this.parent.unselect(this);
|
|
switch (direction) {
|
|
case 0 /* left */:
|
|
case 2 /* up */:
|
|
this.#setCaret(/* start = */ true);
|
|
break;
|
|
case 1 /* right */:
|
|
case 3 /* down */:
|
|
this.#setCaret(/* start = */ false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#setCaret(start) {
|
|
if (!this.#anchorNode) {
|
|
return;
|
|
}
|
|
const selection = window.getSelection();
|
|
if (start) {
|
|
selection.setPosition(this.#anchorNode, this.#anchorOffset);
|
|
} else {
|
|
selection.setPosition(this.#focusNode, this.#focusOffset);
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
select() {
|
|
super.select();
|
|
if (!this.#outlineId) {
|
|
return;
|
|
}
|
|
this.parent?.drawLayer.removeClass(this.#outlineId, "hovered");
|
|
this.parent?.drawLayer.addClass(this.#outlineId, "selected");
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
unselect() {
|
|
super.unselect();
|
|
if (!this.#outlineId) {
|
|
return;
|
|
}
|
|
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
|
|
if (!this.#isFreeHighlight) {
|
|
this.#setCaret(/* start = */ false);
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
get _mustFixPosition() {
|
|
return !this.#isFreeHighlight;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
show(visible = this._isVisible) {
|
|
super.show(visible);
|
|
if (this.parent) {
|
|
this.parent.drawLayer.show(this.#id, visible);
|
|
this.parent.drawLayer.show(this.#outlineId, visible);
|
|
}
|
|
}
|
|
|
|
#getRotation() {
|
|
// Highlight annotations are always drawn horizontally but if
|
|
// a free highlight annotation can be rotated.
|
|
return this.#isFreeHighlight ? this.rotation : 0;
|
|
}
|
|
|
|
#serializeBoxes() {
|
|
if (this.#isFreeHighlight) {
|
|
return null;
|
|
}
|
|
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(rect) {
|
|
return this.#highlightOutlines.serialize(rect, this.#getRotation());
|
|
}
|
|
|
|
static startHighlighting(parent, isLTR, { target: textLayer, x, y }) {
|
|
const {
|
|
x: layerX,
|
|
y: layerY,
|
|
width: parentWidth,
|
|
height: parentHeight,
|
|
} = textLayer.getBoundingClientRect();
|
|
const pointerMove = e => {
|
|
this.#highlightMove(parent, e);
|
|
};
|
|
const pointerDownOptions = { capture: true, passive: false };
|
|
const pointerDown = e => {
|
|
// Avoid to have undesired clicks during the drawing.
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
const pointerUpCallback = e => {
|
|
textLayer.removeEventListener("pointermove", pointerMove);
|
|
window.removeEventListener("blur", pointerUpCallback);
|
|
window.removeEventListener("pointerup", pointerUpCallback);
|
|
window.removeEventListener(
|
|
"pointerdown",
|
|
pointerDown,
|
|
pointerDownOptions
|
|
);
|
|
window.removeEventListener("contextmenu", noContextMenu);
|
|
this.#endHighlight(parent, e);
|
|
};
|
|
window.addEventListener("blur", pointerUpCallback);
|
|
window.addEventListener("pointerup", pointerUpCallback);
|
|
window.addEventListener("pointerdown", pointerDown, pointerDownOptions);
|
|
window.addEventListener("contextmenu", noContextMenu);
|
|
|
|
textLayer.addEventListener("pointermove", pointerMove);
|
|
this._freeHighlight = new FreeOutliner(
|
|
{ x, y },
|
|
[layerX, layerY, parentWidth, parentHeight],
|
|
parent.scale,
|
|
this._defaultThickness / 2,
|
|
isLTR,
|
|
/* innerMargin = */ 0.001
|
|
);
|
|
({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
|
|
parent.drawLayer.highlight(
|
|
this._freeHighlight,
|
|
this._defaultColor,
|
|
this._defaultOpacity,
|
|
/* isPathUpdatable = */ true
|
|
));
|
|
}
|
|
|
|
static #highlightMove(parent, event) {
|
|
if (this._freeHighlight.add(event)) {
|
|
// Redraw only if the point has been added.
|
|
parent.drawLayer.updatePath(this._freeHighlightId, this._freeHighlight);
|
|
}
|
|
}
|
|
|
|
static #endHighlight(parent, event) {
|
|
if (!this._freeHighlight.isEmpty()) {
|
|
parent.createAndAddNewEditor(event, false, {
|
|
highlightId: this._freeHighlightId,
|
|
highlightOutlines: this._freeHighlight.getOutlines(),
|
|
clipPathId: this._freeHighlightClipId,
|
|
methodOfCreation: "main_toolbar",
|
|
});
|
|
} else {
|
|
parent.drawLayer.removeFreeHighlight(this._freeHighlightId);
|
|
}
|
|
this._freeHighlightId = -1;
|
|
this._freeHighlight = null;
|
|
this._freeHighlightClipId = "";
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
static deserialize(data, parent, uiManager) {
|
|
const editor = super.deserialize(data, parent, uiManager);
|
|
|
|
const {
|
|
rect: [blX, blY, trX, trY],
|
|
color,
|
|
quadPoints,
|
|
} = data;
|
|
editor.color = Util.makeHexColor(...color);
|
|
editor.#opacity = data.opacity;
|
|
|
|
const [pageWidth, pageHeight] = editor.pageDimensions;
|
|
editor.width = (trX - blX) / pageWidth;
|
|
editor.height = (trY - blY) / pageHeight;
|
|
const boxes = (editor.#boxes = []);
|
|
for (let i = 0; i < quadPoints.length; i += 8) {
|
|
boxes.push({
|
|
x: (quadPoints[4] - trX) / pageWidth,
|
|
y: (trY - (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,
|
|
thickness: this.#thickness,
|
|
quadPoints: this.#serializeBoxes(),
|
|
outlines: this.#serializeOutlines(rect),
|
|
pageIndex: this.pageIndex,
|
|
rect,
|
|
rotation: this.#getRotation(),
|
|
structTreeParentId: this._structTreeParentId,
|
|
};
|
|
}
|
|
|
|
static canCreateNewEmptyEditor() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export { HighlightEditor };
|