Determine whether user clicking scrollbar or content (onclick for native scroll bar)

EventsScrollbarJquery

Events Problem Overview


I'm trying to create custom events in JQuery that are supposed to detect when a scrollbar is clicked1.
I know there's lots of text, but all my questions are boldfaced and there's a JSFiddle example you can work on straight away.

Because I haven't found any built in functionality for this,
I had to create a hasScroll function, checking if the element has a scrollbar,

$.fn.hasScroll = function(axis){
    var overflow = this.css("overflow"),
        overflowAxis;
    
    if(typeof axis == "undefined" || axis == "y") overflowAxis = this.css("overflow-y");
    else overflowAxis = this.css("overflow-x");
    
    var bShouldScroll = this.get(0).scrollHeight > this.innerHeight();
    
    var bAllowedScroll = (overflow == "auto" || overflow == "visible") ||
                         (overflowAxis == "auto" || overflowAxis == "visible");
    
    var bOverrideScroll = overflow == "scroll" || overflowAxis == "scroll";
    
    return (bShouldScroll && bAllowedScroll) || bOverrideScroll;
};

and an inScrollRange function, checking if the click performed was within the scroll range.

var scrollSize = 18;

function inScrollRange(event){
    var x = event.pageX,
        y = event.pageY,
        e = $(event.target),
        hasY = e.hasScroll(),
        hasX = e.hasScroll("x"),
        rX = null,
        rY = null,
        bInX = false,
        bInY = false
    
    if(hasY){
        rY = new RECT();
        rY.top = e.offset().top;
        rY.right = e.offset().left + e.width();
        rY.bottom = rY.top +e.height();
        rY.left = rY.right - scrollSize;
            
        //if(hasX) rY.bottom -= scrollSize;
        bInY = inRect(rY, x, y);
    }
            
    if(hasX){
        rX = new RECT();
        rX.bottom = e.offset().top + e.height();
        rX.left = e.offset().left;
        rX.top = rX.bottom - scrollSize;
        rX.right = rX.left + e.width();
        
        //if(hasY) rX.right -= scrollSize;
        bInX = inRect(rX, x, y);
    }
             
    return bInX || bInY;
}

Are all scrollbar sizes uniform? E.g in Firefox and IE it's 18px.
Assuming there are no customized scrollbars, is there any extra padding or sizes in some browsers?

These functions all perform as intended (from what I can discern).

Making custom events was a bit trickier, but I got it to work somewhat. The only problem is that if the element clicked has a mousedown/up event attached to it, that will be triggered as well.

I can't seem to stop the other events from triggering while simultaneously triggering, what I call, the mousedownScroll/mouseupScroll events.

$.fn.mousedownScroll = function(fn, data){
    if(typeof fn == "undefined" && typeof data == "undefined"){
        $(this).trigger("mousedownScroll");
        return;
    }
    
    $(this).on("mousedownScroll", data, fn);
};

$.fn.mouseupScroll = function(fn, data){
    if(typeof fn == "undefined" && typeof data == "undefined"){
        $(this).trigger("mouseupScroll");
        return;
    }
    
    $(this).on("mouseupScroll", data, fn);
};

$(document).on("mousedown", function(e){
    if(inScrollRange(e)){
        $(e.target).trigger("mousedownScroll");
    }
});

$(document).on("mouseup", function(e){
    if(inScrollRange(e)){
        $(e.target).trigger("mouseupScroll");
    }
});

$("selector").mousedown(function(e){
    console.log("Clicked content."); //Fired when clicking scroller as well
});

$("selector").mousedownScroll(function(e){
    console.log("Clicked scroller.");
});

How do I stop the other "click" events from triggering?

While I'm asking, please feel free to optimize the code as much as possible.

Here's a JSFiddle to mess around with.

The reason I'm making this is because of a bigger plugin I'm developing. It's got a custom context menu that is showing up when I right click one of the scrollers. I don't want that. So I thought I should make an event that checks for scroll clicks (mouseup/downs) and then prevent the context menu from being displayed. In order to do that though, I need the scroll click to come before the normal click, and also, if possible, stop the normal clicks from firing.

I'm just thinking out loud here but maybe there's a way to get all the functions that are bound to the element and then switch the order in which they were added? I know that functions are executed in the order they were added (1st added 1st called), so, if I could tap into that process, perhaps the whole "registering" of the event to JQuery could just be inserted before the click events.


