How I created the open source One Page Scroll plugin. How to find out the scroll direction of the mouse wheel

How does today's trick relate to our site? At first glance, nothing. But if on your Mac in the section Boot Camp Windows lives, then you probably already noticed that Microsoft did not plan and do not intend to change the direction of scrolling of the mouse and trackpad. Because of this, it turns out that in Mac OS X the content in the windows will follow the movement of your finger (i.e. reverse scrolling), and in Windows you will move the slider on the screen, and the content itself will move in the other direction.

In general, if you can’t switch from one approach to controlling the mouse to another, it’s time to take decisive action. There are two ways (option with uninstalling Windows and its transfer to virtual machine we will not consider):

  • disable backscrolling in Lion or Mountain Lion . This is done in the system settings, in the “Mouse” and “Trackpad” remote controls, respectively.
  • enable reverse scrolling in Windows. Oddly enough, there is a parameter for this in the system registry, however, finding it is a separate adventure

Since we are not looking for easy ways, today we will talk about the second alternative - activating reverse scrolling in Windows.

First you will need to get into, for example, this can be done through the Control Panel. In the Manager, look at the “Mice and other pointing devices” section:

Select your mouse from the list of devices double click. Go to the “Details” tab, in the drop-down list you will need the “Equipment ID” item:

Pay attention to the first line, starting with the letters VID (for example, VID_203A&PID_FFFC&REV_0100&MI_01). Remember this combination.

On your keyboard, press Command+R, type regedit in the window that opens and press Enter. The editor will open Windows registry. On the left select a branch HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\HID. You'll see a list of mice and trackpads labeled with VID, PID, and MI combinations. You will need to identify the device you are configuring here:

Inside the device you can find several more folders. In each of them you need to go inside the Device Parameters folder and change the parameter value FlipFlopWheel from 0 to 1. After rebooting, reverse scrolling will work in Windows.

  • Translation

Scrolling effects have been around on the web for a long time, and while there are already many plugins from which to choose, only a small number of them have the light weight and simplicity that many designers and developers require. Most plugins I've seen try to do too much, making them difficult to include in your projects.

Not long ago, Apple introduced the iPhone 5S, and a presentation site where the page was divided into sections, and each section described one of the features of the product. I thought this was a great way to present the product without missing key information.

I went looking for a suitable plugin, and to my surprise, I didn’t find one. This is how the page scrolling plugin was born.

Page scrolling plugin.

A jQuery-based plugin that allows you to create a layout for a page with multiple sections using minimal markup.

I'll tell you how it was created, from concept, to planning, testing and releasing free code.

Note: Before building the plugin, I was already aware of the debate regarding whether scripts should change the natural scrolling behavior of browsers - this can be confusing for users. So I tried to reduce negative effect from changing habitual behavior. In the plugin settings you can set the screen sizes at which the plugin returns to normal screen scrolling. Thus, on low-power devices such as smartphones and tablets, you can maintain site performance. In addition, you can set the duration of the animation when transitioning between sections.

What is all this for?

As I already mentioned, most ready-made plugins included many optional functions, making integration difficult. This plugin should be:

Easy to use
- easy to integrate
- require minimal markup
- perform one function, but well

1. Drawings

I started planning the plugin from general to specific. It should scroll by section. To do this, you need to disable normal scrolling in the browser, while serving sections one after another, and advancing the page if necessary.

You can imagine everything in your mind, or you can make sketches.

Divide the concept into small tasks, solving each one sequentially.

1. Prepare the layout of the sections
Let's disable normal scrolling by applying overflow: hidden to the body. We will arrange the sections in the required sequence, calculate and adapt necessary information and classes.

2. Set up a manual scroll trigger
We catch the trigger using jQuery, determine the scrolling direction, and move the layout using CSS.

3. Let's add features
Let's add responsiveness, looping, support for scrolling on touchscreens, pagination, etc.

4. Let's check in different browsers.
Let's check Chrome browsers, Safari, Firefox, Internet Explorer 10 and most popular Windows operating systems, Mac OS X, iOS and Android 4.0+.

5. Let's make the plugin available in the repository
Let's create a repository and write instructions for using the plugin

6. Let's expand support.
Let's explore other ways to increase plugin support.

2. Building the foundation

After designing the plugin, I set about building the foundation on this template:

