@@ -127,6 +127,12 @@ class PDFThumbnailViewer {
127127
128128 #isCut = false ;
129129
130+ #isOneColumnView = false ;
131+
132+ #scrollableContainerWidth = 0 ;
133+
134+ #scrollableContainerHeight = 0 ;
135+
130136 /**
131137 * @param {PDFThumbnailViewerOptions } options
132138 */
@@ -720,18 +726,36 @@ class PDFThumbnailViewer {
720726 }
721727
722728 #moveDraggedContainer( dx , dy ) {
723- this . #draggedImageOffsetX += dx ;
724- this . #draggedImageOffsetY += dy ;
729+ if ( this . #isOneColumnView) {
730+ dx = 0 ;
731+ }
732+ if (
733+ this . #draggedImageX + dx < 0 ||
734+ this . #draggedImageX + this . #draggedImageWidth + dx >
735+ this . #scrollableContainerWidth
736+ ) {
737+ dx = 0 ;
738+ }
739+ if (
740+ this . #draggedImageY + dy < 0 ||
741+ this . #draggedImageY + this . #draggedImageHeight + dy >
742+ this . #scrollableContainerHeight
743+ ) {
744+ dy = 0 ;
745+ }
746+
725747 this . #draggedImageX += dx ;
726748 this . #draggedImageY += dy ;
749+ this . #draggedImageOffsetX += dx ;
750+ this . #draggedImageOffsetY += dy ;
727751 this . #draggedContainer. style . translate = `${ this . #draggedImageOffsetX} px ${ this . #draggedImageOffsetY} px` ;
728752 if (
729753 this . #draggedImageY + this . #draggedImageHeight >
730754 this . #currentScrollBottom
731755 ) {
732756 this . scrollableContainer . scrollTop = Math . min (
733757 this . scrollableContainer . scrollTop + PIXELS_TO_SCROLL_WHEN_DRAGGING ,
734- this . scrollableContainer . scrollHeight
758+ this . #scrollableContainerHeight
735759 ) ;
736760 } else if ( this . #draggedImageY < this . #currentScrollTop) {
737761 this . scrollableContainer . scrollTop = Math . max (
@@ -843,6 +867,11 @@ class PDFThumbnailViewer {
843867 lastSpace : ( positionsLastX . at ( - 1 ) - lastRightX ) / 2 ,
844868 bbox,
845869 } ;
870+ this . #isOneColumnView = positionsX . length === 1 ;
871+ ( {
872+ clientWidth : this . #scrollableContainerWidth,
873+ scrollHeight : this . #scrollableContainerHeight,
874+ } = this . scrollableContainer ) ;
846875 }
847876
848877 #addEventListeners( ) {
@@ -954,6 +983,7 @@ class PDFThumbnailViewer {
954983 pointerId : dragPointerId ,
955984 } = e ;
956985 if (
986+ e . button !== 0 || // Skip right click.
957987 this . #pagesMapper. copiedPageNumbers ?. length > 0 ||
958988 ! isNaN ( this . #lastDraggedOverIndex) ||
959989 ! draggedImage . classList . contains ( "thumbnailImageContainer" )
@@ -973,11 +1003,18 @@ class PDFThumbnailViewer {
9731003 // same position on the thumbnail, we need to adjust the offset
9741004 // accordingly.
9751005 const scaleFactor = PDFThumbnailViewer . #getScaleFactor( draggedImage ) ;
976- this . #draggedImageOffsetX =
977- ( ( scaleFactor - 1 ) * e . layerX + draggedImage . offsetLeft ) / scaleFactor ;
9781006 this . #draggedImageOffsetY =
9791007 ( ( scaleFactor - 1 ) * e . layerY + draggedImage . offsetTop ) / scaleFactor ;
9801008
1009+ if ( this . #isOneColumnView) {
1010+ this . #draggedImageOffsetX =
1011+ draggedImage . offsetLeft +
1012+ ( ( scaleFactor - 1 ) * 0.5 * draggedImage . offsetWidth ) / scaleFactor ;
1013+ } else {
1014+ this . #draggedImageOffsetX =
1015+ ( ( scaleFactor - 1 ) * e . layerX + draggedImage . offsetLeft ) /
1016+ scaleFactor ;
1017+ }
9811018 this . #draggedImageX = thumbnail . offsetLeft + this . #draggedImageOffsetX;
9821019 this . #draggedImageY = thumbnail . offsetTop + this . #draggedImageOffsetY;
9831020 this . #draggedImageWidth = draggedImage . offsetWidth / scaleFactor ;
@@ -987,16 +1024,16 @@ class PDFThumbnailViewer {
9871024 "pointermove" ,
9881025 ev => {
9891026 const { clientX : x , clientY : y , pointerId } = ev ;
990- if (
991- pointerId !== dragPointerId ||
992- ( Math . abs ( x - clickX ) <= DRAG_THRESHOLD_IN_PIXELS &&
993- Math . abs ( y - clickY ) <= DRAG_THRESHOLD_IN_PIXELS )
994- ) {
995- // Not enough movement to be considered a drag.
996- return ;
997- }
998-
9991027 if ( isNaN ( this . #lastDraggedOverIndex) ) {
1028+ if (
1029+ pointerId !== dragPointerId ||
1030+ ( Math . abs ( x - clickX ) <= DRAG_THRESHOLD_IN_PIXELS &&
1031+ Math . abs ( y - clickY ) <= DRAG_THRESHOLD_IN_PIXELS )
1032+ ) {
1033+ // Not enough movement to be considered a drag.
1034+ return ;
1035+ }
1036+
10001037 // First movement while dragging.
10011038 this . #onStartDragging( thumbnail ) ;
10021039 const stopDragging = ( _e , isDropping = false ) => {
@@ -1047,6 +1084,18 @@ class PDFThumbnailViewer {
10471084 passive : false ,
10481085 signal,
10491086 } ) ;
1087+ window . addEventListener (
1088+ "keydown" ,
1089+ kEv => {
1090+ if (
1091+ kEv . key === "Escape" &&
1092+ ! isNaN ( this . #lastDraggedOverIndex)
1093+ ) {
1094+ stopDragging ( kEv ) ;
1095+ }
1096+ } ,
1097+ { signal }
1098+ ) ;
10501099 }
10511100
10521101 const dx = x - prevDragX ;
@@ -1155,6 +1204,16 @@ class PDFThumbnailViewer {
11551204 }
11561205 }
11571206
1207+ // Given the drag center (x, y), find the drop slot index: the drag marker
1208+ // will be placed after thumbnail[index], or before all thumbnails if index
1209+ // is -1. Returns null when the drop slot hasn't changed (no marker update
1210+ // needed), or [index, space] where space is the gap (in px) between
1211+ // thumbnails at that slot, used to position the marker.
1212+ //
1213+ // positionsX holds the x-center of each column, positionsY the y-center of
1214+ // each row. positionsLastX holds the x-centers for an incomplete last row
1215+ // (when the total number of thumbnails is not a multiple of the column
1216+ // count).
11581217 #findClosestThumbnail( x , y ) {
11591218 if ( ! this . #thumbnailsPositions) {
11601219 this . #computeThumbnailsPosition( ) ;
@@ -1167,6 +1226,9 @@ class PDFThumbnailViewer {
11671226 lastSpace : lastSpaceBetweenThumbnails ,
11681227 } = this . #thumbnailsPositions;
11691228 const lastDraggedOverIndex = this . #lastDraggedOverIndex;
1229+
1230+ // Fast-path: reconstruct the row/col of the previous drop slot and check
1231+ // whether (x, y) still falls inside the same cell's bounds.
11701232 let xPos = lastDraggedOverIndex % positionsX . length ;
11711233 let yPos = Math . floor ( lastDraggedOverIndex / positionsX . length ) ;
11721234 let xArray = yPos === positionsY . length - 1 ? positionsLastX : positionsX ;
@@ -1180,28 +1242,58 @@ class PDFThumbnailViewer {
11801242 return null ;
11811243 }
11821244
1183- yPos = binarySearchFirstItem ( positionsY , cy => y < cy ) - 1 ;
1184- xArray =
1185- yPos === positionsY . length - 1 && positionsLastX . length > 0
1186- ? positionsLastX
1187- : positionsX ;
1188- xPos = Math . max ( 0 , binarySearchFirstItem ( xArray , cx => x < cx ) - 1 ) ;
1189- if ( yPos < 0 ) {
1190- if ( xPos <= 0 ) {
1191- xPos = - 1 ;
1245+ let index ;
1246+ // binarySearchFirstItem returns the first row index whose center is below
1247+ // y, i.e. the first i such that positionsY[i] > y.
1248+ yPos = binarySearchFirstItem ( positionsY , cy => y < cy ) ;
1249+ if ( this . #isOneColumnView) {
1250+ // In a single column the drop slot is simply the row boundary: the marker
1251+ // goes after row (yPos - 1), meaning before row yPos. index = -1 when y
1252+ // is above the first thumbnail's center (drop before thumbnail 0).
1253+ index = yPos - 1 ;
1254+ } else {
1255+ // Grid layout: first pick the nearest row, then the nearest column.
1256+
1257+ if ( yPos === positionsY . length ) {
1258+ // y is below the last row's center — clamp to the last row.
1259+ yPos = positionsY . length - 1 ;
1260+ } else {
1261+ // Choose between the row just above (yPos - 1) and the row at yPos by
1262+ // comparing distances, so the marker snaps to whichever row center is
1263+ // closer to y.
1264+ const dist1 = Math . abs ( positionsY [ yPos - 1 ] - y ) ;
1265+ const dist2 = Math . abs ( positionsY [ yPos ] - y ) ;
1266+ yPos = dist1 < dist2 ? yPos - 1 : yPos ;
11921267 }
1193- yPos = 0 ;
1268+ // The last row may be incomplete, so use its own x-center array.
1269+ xArray =
1270+ yPos === positionsY . length - 1 && positionsLastX . length > 0
1271+ ? positionsLastX
1272+ : positionsX ;
1273+ // Find the column: the first column whose center is to the right of x,
1274+ // minus 1, gives the column the cursor is in (or -1 if before column 0).
1275+ xPos = binarySearchFirstItem ( xArray , cx => x < cx ) - 1 ;
1276+ if ( yPos < 0 ) {
1277+ // y is above the first row: force drop before the very first thumbnail.
1278+ if ( xPos <= 0 ) {
1279+ xPos = - 1 ;
1280+ }
1281+ yPos = 0 ;
1282+ }
1283+ // Convert (row, col) to a flat thumbnail index, clamped to
1284+ // [-1, length-1].
1285+ index = MathClamp (
1286+ yPos * positionsX . length + xPos ,
1287+ - 1 ,
1288+ this . _thumbnails . length - 1
1289+ ) ;
11941290 }
1195- const index = MathClamp (
1196- yPos * positionsX . length + xPos ,
1197- - 1 ,
1198- this . _thumbnails . length - 1
1199- ) ;
12001291 if ( index === lastDraggedOverIndex ) {
12011292 // No change.
12021293 return null ;
12031294 }
12041295 this . #lastDraggedOverIndex = index ;
1296+ // Use the last-row gap when the drop slot is in the incomplete last row.
12051297 const space =
12061298 yPos === positionsY . length - 1 && positionsLastX . length > 0 && xPos >= 0
12071299 ? lastSpaceBetweenThumbnails
0 commit comments