Merge pull request #16781 from calixteman/editor_rewrite_dragging

[Editor] Refactor dragging and dropping an editor (bug 1802895, bug 1844618)
This commit is contained in:
calixteman 2023-08-03 15:38:12 +02:00 committed by GitHub
commit 399475247f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 170 additions and 75 deletions

View File

@ -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);
}

View File

@ -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);
}
/**

View File

@ -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;
}

View File

@ -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 */

View File

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

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;

View File

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

View File

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

View File

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