1 can only use mousedown/mouseup because click doesn't trigger when clicking on a scrollbar. If this is false, please provide a working example/code

Events Solutions


Solution 1 - Events

Solved:

A shortest scrollbar click detection I could come up with, tested on IE, Firefox, Chrome.

var clickedOnScrollbar = function(mouseX){
  if( $(window).outerWidth() <= mouseX ){
    return true;
  }
}

$(document).mousedown(function(e){
  if( clickedOnScrollbar(e.clientX) ){
    alert("clicked on scrollbar");
  }
});

Working example: https://jsfiddle.net/s6mho19z/

Solution 2 - Events

You may probably use this hack.

You could try hijacking the mousedown and mouseup events and avoiding them when click on a scrollbar with your custom powered function.

$.fn.mousedown = function(data, fn) {
    if ( fn == null ) {
        fn = data;
        data = null;
    }    
    var o = fn;
    fn = function(e){
        if(!inScrollRange(e)) {
            return o.apply(this, arguments);
        }
        return;
    };
    if ( arguments.length > 0 ) {
        return this.bind( "mousedown", data, fn );
    } 
    return this.trigger( "mousedown" );
};

And the inverse for mousedownScroll and mouseupScroll events.

$.fn.mousedownScroll = function(data, fn) {
    if ( fn == null ) {
        fn = data;
        data = null;
    }    
    var o = fn;
    fn = function(e){
        if(inScrollRange(e)) {
            e.type = "mousedownscroll";
            return o.apply(this, arguments);
        }
        return;
    };
    if ( arguments.length > 0 ) {
        return this.bind( "mousedown", data, fn );
    } 
    return this.trigger( "mousedown" );
};

By the way, I think the scrollbar width is an OS setting.

Solution 3 - Events

Use following solution to detect if user clicked mouse over element's scrollbar. Didn't test how it works with window's scrollbar. I guess Pete's solution works better with window scrolls.

window.addEventListener("mousedown", onMouseDown);

function onMouseDown(e) {
  if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) 
  {
      // mouse down over scroll element
   }
}

Solution 4 - Events

Ensure that the content of your scollarea completely [over]fills the parent div.

Then, you can differentiate between clicks on your content and clicks on your container.

html:

<div class='test container'><div class='test content'></div></div>
<div id="results">please click</div>

css:

#results {
  position: absolute;
  top: 110px;
  left: 10px;
}

.test {
  position: absolute;
  left: 0;
  top: 0;
  width: 100px;
  height: 100px;
  background-color: green;
}

.container {
  overflow: scroll;
}

.content {
  background-color: red;
}

js:

function log( _l ) {
  $("#results").html( _l );
}

$('.content').on( 'mousedown', function( e ) {
  log( "content-click" );
  e.stopPropagation();
});

$('.container').on( 'mousedown', function( e ) {
  var pageX = e.pageX;
  var pageY = e.pageY;
  log( "scrollbar-click" );
});

http://codepen.io/jedierikb/pen/JqaCb

Solution 5 - Events

I had the same problem in a previous project, and i recommend this solution. It's not very clean but it works and i doubt we can do much better with html. Here are the two steps of my solution:

1. Measure the width of the scrollbar on your Desktop environment.

In order to achieve this, at application startup, you perform the following things:

Add the following element to the body:

<div style='width: 50px; height: 50px; overflow: scroll'><div style='height: 1px;'/></div>

Measure the with of the inner div of the previously added element with jQUery's .width(), and store the width of the scrollbar somewhere (the width of the scollbar is 50 - inner div's with)

Remove the extra element used to measure scrollbar (now that you have the result, remove the element that you added to the body).

All these steps should not be visible by the user and you have the width of the scrollbar on your OS

For example, you can use this snippet:

var measureScrollBarWidth = function() {
	var scrollBarMeasure = $('<div />');
	$('body').append(scrollBarMeasure);
	scrollBarMeasure.width(50).height(50)
		.css({
			overflow: 'scroll',
			visibility: 'hidden',
		    position: 'absolute'
		});

	var scrollBarMeasureContent = $('<div />').height(1);
	scrollBarMeasure.append(scrollBarMeasureContent);
	
	var insideWidth = scrollBarMeasureContent.width();
	var outsideWitdh = scrollBarMeasure.width();
	scrollBarMeasure.remove();
	
	return outsideWitdh - insideWidth;
};