Function($) ( var defaults = ( sectionContainer: "section", ... ); $.fn.onepage_scroll = function(options) ( var settings = $.extend((), defaults, options); ... ) )($)

We start the template with the!function($) ( ... )($) module, which places the jQuery global variable in a local scope - this will help reduce the load and prevent conflicts with other libraries.

The defaults variable contains default settings.

$.fn.onepage_scroll is the main function that initializes everything. If you are making your own plugin, do not forget to write a different name instead of onepage_scroll.

You can prevent standard scrolling by assigning it to the body tag overflow property: hidden
via a plugin-specific class name. Important to use unique names styles to avoid conflict with existing ones. I usually use an abbreviation from the name of the plugin, and then a dash separated by the name for the style, for example: .onepage-wrapper.

The foundation has been laid, let's move on to the first function.

3. Prepare the layout and arrange the sections

At first I went the wrong way. I thought I would put all the sections in order, going through them in a loop. What I got first:

Var sections = $(settings.sectionContainer); var topPos = 0; $.each(sections, function(i) ( $(this).css(( position: "absolute", top: topPos + "%" )).addClass("ops-section").attr("data-index ", i+1); topPos = topPos + 100; ));

The loop iterates through all selectors (sectionContainer is defined in the default variables section), assigns position: absolute and assigns each next section the correct top position so that they do not run over each other.

The top position is stored in topPos. We start from scratch and add with each cycle. To make each section take up the entire page, I set their height to 100% and add 100 to topPos.

Development and testing took me a couple of hours, but at the next step I realized that all this was not necessary.

4. Manual trigger and page conversion

You'd think the next step would be to move each section to a new position when the scroll trigger fires... But there's a better way. Instead of shifting each of the sections in a loop, I just put them all in one container and use the CSS3 translate3d function to shift it. This function supports percentages, we can move sections so that they are precisely positioned in the window without having to recalculate everything again. It also makes it easier to control the speed and other animation parameters.


The first solution is not always the most effective, so remember to leave time for experimentation.

Now all that remains is to determine the scrolling direction and move the container in the desired direction.

