Skip to content

Commit 4b7fa1c

Browse files
committed
Avoid to scroll too much when the thumbnail is at the bottom of the sidebar (bug 2016693)
1 parent f626a36 commit 4b7fa1c

File tree

1 file changed

+121
-29
lines changed

1 file changed

+121
-29
lines changed

web/pdf_thumbnail_viewer.js

Lines changed: 121 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ class PDFThumbnailViewer {
126126

127127
#isCut = false;
128128

129+
#isOneColumnView = false;
130+
131+
#scrollableContainerWidth = 0;
132+
133+
#scrollableContainerHeight = 0;
134+
129135
/**
130136
* @param {PDFThumbnailViewerOptions} options
131137
*/
@@ -716,18 +722,36 @@ class PDFThumbnailViewer {
716722
}
717723

718724
#moveDraggedContainer(dx, dy) {
719-
this.#draggedImageOffsetX += dx;
720-
this.#draggedImageOffsetY += dy;
725+
if (this.#isOneColumnView) {
726+
dx = 0;
727+
}
728+
if (
729+
this.#draggedImageX + dx < 0 ||
730+
this.#draggedImageX + this.#draggedImageWidth + dx >
731+
this.#scrollableContainerWidth
732+
) {
733+
dx = 0;
734+
}
735+
if (
736+
this.#draggedImageY + dy < 0 ||
737+
this.#draggedImageY + this.#draggedImageHeight + dy >
738+
this.#scrollableContainerHeight
739+
) {
740+
dy = 0;
741+
}
742+
721743
this.#draggedImageX += dx;
722744
this.#draggedImageY += dy;
745+
this.#draggedImageOffsetX += dx;
746+
this.#draggedImageOffsetY += dy;
723747
this.#draggedContainer.style.translate = `${this.#draggedImageOffsetX}px ${this.#draggedImageOffsetY}px`;
724748
if (
725749
this.#draggedImageY + this.#draggedImageHeight >
726750
this.#currentScrollBottom
727751
) {
728752
this.scrollableContainer.scrollTop = Math.min(
729753
this.scrollableContainer.scrollTop + PIXELS_TO_SCROLL_WHEN_DRAGGING,
730-
this.scrollableContainer.scrollHeight
754+
this.#scrollableContainerHeight
731755
);
732756
} else if (this.#draggedImageY < this.#currentScrollTop) {
733757
this.scrollableContainer.scrollTop = Math.max(
@@ -839,6 +863,11 @@ class PDFThumbnailViewer {
839863
lastSpace: (positionsLastX.at(-1) - lastRightX) / 2,
840864
bbox,
841865
};
866+
this.#isOneColumnView = positionsX.length === 1;
867+
({
868+
clientWidth: this.#scrollableContainerWidth,
869+
scrollHeight: this.#scrollableContainerHeight,
870+
} = this.scrollableContainer);
842871
}
843872

844873
#addEventListeners() {
@@ -950,6 +979,7 @@ class PDFThumbnailViewer {
950979
pointerId: dragPointerId,
951980
} = e;
952981
if (
982+
e.button !== 0 || // Skip right click.
953983
this.#pagesMapper.copiedPageNumbers?.length > 0 ||
954984
!isNaN(this.#lastDraggedOverIndex) ||
955985
!draggedImage.classList.contains("thumbnailImageContainer")
@@ -969,11 +999,18 @@ class PDFThumbnailViewer {
969999
// same position on the thumbnail, we need to adjust the offset
9701000
// accordingly.
9711001
const scaleFactor = PDFThumbnailViewer.#getScaleFactor(draggedImage);
972-
this.#draggedImageOffsetX =
973-
((scaleFactor - 1) * e.layerX + draggedImage.offsetLeft) / scaleFactor;
9741002
this.#draggedImageOffsetY =
9751003
((scaleFactor - 1) * e.layerY + draggedImage.offsetTop) / scaleFactor;
9761004

1005+
if (this.#isOneColumnView) {
1006+
this.#draggedImageOffsetX =
1007+
draggedImage.offsetLeft +
1008+
((scaleFactor - 1) * 0.5 * draggedImage.offsetWidth) / scaleFactor;
1009+
} else {
1010+
this.#draggedImageOffsetX =
1011+
((scaleFactor - 1) * e.layerX + draggedImage.offsetLeft) /
1012+
scaleFactor;
1013+
}
9771014
this.#draggedImageX = thumbnail.offsetLeft + this.#draggedImageOffsetX;
9781015
this.#draggedImageY = thumbnail.offsetTop + this.#draggedImageOffsetY;
9791016
this.#draggedImageWidth = draggedImage.offsetWidth / scaleFactor;
@@ -983,16 +1020,16 @@ class PDFThumbnailViewer {
9831020
"pointermove",
9841021
ev => {
9851022
const { clientX: x, clientY: y, pointerId } = ev;
986-
if (
987-
pointerId !== dragPointerId ||
988-
(Math.abs(x - clickX) <= DRAG_THRESHOLD_IN_PIXELS &&
989-
Math.abs(y - clickY) <= DRAG_THRESHOLD_IN_PIXELS)
990-
) {
991-
// Not enough movement to be considered a drag.
992-
return;
993-
}
994-
9951023
if (isNaN(this.#lastDraggedOverIndex)) {
1024+
if (
1025+
pointerId !== dragPointerId ||
1026+
(Math.abs(x - clickX) <= DRAG_THRESHOLD_IN_PIXELS &&
1027+
Math.abs(y - clickY) <= DRAG_THRESHOLD_IN_PIXELS)
1028+
) {
1029+
// Not enough movement to be considered a drag.
1030+
return;
1031+
}
1032+
9961033
// First movement while dragging.
9971034
this.#onStartDragging(thumbnail);
9981035
const stopDragging = (_e, isDropping = false) => {
@@ -1043,6 +1080,18 @@ class PDFThumbnailViewer {
10431080
passive: false,
10441081
signal,
10451082
});
1083+
window.addEventListener(
1084+
"keydown",
1085+
kEv => {
1086+
if (
1087+
kEv.key === "Escape" &&
1088+
!isNaN(this.#lastDraggedOverIndex)
1089+
) {
1090+
stopDragging(kEv);
1091+
}
1092+
},
1093+
{ signal }
1094+
);
10461095
}
10471096

10481097
const dx = x - prevDragX;
@@ -1151,6 +1200,16 @@ class PDFThumbnailViewer {
11511200
}
11521201
}
11531202

1203+
// Given the drag center (x, y), find the drop slot index: the drag marker
1204+
// will be placed after thumbnail[index], or before all thumbnails if index
1205+
// is -1. Returns null when the drop slot hasn't changed (no marker update
1206+
// needed), or [index, space] where space is the gap (in px) between
1207+
// thumbnails at that slot, used to position the marker.
1208+
//
1209+
// positionsX holds the x-center of each column, positionsY the y-center of
1210+
// each row. positionsLastX holds the x-centers for an incomplete last row
1211+
// (when the total number of thumbnails is not a multiple of the column
1212+
// count).
11541213
#findClosestThumbnail(x, y) {
11551214
if (!this.#thumbnailsPositions) {
11561215
this.#computeThumbnailsPosition();
@@ -1163,6 +1222,9 @@ class PDFThumbnailViewer {
11631222
lastSpace: lastSpaceBetweenThumbnails,
11641223
} = this.#thumbnailsPositions;
11651224
const lastDraggedOverIndex = this.#lastDraggedOverIndex;
1225+
1226+
// Fast-path: reconstruct the row/col of the previous drop slot and check
1227+
// whether (x, y) still falls inside the same cell's bounds.
11661228
let xPos = lastDraggedOverIndex % positionsX.length;
11671229
let yPos = Math.floor(lastDraggedOverIndex / positionsX.length);
11681230
let xArray = yPos === positionsY.length - 1 ? positionsLastX : positionsX;
@@ -1176,28 +1238,58 @@ class PDFThumbnailViewer {
11761238
return null;
11771239
}
11781240

1179-
yPos = binarySearchFirstItem(positionsY, cy => y < cy) - 1;
1180-
xArray =
1181-
yPos === positionsY.length - 1 && positionsLastX.length > 0
1182-
? positionsLastX
1183-
: positionsX;
1184-
xPos = Math.max(0, binarySearchFirstItem(xArray, cx => x < cx) - 1);
1185-
if (yPos < 0) {
1186-
if (xPos <= 0) {
1187-
xPos = -1;
1241+
let index;
1242+
// binarySearchFirstItem returns the first row index whose center is below
1243+
// y, i.e. the first i such that positionsY[i] > y.
1244+
yPos = binarySearchFirstItem(positionsY, cy => y < cy);
1245+
if (this.#isOneColumnView) {
1246+
// In a single column the drop slot is simply the row boundary: the marker
1247+
// goes after row (yPos - 1), meaning before row yPos. index = -1 when y
1248+
// is above the first thumbnail's center (drop before thumbnail 0).
1249+
index = yPos - 1;
1250+
} else {
1251+
// Grid layout: first pick the nearest row, then the nearest column.
1252+
1253+
if (yPos === positionsY.length) {
1254+
// y is below the last row's center — clamp to the last row.
1255+
yPos = positionsY.length - 1;
1256+
} else {
1257+
// Choose between the row just above (yPos - 1) and the row at yPos by
1258+
// comparing distances, so the marker snaps to whichever row center is
1259+
// closer to y.
1260+
const dist1 = Math.abs(positionsY[yPos - 1] - y);
1261+
const dist2 = Math.abs(positionsY[yPos] - y);
1262+
yPos = dist1 < dist2 ? yPos - 1 : yPos;
11881263
}
1189-
yPos = 0;
1264+
// The last row may be incomplete, so use its own x-center array.
1265+
xArray =
1266+
yPos === positionsY.length - 1 && positionsLastX.length > 0
1267+
? positionsLastX
1268+
: positionsX;
1269+
// Find the column: the first column whose center is to the right of x,
1270+
// minus 1, gives the column the cursor is in (or -1 if before column 0).
1271+
xPos = binarySearchFirstItem(xArray, cx => x < cx) - 1;
1272+
if (yPos < 0) {
1273+
// y is above the first row: force drop before the very first thumbnail.
1274+
if (xPos <= 0) {
1275+
xPos = -1;
1276+
}
1277+
yPos = 0;
1278+
}
1279+
// Convert (row, col) to a flat thumbnail index, clamped to
1280+
// [-1, length-1].
1281+
index = MathClamp(
1282+
yPos * positionsX.length + xPos,
1283+
-1,
1284+
this._thumbnails.length - 1
1285+
);
11901286
}
1191-
const index = MathClamp(
1192-
yPos * positionsX.length + xPos,
1193-
-1,
1194-
this._thumbnails.length - 1
1195-
);
11961287
if (index === lastDraggedOverIndex) {
11971288
// No change.
11981289
return null;
11991290
}
12001291
this.#lastDraggedOverIndex = index;
1292+
// Use the last-row gap when the drop slot is in the incomplete last row.
12011293
const space =
12021294
yPos === positionsY.length - 1 && positionsLastX.length > 0 && xPos >= 0
12031295
? lastSpaceBetweenThumbnails

0 commit comments

Comments
 (0)