Merge pull request #17708 from calixteman/editor_highlight_context_menu
[Editor] Add the possibility to create an highlight from the context menu when some text is selected (bug 1867739)
This commit is contained in:
commit
101e8efad7
@ -184,6 +184,10 @@ class AnnotationEditorLayer {
|
||||
this.div.hidden = false;
|
||||
}
|
||||
|
||||
hasTextLayer(textLayer) {
|
||||
return textLayer === this.#textLayer?.div;
|
||||
}
|
||||
|
||||
addInkEditorIfNeeded(isCommitting) {
|
||||
if (this.#uiManager.getMode() !== AnnotationEditorType.INK) {
|
||||
// We don't want to add an ink editor if we're not in ink mode!
|
||||
@ -721,78 +725,13 @@ class AnnotationEditorLayer {
|
||||
* @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) {
|
||||
const boxes = this.#uiManager.getSelectionBoxes(this.#textLayer?.div);
|
||||
if (boxes) {
|
||||
this.createAndAddNewEditor(event, false, {
|
||||
boxes,
|
||||
});
|
||||
document.getSelection().empty();
|
||||
}
|
||||
selection.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -551,6 +551,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#focusMainContainerTimeoutId = null;
|
||||
|
||||
#hasSelection = false;
|
||||
|
||||
#highlightColors = null;
|
||||
|
||||
#idManager = new IdManager();
|
||||
@ -569,6 +571,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#selectedEditors = new Set();
|
||||
|
||||
#selectedTextNode = null;
|
||||
|
||||
#pageColors = null;
|
||||
|
||||
#boundBlur = this.blur.bind(this);
|
||||
@ -591,6 +595,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#boundOnScaleChanging = this.onScaleChanging.bind(this);
|
||||
|
||||
#boundSelectionChange = this.#selectionChange.bind(this);
|
||||
|
||||
#boundOnRotationChanging = this.onRotationChanging.bind(this);
|
||||
|
||||
#previousStates = {
|
||||
@ -599,6 +605,7 @@ class AnnotationEditorUIManager {
|
||||
hasSomethingToUndo: false,
|
||||
hasSomethingToRedo: false,
|
||||
hasSelectedEditor: false,
|
||||
hasSelectedText: false,
|
||||
};
|
||||
|
||||
#translation = [0, 0];
|
||||
@ -762,6 +769,7 @@ class AnnotationEditorUIManager {
|
||||
this._eventBus._on("pagechanging", this.#boundOnPageChanging);
|
||||
this._eventBus._on("scalechanging", this.#boundOnScaleChanging);
|
||||
this._eventBus._on("rotationchanging", this.#boundOnRotationChanging);
|
||||
this.#addSelectionListener();
|
||||
this.#annotationStorage = pdfDocument.annotationStorage;
|
||||
this.#filterFactory = pdfDocument.filterFactory;
|
||||
this.#pageColors = pageColors;
|
||||
@ -799,6 +807,7 @@ class AnnotationEditorUIManager {
|
||||
clearTimeout(this.#translationTimeoutId);
|
||||
this.#translationTimeoutId = null;
|
||||
}
|
||||
this.#removeSelectionListener();
|
||||
}
|
||||
|
||||
async mlGuess(data) {
|
||||
@ -905,6 +914,33 @@ class AnnotationEditorUIManager {
|
||||
this.viewParameters.rotation = pagesRotation;
|
||||
}
|
||||
|
||||
highlightSelection() {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
const { anchorNode } = selection;
|
||||
const anchorElement =
|
||||
anchorNode.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement
|
||||
: anchorNode;
|
||||
const textLayer = anchorElement.closest(".textLayer");
|
||||
const boxes = this.getSelectionBoxes(textLayer);
|
||||
selection.empty();
|
||||
if (this.#mode === AnnotationEditorType.NONE) {
|
||||
this._eventBus.dispatch("showannotationeditorui", {
|
||||
source: this,
|
||||
mode: AnnotationEditorType.HIGHLIGHT,
|
||||
});
|
||||
}
|
||||
for (const layer of this.#allLayers.values()) {
|
||||
if (layer.hasTextLayer(textLayer)) {
|
||||
layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { boxes });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an editor in the annotation storage.
|
||||
* @param {AnnotationEditor} editor
|
||||
@ -919,6 +955,52 @@ class AnnotationEditorUIManager {
|
||||
}
|
||||
}
|
||||
|
||||
#selectionChange() {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.isCollapsed) {
|
||||
if (this.#hasSelection) {
|
||||
this.#hasSelection = false;
|
||||
this.#selectedTextNode = null;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { anchorNode } = selection;
|
||||
if (anchorNode === this.#selectedTextNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorElement =
|
||||
anchorNode.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement
|
||||
: anchorNode;
|
||||
if (!anchorElement.closest(".textLayer")) {
|
||||
if (this.#hasSelection) {
|
||||
this.#hasSelection = false;
|
||||
this.#selectedTextNode = null;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.#hasSelection = true;
|
||||
this.#selectedTextNode = anchorNode;
|
||||
this.#dispatchUpdateStates({
|
||||
hasSelectedText: true,
|
||||
});
|
||||
}
|
||||
|
||||
#addSelectionListener() {
|
||||
document.addEventListener("selectionchange", this.#boundSelectionChange);
|
||||
}
|
||||
|
||||
#removeSelectionListener() {
|
||||
document.removeEventListener("selectionchange", this.#boundSelectionChange);
|
||||
}
|
||||
|
||||
#addFocusManager() {
|
||||
window.addEventListener("focus", this.#boundFocus);
|
||||
window.addEventListener("blur", this.#boundBlur);
|
||||
@ -1127,7 +1209,11 @@ class AnnotationEditorUIManager {
|
||||
* @param {Object} details
|
||||
*/
|
||||
onEditingAction(details) {
|
||||
if (["undo", "redo", "delete", "selectAll"].includes(details.name)) {
|
||||
if (
|
||||
["undo", "redo", "delete", "selectAll", "highlightSelection"].includes(
|
||||
details.name
|
||||
)
|
||||
) {
|
||||
this[details.name]();
|
||||
}
|
||||
}
|
||||
@ -1916,6 +2002,80 @@ class AnnotationEditorUIManager {
|
||||
get imageManager() {
|
||||
return shadow(this, "imageManager", new ImageManager());
|
||||
}
|
||||
|
||||
getSelectionBoxes(textLayer) {
|
||||
if (!textLayer) {
|
||||
return null;
|
||||
}
|
||||
const selection = document.getSelection();
|
||||
for (let i = 0, ii = selection.rangeCount; i < ii; i++) {
|
||||
if (
|
||||
!textLayer.contains(selection.getRangeAt(i).commonAncestorContainer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
x: layerX,
|
||||
y: layerY,
|
||||
width: parentWidth,
|
||||
height: parentHeight,
|
||||
} = textLayer.getBoundingClientRect();
|
||||
|
||||
// We must rotate the boxes because we want to have them in the non-rotated
|
||||
// page coordinates.
|
||||
let rotator;
|
||||
switch (textLayer.getAttribute("data-main-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 (let i = 0, ii = selection.rangeCount; i < ii; i++) {
|
||||
const range = selection.getRangeAt(i);
|
||||
if (range.collapsed) {
|
||||
continue;
|
||||
}
|
||||
for (const { x, y, width, height } of range.getClientRects()) {
|
||||
if (width === 0 || height === 0) {
|
||||
continue;
|
||||
}
|
||||
boxes.push(rotator(x, y, width, height));
|
||||
}
|
||||
}
|
||||
return boxes.length === 0 ? null : boxes;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -18,7 +18,7 @@ import { closePages, loadAndWait } from "./test_utils.mjs";
|
||||
const waitForSelectionChange = (page, selection) =>
|
||||
page.waitForFunction(
|
||||
// We need to replace EOL on Windows to make the test pass.
|
||||
sel => window.getSelection().toString().replaceAll("\r\n", "\n") === sel,
|
||||
sel => document.getSelection().toString().replaceAll("\r\n", "\n") === sel,
|
||||
{},
|
||||
selection
|
||||
);
|
||||
|
@ -2829,7 +2829,7 @@ describe("FreeText Editor", () => {
|
||||
count: 3,
|
||||
});
|
||||
const selection = await page.evaluate(() =>
|
||||
window.getSelection().toString()
|
||||
document.getSelection().toString()
|
||||
);
|
||||
|
||||
expect(selection).withContext(`In ${browserName}`).toEqual(data);
|
||||
|
@ -943,4 +943,91 @@ describe("Highlight Editor", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Send a message when some text is selected", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait(
|
||||
"tracemonkey.pdf",
|
||||
`.page[data-page-number = "1"] .endOfContent`,
|
||||
null,
|
||||
async page => {
|
||||
await page.waitForFunction(async () => {
|
||||
await window.PDFViewerApplication.initializedPromise;
|
||||
return true;
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
window.editingEvents = [];
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"annotationeditorstateschanged",
|
||||
({ details }) => {
|
||||
window.editingEvents.push(details);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
{ highlightEditorColors: "red=#AB0000" }
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that a message is sent on selection", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const rect = await getSpanRectFromText(page, 1, "Abstract");
|
||||
const x = rect.x + rect.width / 2;
|
||||
const y = rect.y + rect.height / 2;
|
||||
await page.mouse.click(x, y, { count: 2 });
|
||||
await page.waitForFunction(() => window.editingEvents.length > 0);
|
||||
|
||||
let editingEvent = await page.evaluate(() => {
|
||||
const e = window.editingEvents[0];
|
||||
window.editingEvents.length = 0;
|
||||
return e;
|
||||
});
|
||||
expect(editingEvent.isEditing)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toBe(false);
|
||||
expect(editingEvent.hasSelectedText)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toBe(true);
|
||||
|
||||
// Click somewhere to unselect the current selection.
|
||||
await page.mouse.click(rect.x + rect.width + 10, y, { count: 1 });
|
||||
await page.waitForFunction(() => window.editingEvents.length > 0);
|
||||
editingEvent = await page.evaluate(() => {
|
||||
const e = window.editingEvents[0];
|
||||
window.editingEvents.length = 0;
|
||||
return e;
|
||||
});
|
||||
expect(editingEvent.hasSelectedText)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toBe(false);
|
||||
|
||||
await page.mouse.click(x, y, { count: 2 });
|
||||
await page.waitForFunction(() => window.editingEvents.length > 0);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.PDFViewerApplication.eventBus.dispatch("editingaction", {
|
||||
name: "highlightSelection",
|
||||
});
|
||||
});
|
||||
|
||||
await page.waitForSelector(getEditorSelector(0));
|
||||
const usedColor = await page.evaluate(() => {
|
||||
const highlight = document.querySelector(
|
||||
`.page[data-page-number = "1"] .canvasWrapper > svg.highlight`
|
||||
);
|
||||
return highlight.getAttribute("fill");
|
||||
});
|
||||
|
||||
expect(usedColor).withContext(`In ${browserName}`).toEqual("#AB0000");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -188,7 +188,7 @@ class PDFPresentationMode {
|
||||
// Text selection is disabled in Presentation Mode, thus it's not possible
|
||||
// for the user to deselect text that is selected (e.g. with "Select all")
|
||||
// when entering Presentation Mode, hence we remove any active selection.
|
||||
window.getSelection().removeAllRanges();
|
||||
document.getSelection().empty();
|
||||
}
|
||||
|
||||
#exit() {
|
||||
|
@ -126,6 +126,14 @@ class Toolbar {
|
||||
);
|
||||
}
|
||||
|
||||
eventBus._on("showannotationeditorui", ({ mode }) => {
|
||||
switch (mode) {
|
||||
case AnnotationEditorType.HIGHLIGHT:
|
||||
options.editorHighlightButton.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user