2. Check if a click is on the scrollbar.

Now that you have the width of the scrollbar, you can with the coordinates of the event compute the coordinates of the event relative to the scrollbar's location rectangle and perfom awesome things...

If you want to filter the clicks, you can return false in the handler to prevent their propagation.

Solution 6 - Events

There are many answers here that involve event.clientX, element.clientHeight, etc. They are all wrong. Do not use them.

  • As has been discussed above, there are platforms where the overflow: scroll scrollbars appear as overlays, or you may have forced it with overflow: overlay.
  • Macs may switch scrollbars between persistent and overlay by plugging or unplugging a mouse. This shoots down the "measure on startup" technique.
  • Vertical scrollbars appear on the left side with right to left reading order. This breaks comparing client width unless you have a bunch of special logic for right to left reading order that I bet will break because you're probably not testing RTL consistently.

You need to look at event.target. If necessary, use an inner element that occupies all of the client area of the scroll element, and see if event.target is that element or a descendant of it.

Solution 7 - Events

It should be pointed out that on Mac OSX 10.7+, there are not persistant scroll bars. Scroll bars appear when you scroll, and disappear when your done. They are also much smaller then 18px (they are 7px).

Screenshot: http://i.stack.imgur.com/zdrUS.png

Solution 8 - Events

I'll submit my own answer and accept Alexander's answer, because it made it work perfectly, and upvote Samuel's answer, because it correctly calculates the scrollbar width, which is what I needed as well.


That being said, I decided to make two independent events instead of trying to overwrite/override JQuery's mousedown event.

This gave me the flexibility I needed without messing with JQuery's own events, and was quite easy to do.

  • mousedownScroll
  • mousedownContent

