@@ -80,6 +80,16 @@ function RemoteFunctions(config = {}) {
8080 let _autoScrollTimer = null ;
8181 let _isAutoScrolling = false ; // to disable highlights when auto scrolling
8282
83+ // these variables are used when cycling between the possible drop locations
84+ let _dragHoverTimer = null ;
85+ let _lastDragX = null ;
86+ let _lastDragY = null ;
87+ let _lastDragTarget = null ;
88+ let _parentCycleList = [ ] ;
89+ let _currentCycleIndex = 0 ;
90+ let _currentDropZone = null ;
91+ let _currentIndicatorType = null ;
92+
8393 // initialized from config, defaults to true if not set
8494 let imageGallerySelected = config . imageGalleryState !== undefined ? config . imageGalleryState : true ;
8595
@@ -137,6 +147,8 @@ function RemoteFunctions(config = {}) {
137147 _autoScrollTimer = setInterval ( ( ) => {
138148 window . scrollBy ( 0 , scrollDirection ) ;
139149 } , 16 ) ; // 16 is ~60fps
150+ } else {
151+ _isAutoScrolling = false ;
140152 }
141153 }
142154
@@ -523,6 +535,7 @@ function RemoteFunctions(config = {}) {
523535 element . style . opacity = 1 ;
524536 }
525537 delete element . _originalDragOpacity ;
538+ _stopParentCycling ( ) ;
526539 }
527540
528541 /**
@@ -1235,6 +1248,161 @@ function RemoteFunctions(config = {}) {
12351248 }
12361249 }
12371250
1251+ /**
1252+ * this function is responsible to get all the elements that could be the users possible drop targets
1253+ * @param {DOMElement } target - the ele to start with
1254+ * @returns {Array } - array of all the possible elements (closest -> farthest)
1255+ */
1256+ function _getAllValidParentCandidates ( target ) {
1257+ const parents = [ ] ;
1258+ let current = target . parentElement ;
1259+
1260+ while ( current ) {
1261+ if ( current . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) && isElementEditable ( current ) ) {
1262+ parents . push ( current ) ;
1263+ }
1264+
1265+ // need to stop at body or html
1266+ if ( current . tagName . toLowerCase ( ) === 'body' ||
1267+ current . tagName . toLowerCase ( ) === 'html' ) {
1268+ break ;
1269+ }
1270+
1271+ current = current . parentElement ;
1272+ }
1273+
1274+ return parents ;
1275+ }
1276+
1277+ /**
1278+ * Cycle to the next parent target and update drop markers
1279+ */
1280+ function _cycleToNextParent ( ) {
1281+ if ( _parentCycleList . length <= 1 ) {
1282+ return ;
1283+ }
1284+
1285+ _currentCycleIndex = ( _currentCycleIndex + 1 ) % _parentCycleList . length ;
1286+ const nextTarget = _parentCycleList [ _currentCycleIndex ] ;
1287+
1288+ _currentIndicatorType = _getIndicatorType ( nextTarget ) ;
1289+
1290+ _clearDropMarkers ( ) ;
1291+ _createDropMarker ( nextTarget , _currentDropZone , _currentIndicatorType ) ;
1292+
1293+ _lastDragTarget = nextTarget ;
1294+
1295+ _dragHoverTimer = setTimeout ( ( ) => {
1296+ _cycleToNextParent ( ) ;
1297+ } , 1000 ) ;
1298+ }
1299+
1300+ /**
1301+ * this function is to check whether an element is a semantic ele or not
1302+ * @param {DOMElement } element - the ele that we need to check
1303+ * @returns {Boolean } - true when its semantic otherwise false
1304+ */
1305+ function _isSemanticElement ( element ) {
1306+ const semanticTags = [
1307+ 'div' , 'section' , 'article' , 'aside' , 'header' , 'footer' , 'main' , 'nav' ,
1308+ 'form' , 'fieldset' , 'details' , 'figure' , 'ul' , 'ol' , 'dl' , 'table' , 'tbody' ,
1309+ 'thead' , 'tfoot' , 'tr' , 'blockquote' , 'pre' , 'address'
1310+ ] ;
1311+ return semanticTags . includes ( element . tagName . toLowerCase ( ) ) ;
1312+ }
1313+
1314+ /**
1315+ * to check whether the 3 edges are aligned or not based on the drop zone
1316+ * for ex: if arrow marker is shown for the top, then top + left + right egde should align
1317+ * for bottom: bottom + left + right
1318+ * this means that there might be a possibility that the child element is blocking access of the parent ele
1319+ * @param {DOMElement } child
1320+ * @param {DOMElement } parent
1321+ * @param {String } dropZone - "before" or "after" (not called for "inside")
1322+ * @returns {Boolean } - true if matches
1323+ */
1324+ function _hasThreeEdgesAligned ( child , parent , dropZone ) {
1325+ const tolerance = 2 ; // 2px tolerance
1326+ const childRect = child . getBoundingClientRect ( ) ;
1327+ const parentRect = parent . getBoundingClientRect ( ) ;
1328+
1329+ const topAligned = Math . abs ( childRect . top - parentRect . top ) <= tolerance ;
1330+ const bottomAligned = Math . abs ( childRect . bottom - parentRect . bottom ) <= tolerance ;
1331+ const leftAligned = Math . abs ( childRect . left - parentRect . left ) <= tolerance ;
1332+ const rightAligned = Math . abs ( childRect . right - parentRect . right ) <= tolerance ;
1333+
1334+ // For "before" drops (top), check: top + left + right (bottom doesn't matter)
1335+ // For "after" drops (bottom), check: bottom + left + right (top doesn't matter)
1336+ if ( dropZone === "before" ) {
1337+ return topAligned && leftAligned && rightAligned ;
1338+ } else if ( dropZone === "after" ) {
1339+ return bottomAligned && leftAligned && rightAligned ;
1340+ }
1341+
1342+ return false ;
1343+ }
1344+
1345+ /**
1346+ * start the parent cycling timer, only to go it when parent has a genuine possibility of being the drop target
1347+ * 3 cases we check to say if parent might be a possibility:
1348+ * (same arrow type, correct 3 edges aligned, semantic parent)
1349+ *
1350+ * @param {DOMElement } initialTarget - The element user is hovering over
1351+ */
1352+ function _startParentCycling ( initialTarget ) {
1353+ // no cycling when drop zone is inside
1354+ if ( _currentDropZone === "inside" ) {
1355+ return ;
1356+ }
1357+
1358+ const initialIndicatorType = _currentIndicatorType ;
1359+ const allParents = _getAllValidParentCandidates ( initialTarget ) ;
1360+
1361+ // (same arrow type, correct 3 edges aligned, semantic parent)
1362+ // all this cases should match for it to be a valid parent
1363+ const validParents = allParents . filter ( parent => {
1364+ if ( ! _isSemanticElement ( parent ) ) {
1365+ return false ;
1366+ }
1367+
1368+ // make sure parent has the same indicator type
1369+ const parentIndicatorType = _getIndicatorType ( parent ) ;
1370+ if ( parentIndicatorType !== initialIndicatorType ) {
1371+ return false ;
1372+ }
1373+
1374+ // the correct 3 edges should align
1375+ return _hasThreeEdgesAligned ( initialTarget , parent , _currentDropZone ) ;
1376+ } ) ;
1377+
1378+ _parentCycleList = [ initialTarget , ...validParents ] ;
1379+ if ( _parentCycleList . length <= 1 ) {
1380+ return ;
1381+ }
1382+
1383+ _currentCycleIndex = 0 ;
1384+ _dragHoverTimer = setTimeout ( ( ) => {
1385+ _cycleToNextParent ( ) ;
1386+ } , 1000 ) ;
1387+ }
1388+
1389+ /**
1390+ * Stop parent cycling and clean up state
1391+ */
1392+ function _stopParentCycling ( ) {
1393+ if ( _dragHoverTimer ) {
1394+ clearTimeout ( _dragHoverTimer ) ;
1395+ _dragHoverTimer = null ;
1396+ }
1397+ _parentCycleList = [ ] ;
1398+ _currentCycleIndex = 0 ;
1399+ _lastDragX = null ;
1400+ _lastDragY = null ;
1401+ _lastDragTarget = null ;
1402+ _currentDropZone = null ;
1403+ _currentIndicatorType = null ;
1404+ }
1405+
12381406 /**
12391407 * this function is for finding the best target element on where to drop the dragged element
12401408 * for ex: div > image...here both the div and image are of the exact same size, then when user is dragging some
@@ -1361,27 +1529,41 @@ function RemoteFunctions(config = {}) {
13611529 target = bestParent ;
13621530 }
13631531
1364- // Store original styles before modifying them
1365- if ( target . _originalDragBackgroundColor === undefined ) {
1366- target . _originalDragBackgroundColor = target . style . backgroundColor ;
1367- }
1368- if ( target . _originalDragTransition === undefined ) {
1369- target . _originalDragTransition = target . style . transition ;
1370- }
1532+ // check if cursor position changed significantly (>5px threshold to avoid jitter)
1533+ const positionChanged = _lastDragX === null || _lastDragY === null ||
1534+ Math . abs ( _lastDragX - event . clientX ) > 5 ||
1535+ Math . abs ( _lastDragY - event . clientY ) > 5 ;
13711536
1372- // Add subtle hover effect to target element
1373- target . style . backgroundColor = "rgba(66, 133, 244, 0.22)" ;
1374- target . style . transition = "background-color 0.2s ease" ;
1537+ // check if we moved to a different target element
1538+ // during cycling, ignore element detection to prevent reset loop
1539+ const elementChanged = ( _parentCycleList . length === 0 ) && ( _lastDragTarget !== target ) ;
13751540
1376- // Determine indicator type and drop zone based on container layout and cursor position
1377- const indicatorType = _getIndicatorType ( target ) ;
1378- const dropZone = _getDropZone (
1379- target , event . clientX , event . clientY , indicatorType , window . _currentDraggedElement
1380- ) ;
1541+ if ( elementChanged || positionChanged ) {
1542+ // reset as user moved to a new element or position changed significantly
1543+ _stopParentCycling ( ) ;
13811544
1382- // before creating a drop marker, make sure that we clear all the drop markers
1383- _clearDropMarkers ( ) ;
1384- _createDropMarker ( target , dropZone , indicatorType ) ;
1545+ _lastDragX = event . clientX ;
1546+ _lastDragY = event . clientY ;
1547+ _lastDragTarget = target ;
1548+
1549+ const indicatorType = _getIndicatorType ( target ) ;
1550+ const dropZone = _getDropZone ( target , event . clientX , event . clientY , indicatorType , window . _currentDraggedElement ) ;
1551+
1552+ _currentIndicatorType = indicatorType ;
1553+ _currentDropZone = dropZone ;
1554+
1555+ if ( ! _isAutoScrolling ) {
1556+ _clearDropMarkers ( ) ;
1557+ _createDropMarker ( target , dropZone , indicatorType ) ;
1558+ }
1559+
1560+ _startParentCycling ( target ) ;
1561+ } else if ( _dragHoverTimer === null && ! _isAutoScrolling ) {
1562+ // hovering in same spot but timer was cleared, so restart it
1563+ _startParentCycling ( target ) ;
1564+ }
1565+
1566+ // Handle auto-scroll
13851567 _handleAutoScroll ( event . clientY ) ;
13861568 }
13871569
@@ -1393,6 +1575,7 @@ function RemoteFunctions(config = {}) {
13931575 if ( ! event . relatedTarget ) {
13941576 _clearDropMarkers ( ) ;
13951577 _stopAutoScroll ( ) ;
1578+ _stopParentCycling ( ) ;
13961579 }
13971580 }
13981581
@@ -1409,21 +1592,32 @@ function RemoteFunctions(config = {}) {
14091592 event . preventDefault ( ) ;
14101593 event . stopPropagation ( ) ;
14111594
1412- // get the element under the cursor
1413- let target = document . elementFromPoint ( event . clientX , event . clientY ) ;
1595+ let target = _lastDragTarget ;
14141596
1415- // get the closest editable element
1416- while ( target && ! target . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ) {
1417- target = target . parentElement ;
1418- }
1597+ if ( ! target ) {
1598+ // get the element under the cursor
1599+ target = document . elementFromPoint ( event . clientX , event . clientY ) ;
14191600
1420- if ( ! isElementEditable ( target ) || target === window . _currentDraggedElement ) {
1421- // if direct detection fails, we try to find a nearby valid target
1422- target = _findNearestValidTarget ( event . clientX , event . clientY ) ;
1601+ // get the closest editable element
1602+ while ( target && ! target . hasAttribute ( GLOBALS . DATA_BRACKETS_ID_ATTR ) ) {
1603+ target = target . parentElement ;
1604+ }
1605+
1606+ if ( ! isElementEditable ( target ) || target === window . _currentDraggedElement ) {
1607+ // if direct detection fails, we try to find a nearby valid target
1608+ target = _findNearestValidTarget ( event . clientX , event . clientY ) ;
1609+ }
1610+
1611+ // Check if we should prefer a parent when all edges are aligned
1612+ const bestParent = _findBestParentTarget ( target ) ;
1613+ if ( bestParent ) {
1614+ target = bestParent ;
1615+ }
14231616 }
14241617
14251618 // skip if no valid target found or if it's the dragged element
14261619 if ( ! isElementEditable ( target ) || target === window . _currentDraggedElement ) {
1620+ _stopParentCycling ( ) ;
14271621 _clearDropMarkers ( ) ;
14281622 _stopAutoScroll ( ) ;
14291623 _dragEndChores ( window . _currentDraggedElement ) ;
@@ -1432,12 +1626,6 @@ function RemoteFunctions(config = {}) {
14321626 return ;
14331627 }
14341628
1435- // Check if we should prefer a parent when all edges are aligned
1436- const bestParent = _findBestParentTarget ( target ) ;
1437- if ( bestParent ) {
1438- target = bestParent ;
1439- }
1440-
14411629 // Determine drop position based on container layout and cursor position
14421630 const indicatorType = _getIndicatorType ( target ) ;
14431631 const dropZone = _getDropZone (
@@ -1470,6 +1658,7 @@ function RemoteFunctions(config = {}) {
14701658 // send message to the editor
14711659 window . _Brackets_MessageBroker . send ( messageData ) ;
14721660
1661+ _stopParentCycling ( ) ;
14731662 _clearDropMarkers ( ) ;
14741663 _stopAutoScroll ( ) ;
14751664 _dragEndChores ( window . _currentDraggedElement ) ;
0 commit comments