diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 8bb812c7a..fdb738b1a 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -24,7 +24,6 @@ import { AnnotationEditorType, FeatureTest } from "../../shared/util.js"; import { AnnotationEditor } from "./editor.js"; -import { bindEvents } from "./tools.js"; import { FreeTextEditor } from "./freetext.js"; import { InkEditor } from "./ink.js"; import { setLayerDimensions } from "../display_utils.js"; @@ -345,7 +344,7 @@ class AnnotationEditorLayer { * being dragged and droped from a page to another. * @param {AnnotationEditor} editor */ - #changeParent(editor) { + changeParent(editor) { if (editor.parent === this) { return; } @@ -370,7 +369,7 @@ class AnnotationEditorLayer { * @param {AnnotationEditor} editor */ add(editor) { - this.#changeParent(editor); + this.changeParent(editor); this.#uiManager.addEditor(editor); this.attach(editor); @@ -579,36 +578,19 @@ class AnnotationEditorLayer { } /** - * Drag callback. - * @param {DragEvent} event + * + * @param {AnnotationEditor} editor + * @param {number} x + * @param {number} y + * @returns */ - drop(event) { - const id = event.dataTransfer.getData("text/plain"); - const editor = this.#uiManager.getEditor(id); - if (!editor) { - return; + findNewParent(editor, x, y) { + const layer = this.#uiManager.findParent(x, y); + if (layer === null || layer === this) { + return false; } - - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - - this.#changeParent(editor); - - const rect = this.div.getBoundingClientRect(); - const endX = event.clientX - rect.x; - const endY = event.clientY - rect.y; - - editor.translate(endX - editor.startX, endY - editor.startY); - this.moveEditorInDOM(editor); - editor.div.focus(); - } - - /** - * Dragover callback. - * @param {DragEvent} event - */ - dragover(event) { - event.preventDefault(); + layer.changeParent(editor); + return true; } /** @@ -650,7 +632,6 @@ class AnnotationEditorLayer { render({ viewport }) { this.viewport = viewport; setLayerDimensions(this.div, viewport); - bindEvents(this, this.div, ["dragover", "drop"]); for (const editor of this.#uiManager.getEditors(this.pageIndex)) { this.add(editor); } diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 131f71ee0..de82156c1 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -57,6 +57,8 @@ class AnnotationEditor { _uiManager = null; + #isDraggable = false; + #zIndex = AnnotationEditor._zIndex++; static _colorManager = new ColorManager(); @@ -148,6 +150,15 @@ class AnnotationEditor { return []; } + get _isDraggable() { + return this.#isDraggable; + } + + set _isDraggable(value) { + this.#isDraggable = value; + this.div?.classList.toggle("draggable", value); + } + /** * Add some commands into the CommandManager (undo/redo stuff). * @param {Object} params @@ -237,18 +248,6 @@ class AnnotationEditor { this._uiManager.addToAnnotationStorage(this); } - /** - * We use drag-and-drop in order to move an editor on a page. - * @param {DragEvent} event - */ - dragstart(event) { - const rect = this.parent.div.getBoundingClientRect(); - this.startX = event.clientX - rect.x; - this.startY = event.clientY - rect.y; - event.dataTransfer.setData("text/plain", this.id); - event.dataTransfer.effectAllowed = "move"; - } - /** * Set the editor position within its parent. * @param {number} x @@ -446,8 +445,8 @@ class AnnotationEditor { event.preventDefault(); this.#resizePosition = [event.clientX, event.clientY]; const boundResizerPointermove = this.#resizerPointermove.bind(this, name); - const savedDraggable = this.div.draggable; - this.div.draggable = false; + const savedDraggable = this._isDraggable; + this._isDraggable = false; const resizingClassName = `resizing${name .charAt(0) .toUpperCase()}${name.slice(1)}`; @@ -462,7 +461,7 @@ class AnnotationEditor { // Stop the undo accumulation in order to have an undo action for each // resize session. this._uiManager.stopUndoAccumulation(); - this.div.draggable = savedDraggable; + this._isDraggable = savedDraggable; this.parent.div.classList.remove(resizingClassName); window.removeEventListener("pointerup", pointerUpCallback); window.removeEventListener("blur", pointerUpCallback); @@ -721,7 +720,7 @@ class AnnotationEditor { const [tx, ty] = this.getInitialTranslation(); this.translate(tx, ty); - bindEvents(this, this.div, ["dragstart", "pointerdown"]); + bindEvents(this, this.div, ["pointerdown"]); return this.div; } @@ -749,6 +748,90 @@ class AnnotationEditor { } this.#hasBeenSelected = true; + + this.#setUpDragSession(event); + } + + #setUpDragSession(event) { + if (!this._isDraggable) { + return; + } + + // Avoid to have spurious text selection in the text layer when dragging. + this._uiManager.disableUserSelect(true); + + const savedParent = this.parent; + const savedX = this.x; + const savedY = this.y; + + const pointerMoveOptions = { passive: true, capture: true }; + const pointerMoveCallback = e => { + const [parentWidth, parentHeight] = this.parentDimensions; + const [tx, ty] = this.screenToPageTranslation(e.movementX, e.movementY); + this.x += tx / parentWidth; + this.y += ty / parentHeight; + if (this.x < 0 || this.x > 1 || this.y < 0 || this.y > 1) { + // The element will be outside of its parent so change the parent. + const { x, y } = this.div.getBoundingClientRect(); + if (this.parent.findNewParent(this, x, y)) { + this.x -= Math.floor(this.x); + this.y -= Math.floor(this.y); + } + } + + this.div.style.left = `${(100 * this.x).toFixed(2)}%`; + this.div.style.top = `${(100 * this.y).toFixed(2)}%`; + this.div.scrollIntoView({ block: "nearest" }); + }; + window.addEventListener( + "pointermove", + pointerMoveCallback, + pointerMoveOptions + ); + + const pointerUpCallback = () => { + this._uiManager.disableUserSelect(false); + window.removeEventListener("pointerup", pointerUpCallback); + window.removeEventListener("blur", pointerUpCallback); + window.removeEventListener( + "pointermove", + pointerMoveCallback, + pointerMoveOptions + ); + const newParent = this.parent; + const newX = this.x; + const newY = this.y; + if (newParent === savedParent && newX === savedX && newY === savedY) { + return; + } + + this.addCommands({ + cmd: () => { + newParent.changeParent(this); + this.x = newX; + this.y = newY; + this.fixAndSetPosition(); + newParent.moveEditorInDOM(this); + }, + undo: () => { + savedParent.changeParent(this); + this.x = savedX; + this.y = savedY; + this.fixAndSetPosition(); + savedParent.moveEditorInDOM(this); + }, + mustExec: false, + }); + + this.fixAndSetPosition(); + this.parent.moveEditorInDOM(this); + this.div.focus(); + }; + window.addEventListener("pointerup", pointerUpCallback); + // If the user is using alt+tab during the dragging session, the pointerup + // event could be not fired, but a blur event is fired so we can use it in + // order to interrupt the dragging session. + window.addEventListener("blur", pointerUpCallback); } /** diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 7658b3c58..5b4081256 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -304,7 +304,7 @@ class FreeTextEditor extends AnnotationEditor { super.enableEditMode(); this.overlayDiv.classList.remove("enabled"); this.editorDiv.contentEditable = true; - this.div.draggable = false; + this._isDraggable = false; this.div.removeAttribute("aria-activedescendant"); this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown); this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus); @@ -323,7 +323,7 @@ class FreeTextEditor extends AnnotationEditor { this.overlayDiv.classList.add("enabled"); this.editorDiv.contentEditable = false; this.div.setAttribute("aria-activedescendant", this.#editorDivId); - this.div.draggable = true; + this._isDraggable = true; this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown); this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus); this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur); @@ -614,10 +614,10 @@ class FreeTextEditor extends AnnotationEditor { } this.#setContent(); - this.div.draggable = true; + this._isDraggable = true; this.editorDiv.contentEditable = false; } else { - this.div.draggable = false; + this._isDraggable = false; this.editorDiv.contentEditable = true; } diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 8c69dcd46..a67676968 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -294,7 +294,7 @@ class InkEditor extends AnnotationEditor { } super.enableEditMode(); - this.div.draggable = false; + this._isDraggable = false; this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); } @@ -305,7 +305,7 @@ class InkEditor extends AnnotationEditor { } super.disableEditMode(); - this.div.draggable = !this.isEmpty(); + this._isDraggable = !this.isEmpty(); this.div.classList.remove("editing"); this.canvas.removeEventListener( @@ -316,7 +316,7 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { - this.div.draggable = !this.isEmpty(); + this._isDraggable = !this.isEmpty(); } /** @inheritdoc */ diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index b0d3d2dc8..5a7a8952d 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -177,7 +177,7 @@ class StampEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { - this.div.draggable = true; + this._isDraggable = true; this.parent.addUndoableEditor(this); this.div.focus(); } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 98c44bd56..147fe72b8 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -586,6 +586,8 @@ class AnnotationEditorUIManager { #container = null; + #viewer = null; + static TRANSLATE_SMALL = 1; // page units. static TRANSLATE_BIG = 10; // page units. @@ -686,8 +688,9 @@ class AnnotationEditorUIManager { ); } - constructor(container, eventBus, pdfDocument, pageColors) { + constructor(container, viewer, eventBus, pdfDocument, pageColors) { this.#container = container; + this.#viewer = viewer; this.#eventBus = eventBus; this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("pagechanging", this.#boundOnPageChanging); @@ -740,6 +743,30 @@ class AnnotationEditorUIManager { this.#container.focus(); } + findParent(x, y) { + for (const layer of this.#allLayers.values()) { + const { + x: layerX, + y: layerY, + width, + height, + } = layer.div.getBoundingClientRect(); + if ( + x >= layerX && + x <= layerX + width && + y >= layerY && + y <= layerY + height + ) { + return layer; + } + } + return null; + } + + disableUserSelect(value = false) { + this.#viewer.classList.toggle("noUserSelect", value); + } + addShouldRescale(editor) { this.#editorsToRescale.add(editor); } @@ -961,6 +988,7 @@ class AnnotationEditorUIManager { this.#dispatchUpdateStates({ isEditing: false, }); + this.disableUserSelect(false); } } diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index a30241919..604461b6c 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -15,6 +15,7 @@ const { closePages, + dragAndDropAnnotation, getEditors, getEditorSelector, getSelectedEditors, @@ -891,13 +892,6 @@ describe("FreeText Editor", () => { it("must move an annotation", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - if (browserName === "firefox") { - pending( - "Disabled in Firefox, because DnD isn't implemented yet (see bug 1838638)." - ); - } - - await page.setDragInterception(true); await page.click("#editorFreeText"); const editorIds = await getEditors(page, "freeText"); @@ -913,16 +907,12 @@ describe("FreeText Editor", () => { return { x, y, width, height }; }); - await page.mouse.dragAndDrop( - { - x: editorRect.x + editorRect.width / 2, - y: editorRect.y + editorRect.height / 2, - }, - { - x: editorRect.x + editorRect.width / 2 + 100, - y: editorRect.y + editorRect.height / 2 + 100, - }, - { delay: 100 } + await dragAndDropAnnotation( + page, + editorRect.x + editorRect.width / 2, + editorRect.y + editorRect.height / 2, + 100, + 100 ); serialized = await getSerialized(page); diff --git a/test/integration/test_utils.js b/test/integration/test_utils.js index 1ab98f67b..0af11b327 100644 --- a/test/integration/test_utils.js +++ b/test/integration/test_utils.js @@ -194,3 +194,11 @@ function serializeBitmapDimensions(page) { }); } exports.serializeBitmapDimensions = serializeBitmapDimensions; + +async function dragAndDropAnnotation(page, startX, startY, tX, tY) { + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.mouse.move(startX + tX, startY + tY); + await page.mouse.up(); +} +exports.dragAndDropAnnotation = dragAndDropAnnotation; diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index c51906526..9adaf955e 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -81,7 +81,7 @@ } .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor)[draggable="true"] { + :is(.freeTextEditor, .inkEditor, .stampEditor).draggable { cursor: move; } diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index 710ad7740..fd875a8ed 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -87,6 +87,10 @@ height: var(--viewer-container-height); } +.pdfViewer.noUserSelect { + user-select: none; +} + /*#if GENERIC*/ .pdfViewer.removePageBorders .page { margin: 0 auto 10px; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index ff6fb9f30..67b5e00d3 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -849,6 +849,7 @@ class PDFViewer { } else if (isValidAnnotationEditorMode(mode)) { this.#annotationEditorUIManager = new AnnotationEditorUIManager( this.container, + this.viewer, this.eventBus, pdfDocument, this.pageColors