Want to catch - Control Key + Scroll Wheel event


#1

Hi,
I'm trying to catch a Control Key + Scroll Wheel event, in the same manner that you would zoom a page. I was working from this example here, MouseEvent.ctrlKey

This is an attempt to catch a "Zoom Text Only" event in Firefox. I have no interest in the text-size, just trying to catch the event itself.

Very few FF users would actually would actually go through the menu and click Zoom In or Zoom Out. Most will use the Ctrl Key + Scroll shortcut or The keyboard shortcuts shown in the image above. ( Ctrl++ or Ctrl+-) I would actually like to catch those too but I'll take things one step at a time for now.

Using the .resize event to catch a change in the window size also includes Page Zooming for all browsers. I will be using that too, but hoping it doesn't conflict with an EventListener set up for "Zoom Text Only".

Here's my JS

function sizeIt(){
  const el = document.querySelector(".get-ht");
  const faux = document.querySelector(".faux");
  const elHt = el.offsetHeight;
    faux.style.height = elHt + "px";
    //console.log("sizeIt has run");
}
sizeIt();

// Would like to catch 'Zoom Text Only' in FF with Control Key + Scroll Wheel
window.addEventListener("wheel", function(event){
  if(event.ctrlKey){
    sizeIt();
    console.log("ctrl key down on wheel event");
  }
});

//Run again on page zoom and window resize
window.addEventListener("resize", sizeIt);

You'll see I'm using the .wheel event along with event.ctrlKey. It works, but it's kinda choppy and causes the sizeIt() function to lag behind. If you run the test page below in Firefox and select Zoom Text Only you will see what I mean by "lags behind". The green box on the left is always about 20px off.

When I use .keydown it works smoothly but fires continuously while the Ctrl key is down.

window.addEventListener("keydown", function(event){
  if(event.ctrlKey){
    sizeIt();
    console.log("ctrl key down on wheel event");
  }
});

In that situation the sizeIt() function works properly and it behaves the same way as my .resize EventListener, which takes care of standard Page Zooming.

Does anyone have any ideas on how I can make sizeIt() run smoothly on a Ctrl Key + Scroll Event ?

Here is the complete test page. It looks like I'm trying to do equal height columns but the green box is not a sibling, so CSS can't match the heights. This is a test case from a much more involved page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Match height of non-sibling</title>
<style>
h1 {
  font-size:1.3em;
  text-align:center;
}
.wrap {
  width: 80%;
  max-width: 600px;
  margin: 0 auto;
  position: relative;
}
.get-ht, .faux {
  width: 75%;
  margin-left:auto;
  border: 1px solid;
  padding: 4px;
  background: yellow;
  box-sizing: border-box;
}
.faux {
  position: absolute;
  top:0; left:0;
  width: calc(25% - 4px);
  height: auto;
  background: palegreen;
}
p {margin: 1em}

</style>
</head>
<body>
<h1>Match height of non-sibling</h1>

<div class="wrap">
  <div class="inner">
    <div class="get-ht">
      <p>Set this div's height on the faux div, which is not a sibling.
      Give this div's height to the green box to the left.</p>
      <p>Set this div's height on the faux div, which is not a sibling.
      Give this div's height to the green box to the left.</p>
    </div>
  </div>
  <div class="faux">faux div</div>
</div>

<script>

function sizeIt(){
  const el = document.querySelector(".get-ht");
  const faux = document.querySelector(".faux");
  const elHt = el.offsetHeight;
    faux.style.height = elHt + "px";
    //console.log("sizeIt has run");
}
sizeIt();

// Would like to catch 'Zoom Text Only' in FF with Control Key + Scroll Wheel
window.addEventListener("wheel", function(event){
  if(event.ctrlKey){
    sizeIt();
    console.log("ctrl key down on wheel event");
  }
});

//Run again on page zoom and window resize
window.addEventListener("resize", sizeIt);

</script>
</body>
</html>

#2

Hi @Ray.H,
I think sizeIt could benefit from some optimization since that function is going to run a lot of times per second. If while at that you're always firing document.querySelector that means a lot of unnecessarily repeated document searching per second. The best would be, IMHO, to define el and faux outside of sizeIt and that way sizeIt will be a lot less resource intensive and probably won't lag behind.

var el = document.querySelector(".get-ht");
var faux = document.querySelector(".faux");
function sizeIt(){
    const elHt = el.offsetHeight;
    faux.style.height = elHt + "px";
    //console.log("sizeIt has run");
}

Hope it helps,

Andres


#3

Hi Andres,
Thanks for taking a look at it. What you suggested makes sense, and I can see how that should make it "less resource intensive".

I applied those changes and unfortunately it didn't improve the lagging issue.

Here are a couple of screenshots to help explain things better.

This first SS is in Firefox with Zoom Text Only, it shows the lagging I was describing. You can see how the green faux div is lagging behind, on both zoom in/out

Now this next SS is using Page Zoom and .resize with the same amount of zooming, plus some window dragging to change the wrapper width. No lagging behind in this case.


#4

And here's that smoother behavior with .keydown, instead of .wheel, while using Zoom Text Only.

