db-lgtv-focus-engine
Version:
the Best TV focus engine
493 lines (435 loc) • 14.2 kB
JavaScript
/* smoothscroll v0.4.4 - 2019 - Dustan Kasten, Jeremias Menichelli - MIT License */
(function () {
'use strict';
// polyfill
function polyfill() {
// aliases
var w = window;
var d = document;
// return if scroll behavior is supported and polyfill is not forced
// if (
// 'scrollBehavior' in d.documentElement.style &&
// w.__forceSmoothScrollPolyfill__ !== true
// ) {
// return;
// }
// globals
var Element = w.HTMLElement || w.Element;
// object gathering original scroll methods
var original = {
scroll: w.scroll || w.scrollTo,
scrollBy: w.scrollBy,
elementScroll: Element.prototype.scroll || scrollElement,
scrollIntoView: Element.prototype.scrollIntoView
};
// define timing method
var now =
w.performance && w.performance.now ?
w.performance.now.bind(w.performance) :
Date.now;
/**
* indicates if a the current browser is made by Microsoft
* @method isMicrosoftBrowser
* @param {String} userAgent
* @returns {Boolean}
*/
function isMicrosoftBrowser(userAgent) {
var userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/'];
return new RegExp(userAgentPatterns.join('|')).test(userAgent);
}
/*
* IE has rounding bug rounding down clientHeight and clientWidth and
* rounding up scrollHeight and scrollWidth causing false positives
* on hasScrollableSpace
*/
var ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0;
/**
* changes scroll position inside an element
* @method scrollElement
* @param {Number} x
* @param {Number} y
* @returns {undefined}
*/
function scrollElement(x, y) {
this.scrollLeft = x;
this.scrollTop = y;
}
/**
* returns result of applying ease math function to a number
* @method ease
* @param {Number} k
* @returns {Number}
*/
function ease(k) {
return 0.5 * (1 - Math.cos(Math.PI * k));
}
/**
* indicates if a smooth behavior should be applied
* @method shouldBailOut
* @param {Number|Object} firstArg
* @returns {Boolean}
*/
function shouldBailOut(firstArg) {
if (
firstArg === null ||
typeof firstArg !== 'object' ||
firstArg.behavior === undefined ||
firstArg.behavior === 'auto' ||
firstArg.behavior === 'instant'
) {
// first argument is not an object/null
// or behavior is auto, instant or undefined
return true;
}
if (typeof firstArg === 'object' && firstArg.behavior === 'smooth') {
// first argument is an object and behavior is smooth
return false;
}
// throw error when behavior is not supported
throw new TypeError(
'behavior member of ScrollOptions ' +
firstArg.behavior +
' is not a valid value for enumeration ScrollBehavior.'
);
}
/**
* indicates if an element has scrollable space in the provided axis
* @method hasScrollableSpace
* @param {Node} el
* @param {String} axis
* @returns {Boolean}
*/
function hasScrollableSpace(el, axis) {
if (!el) return
if (axis === 'Y') {
return el.clientHeight + ROUNDING_TOLERANCE < el.scrollHeight;
}
if (axis === 'X') {
return el.clientWidth + ROUNDING_TOLERANCE < el.scrollWidth;
}
}
/**
* indicates if an element has a scrollable overflow property in the axis
* @method canOverflow
* @param {Node} el
* @param {String} axis
* @returns {Boolean}
*/
function canOverflow(el, axis) {
var overflowValue = w.getComputedStyle(el, null)['overflow' + axis];
return overflowValue === 'auto' || overflowValue === 'scroll';
}
/**
* indicates if an element can be scrolled in either axis
* @method isScrollable
* @param {Node} el
* @param {String} axis
* @returns {Boolean}
*/
function isScrollable(el) {
var isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y');
var isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X');
return isScrollableY || isScrollableX;
}
/**
* finds scrollable parent of an element
* @method findScrollableParent
* @param {Node} el
* @returns {Node} el
*/
function findScrollableParent(el) {
let body = findBody(el)
el = el.parentNode
while (el !== body && isScrollable(el) === false) {
el = el.parentNode || el.host;
}
if (el === body) return null
return el;
}
/**
* is in fixed
* @method isFixed
* @param {Node} el
* @returns {Node} el
*/
function isFixed(el) {
if (!el) return false
while (el !== d.body && w.getComputedStyle(el).position !== 'fixed') {
el = el.parentNode
}
if (el === d.body) return false
return true;
}
/**
* find real body
* @method findBody
* @param {Node} el
* @returns {Node} el
*/
function findBody(el) {
if (!el) return false
while (el !== d.body && w.getComputedStyle(el).position !== 'fixed') {
el = el.parentNode
}
return el;
}
/**
* self invoked function that, given a context, steps through scrolling
* @method step
* @param {Object} context
* @returns {undefined}
*/
function step(context) {
var time = now();
var value;
var currentX;
var currentY;
var elapsed = (time - context.startTime) / (window.$engine.scroll_speed || window.$engine.options.SCROLL_SPEED);
// avoid elapsed times higher than one
elapsed = elapsed > 1 ? 1 : elapsed;
// apply easing to elapsed time
value = ease(elapsed);
currentX = context.startX + (context.x - context.startX) * value;
currentY = context.startY + (context.y - context.startY) * value;
context.method.call(context.scrollable, currentX, currentY);
// scroll more if we have not reached our destination
if (currentX !== context.x || currentY !== context.y) {
window.$engine.animation_id = w.requestAnimationFrame(step.bind(w, context));
} else {
window.$engine.$scrollCallback()
}
}
/**
* scrolls window or element with a smooth behavior
* @method smoothScroll
* @param {Object|Node} el
* @param {Number} x
* @param {Number} y
* @returns {undefined}
*/
function smoothScroll(el, x, y) {
var scrollable;
var startX;
var startY;
var method;
var startTime = now();
clearTimeout(window.is_smooth_scrolling_t)
window.$engine.is_smooth_scrolling = true
window.is_smooth_scrolling_t = setTimeout(() => {
if (window.$engine.scroll_speed) window.$engine.scroll_speed = null
window.$engine.is_smooth_scrolling = false
}, window.$engine.options.SCROLL_SPEED / 2)
// define scroll context
if (el === d.body) {
scrollable = w;
startX = w.scrollX || w.pageXOffset;
startY = w.scrollY || w.pageYOffset;
method = original.scroll;
} else {
scrollable = el;
startX = el.scrollLeft;
startY = el.scrollTop;
method = scrollElement;
}
// scroll looping over a frame
step({
scrollable: scrollable,
method: method,
startTime: startTime,
startX: startX,
startY: startY,
x: x,
y: y
});
}
// ORIGINAL METHODS OVERRIDES
// w.scroll and w.scrollTo
w.scroll = w.scrollTo = function () {
// avoid action when no arguments are passed
if (arguments[0] === undefined) {
return;
}
// avoid smooth behavior if not required
if (shouldBailOut(arguments[0]) === true) {
original.scroll.call(
w,
arguments[0].left !== undefined ?
arguments[0].left :
typeof arguments[0] !== 'object' ?
arguments[0] :
w.scrollX || w.pageXOffset,
// use top prop, second argument if present or fallback to scrollY
arguments[0].top !== undefined ?
arguments[0].top :
arguments[1] !== undefined ?
arguments[1] :
w.scrollY || w.pageYOffset
);
return;
}
// LET THE SMOOTHNESS BEGIN!
smoothScroll.call(
w,
d.body,
arguments[0].left !== undefined ?
~~arguments[0].left :
w.scrollX || w.pageXOffset,
arguments[0].top !== undefined ?
~~arguments[0].top :
w.scrollY || w.pageYOffset
);
};
// w.scrollBy
w.scrollBy = function () {
// avoid action when no arguments are passed
if (arguments[0] === undefined) {
return;
}
// avoid smooth behavior if not required
if (shouldBailOut(arguments[0])) {
original.scrollBy.call(
w,
arguments[0].left !== undefined ?
arguments[0].left :
typeof arguments[0] !== 'object' ? arguments[0] : 0,
arguments[0].top !== undefined ?
arguments[0].top :
arguments[1] !== undefined ? arguments[1] : 0
);
return;
}
// LET THE SMOOTHNESS BEGIN!
smoothScroll.call(
w,
d.body,
~~arguments[0].left + (w.scrollX || w.pageXOffset),
~~arguments[0].top + (w.scrollY || w.pageYOffset)
);
};
// Element.prototype.scroll and Element.prototype.scrollTo
Element.prototype.scroll = Element.prototype.scrollTo = function () {
// avoid action when no arguments are passed
if (arguments[0] === undefined) {
return;
}
// avoid smooth behavior if not required
if (shouldBailOut(arguments[0]) === true) {
// if one number is passed, throw error to match Firefox implementation
if (typeof arguments[0] === 'number' && arguments[1] === undefined) {
throw new SyntaxError('Value could not be converted');
}
original.elementScroll.call(
this,
// use left prop, first number argument or fallback to scrollLeft
arguments[0].left !== undefined ?
~~arguments[0].left :
typeof arguments[0] !== 'object' ? ~~arguments[0] : this.scrollLeft,
// use top prop, second argument or fallback to scrollTop
arguments[0].top !== undefined ?
~~arguments[0].top :
arguments[1] !== undefined ? ~~arguments[1] : this.scrollTop
);
return;
}
var left = arguments[0].left;
var top = arguments[0].top;
// LET THE SMOOTHNESS BEGIN!
smoothScroll.call(
this,
this,
typeof left === 'undefined' ? this.scrollLeft : ~~left,
typeof top === 'undefined' ? this.scrollTop : ~~top
);
};
// Element.prototype.scrollBy
Element.prototype.scrollBy = function () {
// avoid action when no arguments are passed
if (arguments[0] === undefined) {
return;
}
// avoid smooth behavior if not required
if (shouldBailOut(arguments[0]) === true) {
original.elementScroll.call(
this,
arguments[0].left !== undefined ?
~~arguments[0].left + this.scrollLeft :
~~arguments[0] + this.scrollLeft,
arguments[0].top !== undefined ?
~~arguments[0].top + this.scrollTop :
~~arguments[1] + this.scrollTop
);
return;
}
this.scroll({
left: ~~arguments[0].left + this.scrollLeft,
top: ~~arguments[0].top + this.scrollTop,
behavior: arguments[0].behavior
});
};
// Element.prototype.scrollIntoView
Element.prototype.scrollIntoView = function (options) {
if (options.speed) {
window.$engine.scroll_speed = options.speed
}
let scrollableParent = findScrollableParent(this);
if (!scrollableParent) return
let parentRects = scrollableParent.getBoundingClientRect();
let clientRects = this.getBoundingClientRect();
let left_more = 0
let top_more = 0
switch (options.inline) {
case 'start':
left_more = 0
break
case undefined:
case 'center':
left_more = (parentRects.width - clientRects.width) / 2
break
case 'end':
left_more = parentRects.width
break
default:
let x_scroll = scrollableParent.scrollWidth - scrollableParent.clientWidth > parseInt(options.inline)
if (!x_scroll) break
left_more = parseInt(options.inline) ? Math.floor(parseInt(options.inline) / 100 * parentRects.width) : 0
delete(options.inline)
break
}
switch (options.block) {
case 'start':
top_more = 0
break
case undefined:
case 'center':
top_more = (parentRects.height - clientRects.height) / 2
break
case 'end':
top_more = parentRects.height
break
default:
let y_scroll = scrollableParent.scrollHeight - scrollableParent.clientHeight > parseInt(options.block)
if (!y_scroll) break
top_more = parseInt(options.block) ? Math.floor(parseInt(options.block) / 100 * parentRects.height) : 0
delete(options.block)
break
}
scrollableParent.scrollBy({
left: clientRects.left - parentRects.left - left_more,
top: clientRects.top - parentRects.top - top_more,
behavior: options.behavior
});
if (!isFixed(scrollableParent) || (isFixed(scrollableParent) && isFixed(findScrollableParent(scrollableParent)))) {
scrollableParent.scrollIntoView(options)
}
};
}
if (typeof exports === 'object' && typeof module !== 'undefined') {
// commonjs
module.exports = {
polyfill: polyfill
};
} else {
// global
polyfill();
}
}());