[Editor] Refactor dragging and dropping an editor (bugs 1802895, 1844618)

It'll help to have a full control on what's happening when moving an editor.
This commit is contained in:
Calixte Denizet 2023-08-02 20:08:09 +02:00
parent 0725b6299f
commit b59b1a81a9
11 changed files with 170 additions and 75 deletions

View File

@ -24,7 +24,6 @@
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js"; import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js"; import { AnnotationEditor } from "./editor.js";
import { bindEvents } from "./tools.js";
import { FreeTextEditor } from "./freetext.js"; import { FreeTextEditor } from "./freetext.js";
import { InkEditor } from "./ink.js"; import { InkEditor } from "./ink.js";
import { setLayerDimensions } from "../display_utils.js"; import { setLayerDimensions } from "../display_utils.js";
@ -345,7 +344,7 @@ class AnnotationEditorLayer {
* being dragged and droped from a page to another. * being dragged and droped from a page to another.
* @param {AnnotationEditor} editor * @param {AnnotationEditor} editor
*/ */
#changeParent(editor) { changeParent(editor) {
if (editor.parent === this) { if (editor.parent === this) {
return; return;
} }
@ -370,7 +369,7 @@ class AnnotationEditorLayer {
* @param {AnnotationEditor} editor * @param {AnnotationEditor} editor
*/ */
add(editor) { add(editor) {
this.#changeParent(editor); this.changeParent(editor);
this.#uiManager.addEditor(editor); this.#uiManager.addEditor(editor);
this.attach(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) { findNewParent(editor, x, y) {
const id = event.dataTransfer.getData("text/plain"); const layer = this.#uiManager.findParent(x, y);
const editor = this.#uiManager.getEditor(id); if (layer === null || layer === this) {
if (!editor) { return false;
return;
} }
layer.changeParent(editor);
event.preventDefault(); return true;
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();
} }
/** /**
@ -650,7 +632,6 @@ class AnnotationEditorLayer {
render({ viewport }) { render({ viewport }) {
this.viewport = viewport; this.viewport = viewport;
setLayerDimensions(this.div, viewport); setLayerDimensions(this.div, viewport);
bindEvents(this, this.div, ["dragover", "drop"]);
for (const editor of this.#uiManager.getEditors(this.pageIndex)) { for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
this.add(editor); this.add(editor);
} }

View File

@ -57,6 +57,8 @@ class AnnotationEditor {
_uiManager = null; _uiManager = null;
#isDraggable = false;
#zIndex = AnnotationEditor._zIndex++; #zIndex = AnnotationEditor._zIndex++;
static _colorManager = new ColorManager(); static _colorManager = new ColorManager();
@ -148,6 +150,15 @@ class AnnotationEditor {
return []; 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). * Add some commands into the CommandManager (undo/redo stuff).
* @param {Object} params * @param {Object} params
@ -237,18 +248,6 @@ class AnnotationEditor {
this._uiManager.addToAnnotationStorage(this); 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. * Set the editor position within its parent.
* @param {number} x * @param {number} x
@ -446,8 +445,8 @@ class AnnotationEditor {
event.preventDefault(); event.preventDefault();
this.#resizePosition = [event.clientX, event.clientY]; this.#resizePosition = [event.clientX, event.clientY];
const boundResizerPointermove = this.#resizerPointermove.bind(this, name); const boundResizerPointermove = this.#resizerPointermove.bind(this, name);
const savedDraggable = this.div.draggable; const savedDraggable = this._isDraggable;
this.div.draggable = false; this._isDraggable = false;
const resizingClassName = `resizing${name const resizingClassName = `resizing${name
.charAt(0) .charAt(0)
.toUpperCase()}${name.slice(1)}`; .toUpperCase()}${name.slice(1)}`;
@ -462,7 +461,7 @@ class AnnotationEditor {
// Stop the undo accumulation in order to have an undo action for each // Stop the undo accumulation in order to have an undo action for each
// resize session. // resize session.
this._uiManager.stopUndoAccumulation(); this._uiManager.stopUndoAccumulation();
this.div.draggable = savedDraggable; this._isDraggable = savedDraggable;
this.parent.div.classList.remove(resizingClassName); this.parent.div.classList.remove(resizingClassName);
window.removeEventListener( window.removeEventListener(
"pointermove", "pointermove",
@ -718,7 +717,7 @@ class AnnotationEditor {
const [tx, ty] = this.getInitialTranslation(); const [tx, ty] = this.getInitialTranslation();
this.translate(tx, ty); this.translate(tx, ty);
bindEvents(this, this.div, ["dragstart", "pointerdown"]); bindEvents(this, this.div, ["pointerdown"]);
return this.div; return this.div;
} }
@ -746,6 +745,90 @@ class AnnotationEditor {
} }
this.#hasBeenSelected = true; 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);
} }
/** /**

View File

@ -304,7 +304,7 @@ class FreeTextEditor extends AnnotationEditor {
super.enableEditMode(); super.enableEditMode();
this.overlayDiv.classList.remove("enabled"); this.overlayDiv.classList.remove("enabled");
this.editorDiv.contentEditable = true; this.editorDiv.contentEditable = true;
this.div.draggable = false; this._isDraggable = false;
this.div.removeAttribute("aria-activedescendant"); this.div.removeAttribute("aria-activedescendant");
this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown); this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus); this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
@ -323,7 +323,7 @@ class FreeTextEditor extends AnnotationEditor {
this.overlayDiv.classList.add("enabled"); this.overlayDiv.classList.add("enabled");
this.editorDiv.contentEditable = false; this.editorDiv.contentEditable = false;
this.div.setAttribute("aria-activedescendant", this.#editorDivId); this.div.setAttribute("aria-activedescendant", this.#editorDivId);
this.div.draggable = true; this._isDraggable = true;
this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown); this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus); this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur); this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
@ -614,10 +614,10 @@ class FreeTextEditor extends AnnotationEditor {
} }
this.#setContent(); this.#setContent();
this.div.draggable = true; this._isDraggable = true;
this.editorDiv.contentEditable = false; this.editorDiv.contentEditable = false;
} else { } else {
this.div.draggable = false; this._isDraggable = false;
this.editorDiv.contentEditable = true; this.editorDiv.contentEditable = true;
} }

View File

@ -294,7 +294,7 @@ class InkEditor extends AnnotationEditor {
} }
super.enableEditMode(); super.enableEditMode();
this.div.draggable = false; this._isDraggable = false;
this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
} }
@ -305,7 +305,7 @@ class InkEditor extends AnnotationEditor {
} }
super.disableEditMode(); super.disableEditMode();
this.div.draggable = !this.isEmpty(); this._isDraggable = !this.isEmpty();
this.div.classList.remove("editing"); this.div.classList.remove("editing");
this.canvas.removeEventListener( this.canvas.removeEventListener(
@ -316,7 +316,7 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */ /** @inheritdoc */
onceAdded() { onceAdded() {
this.div.draggable = !this.isEmpty(); this._isDraggable = !this.isEmpty();
} }
/** @inheritdoc */ /** @inheritdoc */

View File

@ -177,7 +177,7 @@ class StampEditor extends AnnotationEditor {
/** @inheritdoc */ /** @inheritdoc */
onceAdded() { onceAdded() {
this.div.draggable = true; this._isDraggable = true;
this.parent.addUndoableEditor(this); this.parent.addUndoableEditor(this);
this.div.focus(); this.div.focus();
} }

View File

@ -587,6 +587,8 @@ class AnnotationEditorUIManager {
#container = null; #container = null;
#viewer = null;
static TRANSLATE_SMALL = 1; // page units. static TRANSLATE_SMALL = 1; // page units.
static TRANSLATE_BIG = 10; // page units. static TRANSLATE_BIG = 10; // page units.
@ -687,8 +689,9 @@ class AnnotationEditorUIManager {
); );
} }
constructor(container, eventBus, pdfDocument, pageColors) { constructor(container, viewer, eventBus, pdfDocument, pageColors) {
this.#container = container; this.#container = container;
this.#viewer = viewer;
this.#eventBus = eventBus; this.#eventBus = eventBus;
this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("editingaction", this.#boundOnEditingAction);
this.#eventBus._on("pagechanging", this.#boundOnPageChanging); this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
@ -741,6 +744,30 @@ class AnnotationEditorUIManager {
this.#container.focus(); 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) { addShouldRescale(editor) {
this.#editorsToRescale.add(editor); this.#editorsToRescale.add(editor);
} }
@ -962,6 +989,7 @@ class AnnotationEditorUIManager {
this.#dispatchUpdateStates({ this.#dispatchUpdateStates({
isEditing: false, isEditing: false,
}); });
this.disableUserSelect(false);
} }
} }