I'm not sure if I was using .wheel correctly, or if a delayed response is what to expect from it.


#5

Hi Ray, interesting situation and seems a little tricky, my guess is that on wheel sizeIt gets called a little before the new zoom value is applied, therefore it applies the previous value...
This might seem a little hacky but it doesn't hurt to try and see what happens when you do the following (maybe play a little with the timeout value):

window.addEventListener("wheel", function(event){
	if(event.ctrlKey){
		setTimeout(function(){
			sizeIt();			
		},5);
		console.log("ctrl key down on wheel event");
	}
});

Hope it helps


#6

I believe that's what was happening.

Using setTimeout resolved the issue, thanks for your help. :slight_smile:

Using .wheel + .ctrlKey runs the function only when the wheel is scrolled, much better than running constantly with keydown.


#7

My pleasure, just out of curiosity, if you set the timeout to 0, does it still work?


#8

Yes, it still works with 0


#9

That's good. This is because although the value is 0 and would make you think it would execute immediately, it actually takes it out of the current execution stack and puts it at the end of the queue... this is a useful trick in JS that comes in handy quite often even if somewhat hacky...


#10

I'm actually having trouble to reproduce the problem at all, but what you might also try is to request the height update to the next animation frame (i.e. when the browser is going to repaint the page anyway); this way it should be in sync with the repaints caused by the changed zoom level.

Anyway the cleanest solution would of course be CSS... sure you can't adjust the layout in a way so that you can work with height: 100%? It would work with your test page at any rate...


#11

Hi megapop,

Yes your right, I could have used height:100% in that test case.
But as I mentioned in my first post, it was simulated version of some much more complicated html. Actually it was part of an HTML table that had some trickery involved in it and it required the use of a div located outside of the table. That's the reason there was no sibling bond.

This is something I turned it into JS learning exercise for myself, it's really a spin-off from this recent thread in the CSS forum, Table cell margins?

To summarize that thread, the OP was looking for a way to extend the border of the <thead> element across a margin on the left side of the scrolling table. We had exhausted all possible CSS solutions and each one had it's own shortcomings.

I've updated my CodePen demo from that thread, with the JS from this thread.

I saw this as an opportunity to further my learning with JS. I learn better by writing code rather than just reading about it. That way I retain what I learn.

So in this exercise I learned how to do the following.

  • catch a "Control Key + Scroll Wheel" event
  • use setTimeout to prevent premature firing of a function
  • turn a querySelectorAll nodelist into an array
  • make use of an => arrow function expression

Here's the example I was working with in this thread merged back into an html table.
table-header-divs.html (2.8 KB)

You would need to be using Firefox and go into "Zoom Text Only" mode as shown in the 1st screenshot of post #1.

Thanks for pitching in, now I need to go learn about animation frame, :slight_smile:


#12

Ah I see -- no further explanation required! :-D And sure your test setup is simplified... sorry for mentioning it.

Boy I really haven't read your OP carefully. ^^ But yes, requestAnimationFrame also does the trick then:

window.addEventListener('wheel', function (event) {
  if (event.ctrlKey) {
    window.requestAnimationFrame(sizeIt)
  }
})

#13

Yes, that works well. It also eliminates the nested function that was required when using setTimeout.

Another question, what would be the simplest way to convert a nodelist into an array with ES6.

The following is working, but I was wondering if it could be trimmed down with any modern methods.

var faux = document.querySelectorAll(".faux"); //querySelectorAll creates a nodelist

// var i; [converts nodelist to an array, pre ES6]

function sizeIt(){
  const elHt = el.offsetHeight;
  var i;
  for (i = 0; i < faux.length; i++) {
    faux[i].style.height = elHt + "px";
  }
}

#14

function sizeIt(){
  const elHt = el.offsetHeight;
  for (let element of faux) {
    element.style.height = elHt + "px";
  }
}

Also, now that ES6 is here, almost nobody uses var anymore. Either use const for things that never change, or let for things do change, like variables in a loop.


#15

You could have written setTimeout() the same way; both take a callback function as an argument, which might also be an anonymous function expression. But if that function does nothing but call another function (with no or the same arguments), then you can just as well pass in that function directly.

Either using Array.from()

const fauxArray = Array.from(faux)

or the spread syntax:

const fauxArray = [...faux]

But in your case you want to use .forEach(), which is actually implemented by the node list as well... so you don't need to convert it first:

faux.forEach(function (current) {
  current.style.height = elHt + 'px'
})

Edit (x-post): yes as @ScallioXTX says, if you're using ES6 anyway don't use var... just changed that in the above.


#16

Ah I missed that faux is a node list. I'm pretty sure the for ... of loop I suggested doesn't work on those.


#17

Yes, that looks much more efficient than the for loop I was using.
I found the before and after examples here, Converting a for loop to forEach.

Actually it did work. If you notice in the MDN link you posted it mentioned this..

(including the built-in String, Array, e.g. the Array-like arguments or NodeList objects, TypedArray, Map and Set, and user-defined iterables),

Thanks for everyone's help. Since I'm just now starting to learn JS I'm trying not to learn old techniques that have been improved with modern methods. This was a good example of such a case.


#18

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.