Below are the two implementations using Alexanders, and my own. Both work as I originally intended them to, but the former is probably the best.


  • Here's a JSFiddle that implements Alexander's answer + Samuel's answer.

     $.fn.hasScroll = function(axis){
         var overflow = this.css("overflow"),
             overflowAxis;
         
         if(typeof axis == "undefined" || axis == "y") overflowAxis = this.css("overflow-y");
         else overflowAxis = this.css("overflow-x");
         
         var bShouldScroll = this.get(0).scrollHeight > this.innerHeight();
         
         var bAllowedScroll = (overflow == "auto" || overflow == "visible") ||
                              (overflowAxis == "auto" || overflowAxis == "visible");
         
         var bOverrideScroll = overflow == "scroll" || overflowAxis == "scroll";
         
         return (bShouldScroll && bAllowedScroll) || bOverrideScroll;
     };
     
     $.fn.mousedown = function(data, fn) {
         if ( fn == null ) {
             fn = data;
             data = null;
         }    
         var o = fn;
         fn = function(e){
             if(!inScrollRange(e)) {
                 return o.apply(this, arguments);
             }
             return;
         };
         if ( arguments.length > 0 ) {
             return this.bind( "mousedown", data, fn );
         } 
         return this.trigger( "mousedown" );
     };
     
     $.fn.mouseup = function(data, fn) {
         if ( fn == null ) {
             fn = data;
             data = null;
         }    
         var o = fn;
         fn = function(e){
             if(!inScrollRange(e)) {
                 return o.apply(this, arguments);
             }
             return;
         };
         if ( arguments.length > 0 ) {
             return this.bind( "mouseup", data, fn );
         } 
         return this.trigger( "mouseup" );
     };
     
     $.fn.mousedownScroll = function(data, fn) {
         if ( fn == null ) {
             fn = data;
             data = null;
         }    
         var o = fn;
         fn = function(e){
             if(inScrollRange(e)) {
                 e.type = "mousedownscroll";
                 return o.apply(this, arguments);
             }
             return;
         };
         if ( arguments.length > 0 ) {
             return this.bind( "mousedown", data, fn );
         } 
         return this.trigger( "mousedown" );
     };
     
     $.fn.mouseupScroll = function(data, fn) {
         if ( fn == null ) {
             fn = data;
             data = null;
         }    
         var o = fn;
         fn = function(e){
             if(inScrollRange(e)) {
                 e.type = "mouseupscroll";
                 return o.apply(this, arguments);
             }
             return;
         };
         if ( arguments.length > 0 ) {
             return this.bind( "mouseup", data, fn );
         } 
         return this.trigger( "mouseup" );
     };
     
     var RECT = function(){
         this.top = 0;
         this.left = 0;
         this.bottom = 0;
         this.right = 0;
     }
         
     function inRect(rect, x, y){
         return (y >= rect.top && y <= rect.bottom) &&
                (x >= rect.left && x <= rect.right)
     }
     
     
     var scrollSize = measureScrollWidth();
     
     function inScrollRange(event){
         var x = event.pageX,
             y = event.pageY,
             e = $(event.target),
             hasY = e.hasScroll(),
             hasX = e.hasScroll("x"),
             rX = null,
             rY = null,
             bInX = false,
             bInY = false
         
         if(hasY){ 
             rY = new RECT();
             rY.top = e.offset().top;
             rY.right = e.offset().left + e.width();
             rY.bottom = rY.top +e.height();
             rY.left = rY.right - scrollSize;
                 
             //if(hasX) rY.bottom -= scrollSize;
             bInY = inRect(rY, x, y);
         }
                 
         if(hasX){
             rX = new RECT();
             rX.bottom = e.offset().top + e.height();
             rX.left = e.offset().left;
             rX.top = rX.bottom - scrollSize;
             rX.right = rX.left + e.width();
             
             //if(hasY) rX.right -= scrollSize;
             bInX = inRect(rX, x, y);
         }
                  
         return bInX || bInY;
     }
     
     $(document).on("mousedown", function(e){
         //Determine if has scrollbar(s)
         if(inScrollRange(e)){
             $(e.target).trigger("mousedownScroll");
         }
     });
     
     $(document).on("mouseup", function(e){
         if(inScrollRange(e)){
             $(e.target).trigger("mouseupScroll");
         }
     });
     });
    
     function measureScrollWidth() {
         var scrollBarMeasure = $('<div />');
         $('body').append(scrollBarMeasure);
         scrollBarMeasure.width(50).height(50)
             .css({
                 overflow: 'scroll',
                 visibility: 'hidden',
                 position: 'absolute'
             });
    
         var scrollBarMeasureContent = $('<div />').height(1);
         scrollBarMeasure.append(scrollBarMeasureContent);
    
         var insideWidth = scrollBarMeasureContent.width();
         var outsideWitdh = scrollBarMeasure.width();
         scrollBarMeasure.remove();
    
         return outsideWitdh - insideWidth;
     };
    
  • Here's a JSFiddle of what I decided to do instead.

     $.fn.hasScroll = function(axis){
         var overflow = this.css("overflow"),
             overflowAxis,
             bShouldScroll,
             bAllowedScroll,
             bOverrideScroll;
         
         if(typeof axis == "undefined" || axis == "y") overflowAxis = this.css("overflow-y");
         else overflowAxis = this.css("overflow-x");
         
         bShouldScroll = this.get(0).scrollHeight > this.innerHeight();
         
         bAllowedScroll = (overflow == "auto" || overflow == "visible") ||
             (overflowAxis == "auto" || overflowAxis == "visible");
         
         bOverrideScroll = overflow == "scroll" || overflowAxis == "scroll";
         
         return (bShouldScroll && bAllowedScroll) || bOverrideScroll;
     };
     
     $.fn.mousedownScroll = function(fn, data){
         var ev_mds = function(e){
             if(inScrollRange(e)) fn.call(data, e);
         }
         $(this).on("mousedown", ev_mds);
         return ev_mds;
     };
     
     $.fn.mouseupScroll = function(fn, data){
         var ev_mus = function(e){
             if(inScrollRange(e)) fn.call(data, e);
         }
         $(this).on("mouseup", ev_mus);
         return ev_mus;
     };
     
     $.fn.mousedownContent = function(fn, data){
         var ev_mdc = function(e){
             if(!inScrollRange(e)) fn.call(data, e);
         }
         
         $(this).on("mousedown", ev_mdc);
         
         return ev_mdc;
     };
     
     $.fn.mouseupContent = function(fn, data){
         var ev_muc = function(e){
             if(!inScrollRange(e)) fn.call(data, e);
         }
         $(this).on("mouseup", ev_muc);
         return ev_muc;
     };
     
     var RECT = function(){
         this.top = 0;
         this.left = 0;
         this.bottom = 0;
         this.right = 0;
     }
     
     function inRect(rect, x, y){
         return (y >= rect.top && y <= rect.bottom) &&
             (x >= rect.left && x <= rect.right)
     }
     
     var scrollSize = measureScrollWidth();
     
     function inScrollRange(event){
         var x = event.pageX,
             y = event.pageY,
             e = $(event.target),
             hasY = e.hasScroll(),
             hasX = e.hasScroll("x"),
             rX = null,
             rY = null,
             bInX = false,
             bInY = false
         
         if(hasY){ 
             rY = new RECT();
             rY.top = e.offset().top;
             rY.right = e.offset().left + e.width();
             rY.bottom = rY.top +e.height();
             rY.left = rY.right - scrollSize;
             
             //if(hasX) rY.bottom -= scrollSize;
             bInY = inRect(rY, x, y);
         }
         
         if(hasX){
             rX = new RECT();
             rX.bottom = e.offset().top + e.height();
             rX.left = e.offset().left;
             rX.top = rX.bottom - scrollSize;
             rX.right = rX.left + e.width();
             
             //if(hasY) rX.right -= scrollSize;
             bInX = inRect(rX, x, y);
         }
         
         return bInX || bInY;
     }
     
     function measureScrollWidth() {
         var scrollBarMeasure = $('<div />');
         $('body').append(scrollBarMeasure);
         scrollBarMeasure.width(50).height(50)
             .css({
                 overflow: 'scroll',
                 visibility: 'hidden',
                 position: 'absolute'
             });
     
         var scrollBarMeasureContent = $('<div />').height(1);
         scrollBarMeasure.append(scrollBarMeasureContent);
     
         var insideWidth = scrollBarMeasureContent.width();
         var outsideWitdh = scrollBarMeasure.width();
         scrollBarMeasure.remove();
     
         return outsideWitdh - insideWidth;
     };
    

