// ==UserScript==
// @name Scroll like Opera
// @version 2.1.1
// @description Based on Linkify Plus. Turn plain text URLs into links.
// @license MIT
// @author eight04 <eight04@gmail.com>
// @homepageURL https://github.com/eight04/scroll-like-opera
// @supportURL https://github.com/eight04/scroll-like-opera/issues
// @namespace eight04.blogspot.com
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addValueChangeListener
// @compatible firefox Tampermonkey latest
// @compatible chrome Tampermonkey latest
// @require https://greasyfork.org/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=29833
// @require https://greasyfork.org/scripts/7108-bezier-easing/code/bezier-easing.js?version=29098
// @include *
// ==/UserScript==
/* eslint-env browser, greasemonkey */
/* global GM_config BezierEasing */
var config;
GM_config.init(
"Scroll like Opera",
{
useWhenOnScrollbar: {
label: "Scroll horizontally if cursor hover on horizontal scrollbar.",
type: "checkbox",
default: true
},
useWhenOneScrollbar: {
label: "Scroll horizontally if there is only horizontal scrollbar presented.",
type: "checkbox",
default: true
},
useAlways: {
label: "Always use script's scrolling handler. Enable this if you want to use the script's smooth scrolling on chrome.",
type: "checkbox",
default: false
},
scrollDelay: {
label: "Smooth scrolling delay.",
type: "text",
default: 400
},
scrollOffset: {
label: "Scrolling offset.",
type: "text",
default: 120
},
continueScrollingTimeout: {
label: "Scrolled target changing delay.",
type: "number",
default: 400
}
}
);
config = GM_config.get();
GM_registerMenuCommand("Scroll like Opera - Configure", function(){
GM_config.open();
});
GM_config.onclose = function(){
config = GM_config.get();
};
/**
Cache current scrolling target
*/
var cache = {
timeout: null,
target: null,
cache: function(element) {
cache.target = element;
cache.delay();
},
reset: function() {
clearTimeout(cache.timeout);
cache.timeout = null;
cache.target = null;
},
delay: function() {
clearTimeout(cache.timeout);
cache.timeout = setTimeout(cache.reset, config.continueScrollingTimeout);
}
};
/**
Register event
*/
window.addEventListener("wheel", function(e){
var q;
if (cache.target) {
q = getScrollInfo(cache.target, e, true);
// Scrolled to edge
if (!q) {
e.preventDefault();
return;
}
cache.delay();
} else {
q = getScrollInfo(e.target, e);
// Can't find scrollable element
if (!q) {
return;
}
cache.cache(q.element);
}
if (q.use || config.useAlways) {
e.preventDefault();
scrollElement(q.element, q.offsetX, q.offsetY);
}
}, {passive: false});
window.addEventListener("mousemove", function(){
cache.reset();
});
/**
Main logic
*/
function getInfo(element, e) {
var rect, css;
if (element == document.documentElement) {
return {
element: element,
onScrollbarX: e.clientY >= element.clientHeight && e.clientY <= window.innerHeight,
scrollableX: element.scrollWidth > element.clientWidth,
scrollableY: element.scrollHeight > element.clientHeight
};
} else if (element == document.body) {
return {
element: element,
onScrollbarX: false,
scrollableX: false,
scrollableY: false
};
} else {
rect = element.getBoundingClientRect();
css = getCss(element);
return {
element: element,
onScrollbarX: element.clientHeight && e.clientY >= rect.top + css.borderTop + element.clientHeight && e.clientY <= rect.bottom - css.borderBottom,
scrollableX: element.clientWidth && element.scrollWidth > element.clientWidth && css.overflowX != "visible" && css.overflowX != "hidden",
scrollableY: element.clientHeight && element.scrollHeight > element.clientHeight && css.overflowY != "visible" && css.overflowY != "hidden"
};
}
}
function getScrollInfo(element, e, noParent) {
var q;
// Get scrollable parent
while (element) {
q = getInfo(element, e);
if (e.deltaY && (q.onScrollbarX || useHorizontalScroll(q)) && scrollable(element, e.deltaY, 0)) {
// Horizontal scroll
q.offsetX = getOffset(e.deltaY);
q.offsetY = 0;
q.use = true;
return q;
}
if ((e.deltaX && q.scrollableX || e.deltaY && q.scrollableY) && scrollable(element, e.deltaX, e.deltaY)) {
q.offsetX = getOffset(e.deltaX);
q.offsetY = getOffset(e.deltaY);
return q;
}
if (noParent) {
return null;
}
element = element.parentNode;
if (element == document) {
return null;
}
}
return null;
}
/**
Scroll function. Should I put them into seperate library?
Thanks to https://github.com/galambalazs/smoothscroll
*/
var animate = null;
var que = [];
function scrollElement(element, x, y) {
var elapsed = config.scrollDelay;
que.push({
offsetX: x,
offsetY: y,
lastX: 0,
lastY: 0,
element: element,
timeStart: null
});
if (animate != null) {
return;
}
function animation(timestamp){
var i, j, len, q, time, offsetX, offsetY, process, swap;
swap = [];
for (i = 0, j = 0, len = que.length; i < len; i++) {
q = que[i];
if (q.timeStart == null) {
q.timeStart = timestamp;
}
if (timestamp - q.timeStart >= elapsed || !scrollable(q.element, q.offsetX, q.offsetY)) {
scrollBy(q.element, q.offsetX - q.lastX, q.offsetY - q.lastY);
} else {
time = (timestamp - q.timeStart) / elapsed;
process = ease(time);
offsetX = Math.floor(q.offsetX * process);
offsetY = Math.floor(q.offsetY * process);
scrollBy(q.element, offsetX - q.lastX, offsetY - q.lastY);
q.lastX = offsetX;
q.lastY = offsetY;
swap[j++] = q;
}
}
que = swap;
if (!que.length) {
animate = null;
return;
}
animate = requestAnimationFrame(animation);
}
animate = requestAnimationFrame(animation);
function scrollBy(element, x, y) {
if (element != document.documentElement) {
element.scrollLeft += x;
element.scrollTop += y;
} else {
window.scrollBy(x, y);
}
}
}
/**
Helpers
*/
function useHorizontalScroll(q) {
return config.useWhenOneScrollbar && q.scrollableX && !q.scrollableY;
}
function getOffset(delta) {
var direction = 0;
if (delta > 0) {
direction = 1;
} else if (delta < 0) {
direction = -1;
}
return direction * config.scrollOffset;
}
function ease(t){
return BezierEasing.css.ease(t);
}
function getCss(element){
var css = getComputedStyle(element);
return {
borderTop: parseInt(css.getPropertyValue("border-top-width"), 10),
borderRight: parseInt(css.getPropertyValue("border-right-width"), 10),
borderBottom: parseInt(css.getPropertyValue("border-bottom-width"), 10),
borderLeft: parseInt(css.getPropertyValue("border-left-width"), 10),
overflowX: css.getPropertyValue("overflow-x"),
overflowY: css.getPropertyValue("overflow-y")
};
}
function scrollable(element, offsetX, offsetY) {
var top, left;
if (element == document.documentElement) {
top = window.scrollY;
left = window.scrollX;
} else {
top = element.scrollTop;
left = element.scrollLeft;
}
if (top == 0 && offsetY < 0) {
return false;
}
if (left == 0 && offsetX < 0) {
return false;
}
if (top + element.clientHeight >= element.scrollHeight && offsetY > 0) {
return false;
}
if (left + element.clientWidth >= element.scrollWidth && offsetX > 0) {
return false;
}
return true;
}