Function init_scroll(event, delta) ( var deltaOfInterest = delta, timeNow = new Date().getTime(), quietPeriod = 500; // Cancel scroll if currently animating or within quiet period if(timeNow - lastAnimation< quietPeriod + settings.animationTime) { event.preventDefault(); return; } if (deltaOfInterest < 0) { el.moveDown() } else { el.moveUp() } lastAnimation = timeNow; } $(document).bind("mousewheel DOMMouseScroll", function(event) { event.preventDefault(); var delta = event.originalEvent.wheelDelta || -event.originalEvent.detail; init_scroll(event, delta); });

First, we hook the function to the mousewheel event (DOMMouseScroll in Firefox), then we can intercept the data and determine the direction. We integrate it into the init_scroll processing, which receives wheelData for this.

In an ideal world, it would be enough to calculate the change in wheelData. However, when animating sequences, you need to build in a check to ensure that the trigger event is not duplicated (otherwise the image will overlap during animation). You can use setInterval to call each animation in turn, but this will not provide accuracy and reliability, because each browser handles it differently. For example, in Chrome and Firefox, setInterval slows down in inactive tabs, as a result, functions do not execute on time. As a result, I settled on using a function that returns the current time.

Var timeNow = new Date().getTime(), quietPeriod = 500; … if(timeNow - lastAnimation< quietPeriod + settings.animationTime) { event.preventDefault(); return; } … lastAnimation = timeNow;

In this snippet, which I quoted from the previous code, I am storing the current time in timeNow so that I can later check if the animation took more than 500ms. If it is not occupied, then the transformation does not occur and the animation does not overlap. Work with current time more reliable, because it is the same for everyone.

If(deltaOfInterest< 0) { el.moveDown() } else { el.moveUp() }

The moveUp and moveDown functions change the layout attributes to reflect current state site. Each of them at the end of the work calls the final transformation method to move the next section into the viewport.

$.fn.transformPage = function(settings, pos, index) ( … $(this).css(( "-webkit-transform": (settings.direction == "horizontal") ? "translate3d(" + pos + " %, 0, 0)" : "translate3d(0, " + pos + "%, 0)", "-webkit-transition": "all " + settings.animationTime + "ms " + settings.easing, "-moz -transform": (settings.direction == "horizontal") ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)", "-moz -transition": "all " + settings.animationTime + "ms " + settings.easing, "-ms-transform": (settings.direction == "horizontal") ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)", "-ms-transition": "all " + settings.animationTime + "ms " + settings.easing, "transform": (settings. direction == "horizontal") ? "translate3d(" + pos + "%, 0, 0)" : "translate3d(0, " + pos + "%, 0)", "transition": "all " + settings. animationTime + "ms " + settings.easing ));

This is a transformation method that shifts sections. I made them in JavaScript instead of specifying separate styles, so that developers would have the opportunity to change settings in the plugin itself (mainly animation speed and acceleration), and would not have to rummage through the styles file looking for settings. In addition, the transformation percentage still needs to be recalculated, so you can’t do without JavaScript.

5. Additional features

At first I didn't want to add anything, but I received so much feedback from the GitHub community that I decided to gradually improve the plugin. I released version 1.2.1, which adds a lot of callbacks and loops, and the hardest part is responsiveness.

I didn’t initially make the plugin with mobile platforms in mind (which I regret). Instead, we had to track and recalculate touchscreen events into a form suitable for use in init_scroll. This doesn’t work well in all browsers, so we had to build in a rollback feature where the browser returns to normal scrolling when a certain window width is reached.

Var defaults = (responsiveFallback: false ... ); function responsive() ( if ($(window).width()< settings.responsiveFallback) { $("body").addClass("disabled-onepage-scroll"); $(document).unbind("mousewheel DOMMouseScroll"); el.swipeEvents().unbind("swipeDown swipeUp"); } else { if($("body").hasClass("disabled-onepage-scroll")) { $("body").removeClass("disabled-onepage-scroll"); $("html, body, .wrapper").animate({ scrollTop: 0 }, "fast"); } el.swipeEvents().bind("swipeDown", function(event) { if (!$("body").hasClass("disabled-onepage-scroll")) event.preventDefault(); el.moveUp(); }).bind("swipeUp", function(event){ if (!$("body").hasClass("disabled-onepage-scroll")) event.preventDefault(); el.moveDown(); }); $(document).bind("mousewheel DOMMouseScroll", function(event) { event.preventDefault(); var delta = event.originalEvent.wheelDelta || -event.originalEvent.detail; init_scroll(event, delta); }); } }

Let's define a default variable. We use responsiveFallback to determine when the plugin should rollback. This code determines the width of the browser. If the width is less than the value from responsiveFallback, the function removes all events, moves the page to the beginning and allows it to scroll as usual. If the width exceeds the value, the plugin checks for the presence of the disabled-onepage-scroll class to see if it is initialized. If not, it is initialized again.

The solution is not perfect, but it gives developers and designers the opportunity to choose how their site is displayed on mobile platform, rather than rejecting these platforms entirely.

6. Testing in different browsers.

Testing is an important part of development; before releasing a plugin, you need to make sure that it works on most machines. I always develop in Chrome - firstly, I like its developer tools, and secondly, I know that if a plugin works in Chrome, it will most likely work in Safari and Opera.

I usually use Macbook Air for development, and at home I have a PC for testing. After the plugin works in Chrome, I test it manually in Safari, Opera, and finally Firefox on Mac OS X, and then Chrome, Firefox, and Internet Explorer 10 on Windows.

That's not all possible browsers, but the good thing about open source is that other developers will be able to test and fix errors themselves - that’s its point. You don’t have to make the perfect product right away, but set a springboard for the start.

Don't forget to test your plugins against mobile devices.

To make testing easier, after finishing the plugin, I create a demo page to show all its capabilities and upload it to my site. There are errors that cannot be caught locally, but which appear when working on a real site. Once the demo page is running, I start testing on mobile devices.

7. Upload the plugin to open source

The last step is to share the plugin on GitHub. To do this, you need to create an account there, configure Git and create a new repository. Then clone it to your local machine - this will create a directory with the name of the plugin. We copy the plugin there and set up the structure.

Repository structure

Customize it as you wish. I do this:

The demo directory contains working demos, with all the necessary resources
- the regular and compressed versions of the plugin are in the root
- CSS and test resources, such as images (if necessary) are in the root
- readme file at the root

Readme structure

An important stage is writing clear instructions for the open source community. I usually write them in the readme, but for complex cases a wiki page may be needed. How I write the readme:

1. Introduction
I explain the purpose of the plugin, provide an image and a link to the demo.
2. Requirements and compatibility.
It is better to move this section higher so that it is immediately clear whether a person can use the plugin.
3. Basic principles of use
Step-by-step instructions, from connecting jQuery to HTML markup and calling a function. The settings are also described.
4. Advanced use.
More complex instructions - public methods, callbacks and other useful information.
5. Other resources
Links to the tutorial, thanks, etc.

8 Expanding support

In general, it would be possible to do without jQuery, but I was in a hurry to make it open source, so I decided to reduce development time and rely on ready-made functions in jQuery.

But to clear my conscience, I reworked the plugin in pure JavaScript (a version with Zepto support is also available). With pure JS there is no need to include jQuery, everything works out of the box.

To make amends, and exclusively for Smashing Magazine’s readers, I have rebuilt One Page Scroll using pure JavaScript (a Zepto version is also available). With the pure JavaScript version, you no longer need to include jQuery. The plugin works right out of the box.

Pure JS and Zepto version

Reworking the plugin to pure JavaScript

Such processing may seem like a difficult task, but this is only at first. The hardest thing is not to make mistakes with math. Because I've already done this, it only took me a few hours to develop the helper functions to get rid of jQuery.

The plugin is based on CSS3, so we just needed to replace the jQuery calls with similar ones of our own. At the same time, I reorganized the structure of the script:

Default variable values
Everything is the same as in previous version
- initialization function
Prepares and arranges the layout and initialization of what happens when the onePageScroll function is called. This is where all the procedures that assign class names, attributes, and positioning styles sit.
- private methods
All internal methods plugin - scroll events, page transformation, adaptive rollback and scroll tracking.
- public methods
All methods for developers: moveDown(), moveUp() and moveTo()
- helper methods
Anything that overrides jQuery calls.

There were a couple of unpleasant moments - a separate function just to add or remove a style name, or the use of document.querySelector instead of $. But in the end we got a better structured plugin.

Rebuilding the plugin for Zepto

I decided to support Zepto, despite the fact that it is intended only for the most modern browsers(IE10+), because it works faster and more efficiently than jQuery 2.0+, while having a more flexible API. Zpeto is 4 times smaller than jQuery, which affects page loading speed. With people using smartphones more often, Zepto is emerging as a better alternative.

It is easier to convert a plugin from jQuery to Zepto because they have similar APIs. Almost everything is the same, except for the animation part. Because Zepto's $.fn.animate() function has CSS3 animation support and support callback animationEnd, next part:

$(this).css(( "-webkit-transform": "translate3d(0, " + pos + "%, 0)", "-webkit-transition": "-webkit-transform " + settings.animationTime + " ms " + settings.easing, "-moz-transform": "translate3d(0, " + pos + "%, 0)", "-moz-transition": "-moz-transform " + settings.animationTime + "ms " + settings.easing, "-ms-transform": "translate3d(0, " + pos + "%, 0)", "-ms-transition": "-ms-transform " + settings.animationTime + "ms " + settings.easing, "transform": "translate3d(0, " + pos + "%, 0)", "transition": "transform " + settings.animationTime + "ms " + settings.easing )); $(this).one("webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", function(e) ( if (typeof settings.afterMove == "function") settings.afterMove(index, next_el); ));

Can be replaced with this code:

$(this).animate(( translate3d: "0, " + pos + "%, 0" ), settings.animationTime, settings.easing, function() ( if (typeof settings.afterMove == "function") settings. afterMove(index, next_el )); )

Zepto allows you to make animations without defining all the styles or assigning callbacks yourself.

And why bother with this?

Because everything more people use jQuery, it becomes more and more complex and sometimes slows down. If you make support for other frameworks, your plugin will be more popular.
Redesigning from the beginning will also help you make better plugins in the future. jQuery and other libraries are forgiving minor errors, such as missing commas - as a result, you don’t really care about the quality of your work. Without these indulgences in pure JavaScript, I had a better feel for how my plugin works – what works, what affects performance, and what can be improved.

Although libraries like jQuery have made our lives easier, using them is not the best effective method achieving the goal. Some plugins can do this.

Conclusion.

Well, here is the whole process of creating the One Page Scroll plugin. There were mistakes, but I learned from them as development progressed. If I were developing it today, I would focus on mobile devices and add more comments to the code.

Without the support of communities like GitHub, StackOverflow and Smashing Magazine, I would not have been able to make the plugin so quickly. These communities helped me a lot in my work, which is why I make my plugins available for free to everyone. This is my way of paying back the wonderful support.

In this short article, I want to show a way to determine the direction of scrolling of the mouse wheel. The scope of application may not be so extensive, but I myself have come across more than once where it was necessary. For example, to create one “parallax effect”, where the page essentially did not have to scroll, and the effect occurred only based on whether the user was turning the mouse wheel and in what direction. It is possible that you will find application in your projects, if not now, then in the future.

Function addEvent(elem, type, handler)( if(elem.addEventListener)( elem.addEventListener(type, handler, false); ) else ( elem.attachEvent("on"+type, handler); ) return false; ) function scrollDirection())( var weelEvt = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel", el = document.body; addEvent(el, weelEvt, function(e)( var evt = e .originalEvent ? e.originalEvent: e, delta = evt.detail ? evt.detail*(-40) : evt.wheelDelta console.log("Scroll " + (delta > 0 ? "up" : "down")); )); ) // Hang up the document loading event handler - DOM-Ready addEvent(window, "load", scrollDirection); // For jQuery - just call the function after the DOM is loaded /*$(function())( scrollDirection(); ));*/

I don’t see the point in describing anything in particular. The only thing you can just note for yourself is that in in this case FireFox "distinguished itself" with its non-standard event "DOMMouseScroll".
For better understanding and perception, I decided to make one example, where we assume the following task: with each user scroll, he should clearly get to the next/previous section of the page, without skipping it, even if he scrolled intensively. At the same time, we will load the corresponding content into the current block. Let's build the following HTML and CSS:

* ( margin: 0; padding: 0; ) #grid li ( list-style: none; height: 300px; border-bottom: 1px dotted #333; ) #fake (height: 5000px;) /* just to there was definitely a scrollbar */

Let's move on to JS. Here our task is to cancel the usual behavior during the mouse scroll event ( essentially - prevent the user from scrolling the page manually), calculate, depending on the direction of movement of the wheel, the next or previous block, scroll animatedly to this block and load the necessary content into it.

$(function())( var flag = false, // needed to prevent actions during animation bn = 0, // index of the current block blocks = $("#grid li"), // all blocks cnt = blocks .length, // number of blocks mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; // wheel scroll event blocks.eq(0).load("loadblocks. html #b0"); // immediately load the content into the first block // function for determining the direction of scrolling the wheel function getDelta(e)( var evt = e || window.event; evt = evt.originalEvent ? evt.originalEvent: evt; return delta = evt.detail ? evt.detail*(-40) : evt.wheelDelta; ) // catch the scroll event $(document).on(mousewheelevt+".my_wheel", function(e)( e.preventDefault(); / / cancel the usual behavior (the page will not scroll) if(flag) return false; // if flag == true, then in at the moment animation occurs if(getDelta(e) > 0)( if(bn<= 0) return false; // если дошли до первого блока, то отменяем further actions--bn; // if the block is not the first, then calculate the index of the previous block) else ( if(bn >= cnt-1) return false; // if we reached the last block, then cancel further actions ++bn; // if the block is not the last, then we calculate the index of the next block) flag = true; // set a flag indicating that the animation has started $("html, body").finish().animate(( scrollTop: blocks.eq(bn).offset().top // scroll the page to the block calculated by index) , 1000, function())( blocks.eq(bn).load("loadblocks.html #b" + bn); // load content for the block flag = false; // remove the flag, indicating that the animation is complete )); )); ));

I described everything in the comments, but it remains to explain where the data is loaded from. In method load(), it is possible not only to specify the document that needs to be loaded, but also to determine which element we will need. In the example, I used a regular html document with several elements ( according to the number of our blocks) have IDs from "b0" to "b6". Having received the index of the current block, we load the element with the corresponding id.

Try, experiment, and most importantly remember that some things that you may not need today can be very helpful tomorrow. And if you have any unusual questions or tasks, write them in this topic or in the section "