Solution 9 - Events

The only solution that works for me (only tested against IE11):

$(document).mousedown(function(e){
    bScrollbarClicked = e.clientX > document.documentElement.clientWidth || e.clientY > document.documentElement.clientHeight;
});

Solution 10 - Events

I needed to detect scrollbar on mousedown but not on window but on div, and I've had element that fill the content, that I was using to detect size without scrollbar:

.element {
    position: relative;
}
.element .fill-node {
    position: absolute;
    left: 0;
    top: -100%;
    width: 100%;
    height: 100%;
    margin: 1px 0 0;
    border: none;
    opacity: 0;
    pointer-events: none;
    box-sizing: border-box;
}

the code for detect was similar to @DariuszSikorski answer but including offset and using the node that was inside scrollable:

function scrollbar_event(e, node) {
    var left = node.offset().left;
    return node.outerWidth() <= e.clientX - left;
}

var node = self.find('.fill-node');
self.on('mousedown', function(e) {
   if (!scrollbar_event(e, node)) {
      // click on content
   }
});

Solution 11 - Events

Tested and working in chrome and firefox in ubuntu 21.10.

const isScrollClick =
  e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight;

Solution 12 - Events

clickOnScrollbar = event.clientX > event.target.clientWidth || event.clientY > event.target.clientHeight;

tested on Chrome / Mac OS

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionShadowScripterView Question on Stackoverflow
Solution 1 - EventsDariusz SikorskiView Answer on Stackoverflow
Solution 2 - EventsAlexanderView Answer on Stackoverflow
Solution 3 - EventsAnttiView Answer on Stackoverflow
Solution 4 - EventsjedierikbView Answer on Stackoverflow
Solution 5 - EventsSamuel RossilleView Answer on Stackoverflow
Solution 6 - EventsAdam LeggettView Answer on Stackoverflow
Solution 7 - EventsApplehatView Answer on Stackoverflow
Solution 8 - EventsShadowScripterView Answer on Stackoverflow
Solution 9 - EventsPete AlvinView Answer on Stackoverflow
Solution 10 - EventsjcubicView Answer on Stackoverflow
Solution 11 - EventsAlbert Chiriyankandath YohannaView Answer on Stackoverflow
Solution 12 - EventsChris JungView Answer on Stackoverflow