Merge pull request #16811 from calixteman/editor_drag_selected

[Editor] Add the possibility to move all the selected editors with the mouse (bug 1847894)
This commit is contained in:
calixteman 2023-08-10 15:23:34 +02:00 committed by GitHub
commit ea259710fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 292 additions and 85 deletions

View File

@ -381,6 +381,10 @@ class AnnotationEditorLayer {
}
moveEditorInDOM(editor) {
if (!editor.isAttachedToDOM) {
return;
}
const { activeElement } = document;
if (editor.div.contains(activeElement)) {
// When the div is moved in the DOM the focus can move somewhere else,
@ -425,9 +429,7 @@ class AnnotationEditorLayer {
* @param {AnnotationEditor} editor
*/
addUndoableEditor(editor) {
const cmd = () => {
this.addOrRebuild(editor);
};
const cmd = () => editor._uiManager.rebuild(editor);
const undo = () => {
editor.remove();
};

View File

@ -42,7 +42,7 @@ class AnnotationEditor {
#boundFocusout = this.focusout.bind(this);
#hasBeenSelected = false;
#hasBeenClicked = false;
#isEditing = false;
@ -195,10 +195,10 @@ class AnnotationEditor {
if (!this._focusEventsAllowed) {
return;
}
if (!this.#hasBeenSelected) {
if (!this.#hasBeenClicked) {
this.parent.setSelected(this);
} else {
this.#hasBeenSelected = false;
this.#hasBeenClicked = false;
}
}
@ -293,7 +293,27 @@ class AnnotationEditor {
*/
translateInPage(x, y) {
this.#translate(this.pageDimensions, x, y);
this.parent.moveEditorInDOM(this);
this.moveInDOM();
this.div.scrollIntoView({ block: "nearest" });
}
drag(tx, ty) {
const [parentWidth, parentHeight] = this.parentDimensions;
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);
}
}
// The editor can be moved wherever the user wants, so we don't need to fix
// the position: it'll be done when the user will release the mouse button.
this.div.style.left = `${(100 * this.x).toFixed(2)}%`;
this.div.style.top = `${(100 * this.y).toFixed(2)}%`;
this.div.scrollIntoView({ block: "nearest" });
}
@ -516,7 +536,7 @@ class AnnotationEditor {
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
this.fixAndSetPosition();
this.parent.moveEditorInDOM(this);
this.moveInDOM();
},
undo: () => {
this.width = savedWidth;
@ -526,7 +546,7 @@ class AnnotationEditor {
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(parentWidth * savedWidth, parentHeight * savedHeight);
this.fixAndSetPosition();
this.parent.moveEditorInDOM(this);
this.moveInDOM();
},
mustExec: true,
});
@ -712,17 +732,7 @@ class AnnotationEditor {
return;
}
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
this.#hasBeenSelected = true;
this.#hasBeenClicked = true;
this.#setUpDragSession(event);
}
@ -732,74 +742,47 @@ class AnnotationEditor {
return;
}
// Avoid to have spurious text selection in the text layer when dragging.
this._uiManager.disableUserSelect(true);
const isSelected = this._uiManager.isSelected(this);
this._uiManager.setUpDragSession();
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(
let pointerMoveOptions, pointerMoveCallback;
if (isSelected) {
pointerMoveOptions = { passive: true, capture: true };
pointerMoveCallback = e => {
const [tx, ty] = this.screenToPageTranslation(e.movementX, e.movementY);
this._uiManager.dragSelectedEditors(tx, ty);
};
window.addEventListener(
"pointermove",
pointerMoveCallback,
pointerMoveOptions
);
const newParent = this.parent;
const newX = this.x;
const newY = this.y;
if (newParent === savedParent && newX === savedX && newY === savedY) {
return;
}
const pointerUpCallback = () => {
window.removeEventListener("pointerup", pointerUpCallback);
window.removeEventListener("blur", pointerUpCallback);
if (isSelected) {
window.removeEventListener(
"pointermove",
pointerMoveCallback,
pointerMoveOptions
);
}
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.#hasBeenClicked = false;
if (!this._uiManager.endDragSession()) {
const { isMac } = FeatureTest.platform;
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
}
};
window.addEventListener("pointerup", pointerUpCallback);
// If the user is using alt+tab during the dragging session, the pointerup
@ -808,6 +791,18 @@ class AnnotationEditor {
window.addEventListener("blur", pointerUpCallback);
}
moveInDOM() {
this.parent.moveEditorInDOM(this);
}
_setParentAndPosition(parent, x, y) {
parent.changeParent(this);
this.x = x;
this.y = y;
this.fixAndSetPosition();
this.moveInDOM();
}
/**
* Convert the current rect into a page one.
*/

View File

@ -281,6 +281,9 @@ class FreeTextEditor extends AnnotationEditor {
/** @inheritdoc */
rebuild() {
if (!this.parent) {
return;
}
super.rebuild();
if (this.div === null) {
return;
@ -447,7 +450,7 @@ class FreeTextEditor extends AnnotationEditor {
return;
}
this.#setContent();
this.rebuild();
this._uiManager.rebuild(this);
this.#setEditorDimensions();
};
this.addCommands({

View File

@ -226,6 +226,9 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */
rebuild() {
if (!this.parent) {
return;
}
super.rebuild();
if (this.div === null) {
return;
@ -626,7 +629,7 @@ class InkEditor extends AnnotationEditor {
// When commiting, the position of this editor is changed, hence we must
// move it to the right position in the DOM.
this.parent.moveEditorInDOM(this);
this.moveInDOM();
this.div.focus({
preventScroll: true /* See issue #15744 */,
});

View File

@ -161,6 +161,14 @@ class StampEditor extends AnnotationEditor {
/** @inheritdoc */
rebuild() {
if (!this.parent) {
// It's possible to have to rebuild an editor which is not on a visible
// page.
if (this.#bitmapId) {
this.#getBitmap();
}
return;
}
super.rebuild();
if (this.div === null) {
return;

View File

@ -532,6 +532,8 @@ class AnnotationEditorUIManager {
#deletedAnnotationsElementIds = new Set();
#draggingEditors = null;
#editorTypes = null;
#editorsToRescale = new Set();
@ -1059,6 +1061,10 @@ class AnnotationEditorUIManager {
return this.#allLayers.get(this.#currentPageIndex);
}
getLayer(pageIndex) {
return this.#allLayers.get(pageIndex);
}
get currentPageIndex() {
return this.#currentPageIndex;
}
@ -1173,7 +1179,7 @@ class AnnotationEditorUIManager {
}
/**
* Get all the editors belonging to a give page.
* Get all the editors belonging to a given page.
* @param {number} pageIndex
* @returns {Array<AnnotationEditor>}
*/
@ -1520,6 +1526,123 @@ class AnnotationEditorUIManager {
}
}
/**
* Set up the drag session for moving the selected editors.
*/
setUpDragSession() {
if (!this.hasSelection) {
return;
}
// Avoid to have spurious text selection in the text layer when dragging.
this.disableUserSelect(true);
this.#draggingEditors = new Map();
for (const editor of this.#selectedEditors) {
this.#draggingEditors.set(editor, {
savedX: editor.x,
savedY: editor.y,
savedPageIndex: editor.parent.pageIndex,
newX: 0,
newY: 0,
newPageIndex: -1,
});
}
}
/**
* Ends the drag session.
* @returns {boolean} true if at least one editor has been moved.
*/
endDragSession() {
if (!this.#draggingEditors) {
return false;
}
this.disableUserSelect(false);
const map = this.#draggingEditors;
this.#draggingEditors = null;
let mustBeAddedInUndoStack = false;
for (const [{ x, y, parent }, value] of map) {
value.newX = x;
value.newY = y;
value.newPageIndex = parent.pageIndex;
mustBeAddedInUndoStack ||=
x !== value.savedX ||
y !== value.savedY ||
parent.pageIndex !== value.savedPageIndex;
}
if (!mustBeAddedInUndoStack) {
return false;
}
const move = (editor, x, y, pageIndex) => {
if (this.#allEditors.has(editor.id)) {
// The editor can be undone/redone on a page which is not visible (and
// which potentially has no annotation editor layer), hence we need to
// use the pageIndex instead of the parent.
const parent = this.#allLayers.get(pageIndex);
if (parent) {
editor._setParentAndPosition(parent, x, y);
} else {
editor.pageIndex = pageIndex;
editor.x = x;
editor.y = y;
}
}
};
this.addCommands({
cmd: () => {
for (const [editor, { newX, newY, newPageIndex }] of map) {
move(editor, newX, newY, newPageIndex);
}
},
undo: () => {
for (const [editor, { savedX, savedY, savedPageIndex }] of map) {
move(editor, savedX, savedY, savedPageIndex);
}
},
mustExec: true,
});
return true;
}
/**
* Drag the set of selected editors.
* @param {number} tx
* @param {number} ty
*/
dragSelectedEditors(tx, ty) {
if (!this.#draggingEditors) {
return;
}
for (const editor of this.#draggingEditors.keys()) {
editor.drag(tx, ty);
}
}
/**
* Rebuild the editor (usually on undo/redo actions) on a potentially
* non-rendered page.
* @param {AnnotationEditor} editor
*/
rebuild(editor) {
if (editor.parent === null) {
const parent = this.getLayer(editor.pageIndex);
if (parent) {
parent.changeParent(editor);
parent.addOrRebuild(editor);
} else {
this.addEditor(editor);
this.addToAnnotationStorage(editor);
editor.rebuild();
}
} else {
editor.parent.addOrRebuild(editor);
}
}
/**
* Is the current editor the one passed as argument?
* @param {AnnotationEditor} editor

View File

@ -921,6 +921,9 @@ describe("FreeText Editor", () => {
return { x, y, width, height };
});
// Select the annotation we want to move.
await page.mouse.click(editorRect.x + 2, editorRect.y + 2);
await dragAndDropAnnotation(
page,
editorRect.x + editorRect.width / 2,
@ -2193,4 +2196,73 @@ describe("FreeText Editor", () => {
);
});
});
describe("Move several FreeTexts", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must move several annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#editorFreeText");
const rect = await page.$eval(".annotationEditorLayer", el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const allPositions = [];
for (let i = 0; i < 10; i++) {
await page.mouse.click(rect.x + 10 + 30 * i, rect.y + 100 + 5 * i);
await page.waitForTimeout(10);
await page.type(
`${getEditorSelector(i)} .internal`,
String.fromCharCode(65 + i)
);
// Commit.
await page.keyboard.press("Escape");
await page.waitForTimeout(10);
allPositions.push(
await page.$eval(getEditorSelector(i), el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
})
);
}
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
await dragAndDropAnnotation(page, rect.x + 161, rect.y + 126, 39, 74);
await page.waitForTimeout(10);
for (let i = 0; i < 10; i++) {
const pos = await page.$eval(getEditorSelector(i), el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const oldPos = allPositions[i];
expect(Math.round(pos.x))
.withContext(`In ${browserName}`)
.toEqual(Math.round(oldPos.x + 39));
expect(Math.round(pos.y))
.withContext(`In ${browserName}`)
.toEqual(Math.round(oldPos.y + 74));
}
})
);
});
});
});

View File

@ -198,6 +198,7 @@ exports.serializeBitmapDimensions = serializeBitmapDimensions;
async function dragAndDropAnnotation(page, startX, startY, tX, tY) {
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.waitForTimeout(10);
await page.mouse.move(startX + tX, startY + tY);
await page.mouse.up();
}

View File

@ -81,7 +81,7 @@
}
.annotationEditorLayer
:is(.freeTextEditor, .inkEditor, .stampEditor).draggable {
:is(.freeTextEditor, .inkEditor, .stampEditor).draggable.selectedEditor {
cursor: move;
}