Skip to content

Commit

Permalink
[Editor] Add a floating button close to the selected text to highligh…
Browse files Browse the repository at this point in the history
…t it (bug 1867742)

For now keep this feature behind a pref in order to make some experiments before
deciding to enable it.
  • Loading branch information
calixteman committed Mar 12, 2024
1 parent eb16072 commit aa0b861
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 19 deletions.
4 changes: 4 additions & 0 deletions extensions/chromium/preferences_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"type": "boolean",
"default": false
},
"enableHighlightFloatingButton": {
"type": "boolean",
"default": false
},
"highlightEditorColors": {
"type": "string",
"default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F"
Expand Down
2 changes: 2 additions & 0 deletions l10n/en-US/viewer.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ pdfjs-editor-stamp-button-label = Add or edit images
pdfjs-editor-highlight-button =
.title = Highlight
pdfjs-editor-highlight-button-label = Highlight
pdfjs-highlight-floating-button =
.title = Highlight
## Remove button for the various kind of editor.

Expand Down
78 changes: 77 additions & 1 deletion src/display/editor/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,80 @@ class EditorToolbar {
}
}

export { EditorToolbar };
class HighlightToolbar {
#buttons = null;

#toolbar = null;

#uiManager;

constructor(uiManager) {
this.#uiManager = uiManager;
}

#render() {
const editToolbar = (this.#toolbar = document.createElement("div"));
editToolbar.className = "editToolbar";
editToolbar.setAttribute("role", "toolbar");
editToolbar.addEventListener("contextmenu", noContextMenu);

const buttons = (this.#buttons = document.createElement("div"));
buttons.className = "buttons";
editToolbar.append(buttons);

this.#addHighlightButton();

return editToolbar;
}

#getLastPoint(boxes, isLTR) {
let lastY = 0;
let lastX = 0;
for (const box of boxes) {
const y = box.y + box.height;
if (y < lastY) {
continue;
}
const x = box.x + (isLTR ? box.width : 0);
if (y > lastY) {
lastX = x;
lastY = y;
continue;
}
if (isLTR) {
if (x > lastX) {
lastX = x;
}
} else if (x < lastX) {
lastX = x;
}
}
return [isLTR ? 1 - lastX : lastX, lastY];
}

show(parent, boxes, isLTR) {
const [x, y] = this.#getLastPoint(boxes, isLTR);
const { style } = (this.#toolbar ||= this.#render());
parent.append(this.#toolbar);
style.insetInlineEnd = `${100 * x}%`;
style.top = `calc(${100 * y}% + var(--editor-toolbar-vert-offset))`;
}

hide() {
this.#toolbar.remove();
}

#addHighlightButton() {
const button = document.createElement("button");
button.className = "highlightButton";
button.tabIndex = 0;
button.setAttribute("data-l10n-id", `pdfjs-highlight-floating-button`);
button.addEventListener("contextmenu", noContextMenu);
button.addEventListener("click", () => {
this.#uiManager.highlightSelection("floating_button");
});
this.#buttons.append(button);
}
}

export { EditorToolbar, HighlightToolbar };
81 changes: 64 additions & 17 deletions src/display/editor/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getRGB,
PixelsPerInch,
} from "../display_utils.js";
import { HighlightToolbar } from "./toolbar.js";

