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:
commit
ea259710fa
@ -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();
|
||||
};
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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({
|
||||
|
@ -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 */,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
.annotationEditorLayer
|
||||
:is(.freeTextEditor, .inkEditor, .stampEditor).draggable {
|
||||
:is(.freeTextEditor, .inkEditor, .stampEditor).draggable.selectedEditor {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user