View File

@ -15,6 +15,7 @@
const { const {
closePages, closePages,
dragAndDropAnnotation,
getEditors, getEditors,
getEditorSelector, getEditorSelector,
getSelectedEditors, getSelectedEditors,
@ -891,13 +892,6 @@ describe("FreeText Editor", () => {
it("must move an annotation", async () => { it("must move an annotation", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { 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"); await page.click("#editorFreeText");
const editorIds = await getEditors(page, "freeText"); const editorIds = await getEditors(page, "freeText");
@ -913,16 +907,12 @@ describe("FreeText Editor", () => {
return { x, y, width, height }; return { x, y, width, height };
}); });
await page.mouse.dragAndDrop( await dragAndDropAnnotation(
{ page,
x: editorRect.x + editorRect.width / 2, editorRect.x + editorRect.width / 2,
y: editorRect.y + editorRect.height / 2, editorRect.y + editorRect.height / 2,
}, 100,
{ 100
x: editorRect.x + editorRect.width / 2 + 100,
y: editorRect.y + editorRect.height / 2 + 100,
},
{ delay: 100 }
); );
serialized = await getSerialized(page); serialized = await getSerialized(page);

View File

@ -194,3 +194,11 @@ function serializeBitmapDimensions(page) {
}); });
} }
exports.serializeBitmapDimensions = serializeBitmapDimensions; 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;

View File

@ -81,7 +81,7 @@
} }
.annotationEditorLayer .annotationEditorLayer
:is(.freeTextEditor, .inkEditor, .stampEditor)[draggable="true"] { :is(.freeTextEditor, .inkEditor, .stampEditor).draggable {
cursor: move; cursor: move;
} }

View File

@ -87,6 +87,10 @@
height: var(--viewer-container-height); height: var(--viewer-container-height);
} }
.pdfViewer.noUserSelect {
user-select: none;
}
/*#if GENERIC*/ /*#if GENERIC*/
.pdfViewer.removePageBorders .page { .pdfViewer.removePageBorders .page {
margin: 0 auto 10px; margin: 0 auto 10px;

View File

@ -849,6 +849,7 @@ class PDFViewer {
} else if (isValidAnnotationEditorMode(mode)) { } else if (isValidAnnotationEditorMode(mode)) {
this.#annotationEditorUIManager = new AnnotationEditorUIManager( this.#annotationEditorUIManager = new AnnotationEditorUIManager(
this.container, this.container,
this.viewer,
this.eventBus, this.eventBus,
pdfDocument, pdfDocument,
this.pageColors this.pageColors