function bindEvents(obj, element, names) {
for (const name of names) {
Expand Down Expand Up @@ -555,6 +556,8 @@ class AnnotationEditorUIManager {

#editorsToRescale = new Set();

#enableHighlightFloatingButton = false;

#filterFactory = null;

#focusMainContainerTimeoutId = null;
Expand All @@ -563,6 +566,8 @@ class AnnotationEditorUIManager {

#highlightWhenShiftUp = false;

#highlightToolbar = null;

#idManager = new IdManager();

#isEnabled = false;
Expand Down Expand Up @@ -771,6 +776,7 @@ class AnnotationEditorUIManager {
pdfDocument,
pageColors,
highlightColors,
enableHighlightFloatingButton,
mlManager
) {
this.#container = container;
Expand All @@ -782,10 +788,12 @@ class AnnotationEditorUIManager {
this._eventBus._on("scalechanging", this.#boundOnScaleChanging);
this._eventBus._on("rotationchanging", this.#boundOnRotationChanging);
this.#addSelectionListener();
this.#addKeyboardManager();
this.#annotationStorage = pdfDocument.annotationStorage;
this.#filterFactory = pdfDocument.filterFactory;
this.#pageColors = pageColors;
this.#highlightColors = highlightColors || null;
this.#enableHighlightFloatingButton = enableHighlightFloatingButton;
this.#mlManager = mlManager || null;
this.viewParameters = {
realScale: PixelsPerInch.PDF_TO_CSS_UNITS,
Expand Down Expand Up @@ -821,6 +829,8 @@ class AnnotationEditorUIManager {
this.#selectedEditors.clear();
this.#commandManager.destroy();
this.#altTextManager?.destroy();
this.#highlightToolbar?.hide();
this.#highlightToolbar = null;
if (this.#focusMainContainerTimeoutId) {
clearTimeout(this.#focusMainContainerTimeoutId);
this.#focusMainContainerTimeoutId = null;
Expand Down Expand Up @@ -946,25 +956,32 @@ class AnnotationEditorUIManager {
this.viewParameters.rotation = pagesRotation;
}

#getAnchorElementForSelection({ anchorNode }) {
return anchorNode.nodeType === Node.TEXT_NODE
? anchorNode.parentElement
: anchorNode;
}

highlightSelection(methodOfCreation = "") {
const selection = document.getSelection();
if (!selection || selection.isCollapsed) {
return;
}
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
const text = selection.toString();
const anchorElement =
anchorNode.nodeType === Node.TEXT_NODE
? anchorNode.parentElement
: anchorNode;
const anchorElement = this.#getAnchorElementForSelection(selection);
const textLayer = anchorElement.closest(".textLayer");
const boxes = this.getSelectionBoxes(textLayer);
if (!boxes) {
return;
}
selection.empty();
if (this.#mode === AnnotationEditorType.NONE) {
this._eventBus.dispatch("showannotationeditorui", {
source: this,
mode: AnnotationEditorType.HIGHLIGHT,
});
this.showAllEditors("highlight", true, /* updateButton = */ true);
}
for (const layer of this.#allLayers.values()) {
if (layer.hasTextLayer(textLayer)) {
Expand All @@ -982,6 +999,21 @@ class AnnotationEditorUIManager {
}
}

#displayHighlightToolbar() {
const selection = document.getSelection();
if (!selection || selection.isCollapsed) {
return;
}
const anchorElement = this.#getAnchorElementForSelection(selection);
const textLayer = anchorElement.closest(".textLayer");
const boxes = this.getSelectionBoxes(textLayer);
if (!boxes) {
return;
}
this.#highlightToolbar ||= new HighlightToolbar(this);
this.#highlightToolbar.show(textLayer, boxes, this.direction === "ltr");
}

/**
* Add an editor in the annotation storage.
* @param {AnnotationEditor} editor
Expand All @@ -1000,6 +1032,7 @@ class AnnotationEditorUIManager {
const selection = document.getSelection();
if (!selection || selection.isCollapsed) {
if (this.#selectedTextNode) {
this.#highlightToolbar?.hide();
this.#selectedTextNode = null;
this.#dispatchUpdateStates({
hasSelectedText: false,
Expand All @@ -1012,29 +1045,34 @@ class AnnotationEditorUIManager {
return;
}

const anchorElement =
anchorNode.nodeType === Node.TEXT_NODE
? anchorNode.parentElement
: anchorNode;
if (!anchorElement.closest(".textLayer")) {
const anchorElement = this.#getAnchorElementForSelection(selection);
const textLayer = anchorElement.closest(".textLayer");
if (!textLayer) {
if (this.#selectedTextNode) {
this.#highlightToolbar?.hide();
this.#selectedTextNode = null;
this.#dispatchUpdateStates({
hasSelectedText: false,
});
}
return;
}
this.#highlightToolbar?.hide();
this.#selectedTextNode = anchorNode;
this.#dispatchUpdateStates({
hasSelectedText: true,
});

if (this.#mode !== AnnotationEditorType.HIGHLIGHT) {
if (
this.#mode !== AnnotationEditorType.HIGHLIGHT &&
this.#mode !== AnnotationEditorType.NONE
) {
return;
}

this.showAllEditors("highlight", true, /* updateButton = */ true);
if (this.#mode === AnnotationEditorType.HIGHLIGHT) {
this.showAllEditors("highlight", true, /* updateButton = */ true);
}

this.#highlightWhenShiftUp = this.isShiftKeyDown;
if (!this.isShiftKeyDown) {
Expand All @@ -1046,14 +1084,22 @@ class AnnotationEditorUIManager {
window.removeEventListener("pointerup", pointerup);
window.removeEventListener("blur", pointerup);
if (e.type === "pointerup") {
this.highlightSelection("main_toolbar");
this.#onSelectEnd("main_toolbar");
}
};
window.addEventListener("pointerup", pointerup);
window.addEventListener("blur", pointerup);
}
}

#onSelectEnd(methodOfCreation = "") {
if (this.#mode === AnnotationEditorType.HIGHLIGHT) {
this.highlightSelection(methodOfCreation);
} else if (this.#enableHighlightFloatingButton) {
this.#displayHighlightToolbar();
}
}

#addSelectionListener() {
document.addEventListener("selectionchange", this.#boundSelectionChange);
}
Expand All @@ -1076,7 +1122,7 @@ class AnnotationEditorUIManager {
this.isShiftKeyDown = false;
if (this.#highlightWhenShiftUp) {
this.#highlightWhenShiftUp = false;
this.highlightSelection("main_toolbar");
this.#onSelectEnd("main_toolbar");
}
if (!this.hasSelection) {
return;
Expand Down Expand Up @@ -1252,7 +1298,10 @@ class AnnotationEditorUIManager {
if (!this.isShiftKeyDown && event.key === "Shift") {
this.isShiftKeyDown = true;
}
if (!this.isEditorHandlingKeyboard) {
if (
this.#mode !== AnnotationEditorType.NONE &&
!this.isEditorHandlingKeyboard
) {
AnnotationEditorUIManager._keyboardManager.exec(this, event);
}
}
Expand All @@ -1266,7 +1315,7 @@ class AnnotationEditorUIManager {
this.isShiftKeyDown = false;
if (this.#highlightWhenShiftUp) {
this.#highlightWhenShiftUp = false;
this.highlightSelection("main_toolbar");
this.#onSelectEnd("main_toolbar");
}
}
}
Expand Down Expand Up @@ -1335,7 +1384,6 @@ class AnnotationEditorUIManager {
setEditingState(isEditing) {
if (isEditing) {
this.#addFocusManager();
this.#addKeyboardManager();
this.#addCopyPasteListeners();
this.#dispatchUpdateStates({
isEditing: this.#mode !== AnnotationEditorType.NONE,
Expand All @@ -1346,7 +1394,6 @@ class AnnotationEditorUIManager {
});
} else {
this.#removeFocusManager();
this.#removeKeyboardManager();
this.#removeCopyPasteListeners();
this.#dispatchUpdateStates({
isEditing: false,
Expand Down
42 changes: 42 additions & 0 deletions test/integration/highlight_editor_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1510,4 +1510,46 @@ describe("Highlight Editor", () => {
);
});
});

describe("Highlight from floating highlight button", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
null,
null,
{ highlightEditorColors: "red=#AB0000" }
);
});

afterAll(async () => {
await closePages(pages);
});

it("must check that clicking on the highlight floating button triggers an highlight", 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, delay: 100 });

await page.waitForSelector(".textLayer .highlightButton");
await page.click(".textLayer .highlightButton");

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");
})
);
});
});
});
Loading

0 comments on commit aa0b861

Please sign in to comment.