From c0783dbe8e60085b5937a646d3fbefdaae0598f4 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 19 Oct 2019 23:36:59 +0300 Subject: [PATCH 01/12] Add scroll manager with base functionality --- src/components/focusManager.js | 4 +- .../polyfills/focusPreventScroll.js | 39 ++ src/components/scrollManager.js | 338 ++++++++++++++++++ src/scripts/site.js | 3 + 4 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 src/components/polyfills/focusPreventScroll.js create mode 100644 src/components/scrollManager.js diff --git a/src/components/focusManager.js b/src/components/focusManager.js index 8c2f0ad442..aaa7dda30e 100644 --- a/src/components/focusManager.js +++ b/src/components/focusManager.js @@ -1,4 +1,4 @@ -define(['dom'], function (dom) { +define(['dom', 'scrollManager'], function (dom, scrollManager) { 'use strict'; var scopes = []; @@ -40,7 +40,7 @@ define(['dom'], function (dom) { try { element.focus({ - preventScroll: false + preventScroll: scrollManager.isEnabled() }); } catch (err) { console.log('Error in focusManager.autoFocus: ' + err); diff --git a/src/components/polyfills/focusPreventScroll.js b/src/components/polyfills/focusPreventScroll.js new file mode 100644 index 0000000000..0fff0e96e2 --- /dev/null +++ b/src/components/polyfills/focusPreventScroll.js @@ -0,0 +1,39 @@ +// Polyfill to add support for preventScroll by focus function + +if (HTMLElement.prototype.nativeFocus === undefined) { + (function () { + var supportsPreventScrollOption = false; + try { + var focusElem = document.createElement("div"); + + focusElem.addEventListener("focus", function(event) { + event.preventDefault(); + event.stopPropagation(); + }, true); + + var opts = Object.defineProperty({}, "preventScroll", { + get: function () { + supportsPreventScrollOption = true; + } + }); + + focusElem.focus(opts); + } catch(e) {} + + if (!supportsPreventScrollOption) { + HTMLElement.prototype.nativeFocus = HTMLElement.prototype.focus; + + HTMLElement.prototype.focus = function(options) { + var scrollX = window.scrollX; + var scrollY = window.scrollY; + + this.nativeFocus(); + + // Restore window scroll if preventScroll + if (options && options.preventScroll) { + window.scroll(scrollX, scrollY); + } + }; + } + })(); +} diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js new file mode 100644 index 0000000000..06c3f8ef1d --- /dev/null +++ b/src/components/scrollManager.js @@ -0,0 +1,338 @@ +define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManager) { + "use strict"; + + // FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers + var _minimumScrollY = 0; + /** + * Returns minimum vertical scroll. + * Scroll less than that value will be zeroed. + * + * @return {number} minimum vertical scroll + */ + function minimumScrollY() { + if (_minimumScrollY === 0) { + var topMenu = document.querySelector(".headerTop"); + if (topMenu) { + _minimumScrollY = topMenu.clientHeight; + } + } + return _minimumScrollY; + } + + var supportsSmoothScroll = "scrollBehavior" in document.documentElement.style; + + var supportsScrollToOptions = false; + try { + var elem = document.createElement("div"); + + var opts = Object.defineProperty({}, "behavior", { + get: function () { + supportsScrollToOptions = true; + } + }); + + elem.scrollTo(opts); + } catch(e) {} + + /** + * Returns value clamped by range [min, max]. + * + * @param {number} value clamped value + * @param {number} min begining of range + * @param {number} max ending of range + * @return {number} clamped value + */ + function clamp(value, min, max) { + return value <= min ? min : value >= max ? max : value; + } + + /** + * Returns the required delta to fit range 1 into range 2. + * In case of range 1 is bigger than range 2 returns delta to fit most out of range part. + * + * @param {number} begin1 begining of range 1 + * @param {number} end1 ending of range 1 + * @param {number} begin2 begining of range 2 + * @param {number} end2 ending of range 2 + * @return {number} delta: <0 move range1 to the left, >0 - to the right + */ + function fitRange(begin1, end1, begin2, end2) { + var delta1 = begin1 - begin2; + var delta2 = end2 - end1; + if (delta1 < 0 && delta1 < delta2) { + return -delta1; + } else if (delta2 < 0) { + return delta2; + } + return 0; + }; + + /** + * Returns parent element that can be scrolled. If no such, returns documentElement. + * + * @param {HTMLElement} element element for which parent is being searched + * @param {boolean} vertical search for vertical scrollable parent + */ + function getScrollableParent(element, vertical) { + if (element) { + var parent = element.parentElement; + + while (parent) { + if ((!vertical && parent.scrollWidth > parent.clientWidth && parent.classList.contains("scrollX")) || + (vertical && parent.scrollHeight > parent.clientHeight) && parent.classList.contains("scrollY")) { + return parent; + } + + parent = parent.parentElement; + } + } + + return document.scrollingElement || document.documentElement; + } + + /** + * @typedef {Object} ScrollerData + * @property {number} scrollPos current scroll position + * @property {number} scrollSize scroll size + * @property {number} clientSize client size + */ + + /** + * Returns scroll data for specified orientation. + * + * @param {HTMLElement} scroller scroller + * @param {boolean} vertical vertical scroll data + * @return {ScrollerData} scroll data + */ + function getScrollerData(scroller, vertical) { + var data = {}; + + if (!vertical) { + data.scrollPos = scroller.scrollLeft; + data.scrollSize = scroller.scrollWidth; + data.clientSize = scroller.clientWidth; + } else { + data.scrollPos = scroller.scrollTop; + data.scrollSize = scroller.scrollHeight; + data.clientSize = scroller.clientHeight; + } + + return data; + } + + /** + * Returns position of child of scroller for specified orientation. + * + * @param {HTMLElement} scroller scroller + * @param {HTMLElement} element child of scroller + * @param {boolean} vertical vertical scroll + * @return {number} child position + */ + function getScrollerChildPos(scroller, element, vertical) { + var elementRect = element.getBoundingClientRect(); + var scrollerRect = scroller.getBoundingClientRect(); + + var scrollerLeft = scrollerRect.left; + var scrollerTop = scrollerRect.top; + + // documentElement scrolls itself - coordinates is changed relative to viewport + if (scroller === getScrollableParent(null, false)) { + scrollerLeft += scroller.scrollLeft; + scrollerTop += scroller.scrollTop; + } + + if (!vertical) { + return scroller.scrollLeft + elementRect.left - scrollerLeft; + } else { + return scroller.scrollTop + elementRect.top - scrollerTop; + } + } + + /** + * Returns scroll position for element. + * + * @param {ScrollerData} scrollerData scroller data + * @param {number} elementPos child element position + * @param {number} elementSize child element size + * @param {boolean} centered scroll to center + * @return {number} scroll position + */ + function calcScroll(scrollerData, elementPos, elementSize, centered) { + var maxScroll = scrollerData.scrollSize - scrollerData.clientSize; + + var scroll; + + if (centered) { + scroll = elementPos + (elementSize - scrollerData.clientSize) / 2; + } else { + var delta = fitRange(elementPos, elementPos + elementSize - 1, scrollerData.scrollPos, scrollerData.scrollPos + scrollerData.clientSize - 1); + scroll = scrollerData.scrollPos - delta; + } + + return clamp(Math.round(scroll), 0, maxScroll); + } + + /** + * Calls scrollTo function in proper way. + * + * @param {HTMLElement} scroller scroller + * @param {ScrollToOptions} options scroll options + */ + function scrollToHelper(scroller, options) { + if ("scrollTo" in scroller) { + if (!supportsScrollToOptions) { + var scrollX = (options.left !== undefined ? options.left : scroller.scrollLeft); + var scrollY = (options.top !== undefined ? options.top : scroller.scrollTop); + scroller.scrollTo(scrollX, scrollY); + } else { + scroller.scrollTo(options); + } + } else if ("scrollLeft" in scroller) { + if (options.left !== undefined) { + scroller.scrollLeft = options.left; + } + if (options.top !== undefined) { + scroller.scrollTop = options.top; + } + } + } + + /** + * Performs scroll. + * + * @param {HTMLElement} xScroller horizontal scroller + * @param {number} scrollX horizontal coordinate + * @param {HTMLElement} yScroller vertical scroller + * @param {number} scrollY vertical coordinate + * @param {boolean} smooth smooth scrolling + */ + function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) { + + var scrollBehavior = smooth ? "smooth" : "instant"; + + if (xScroller !== yScroller) { + scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); + scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); + } else { + scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); + } + } + + /** + * Returns true if smooth scroll must be used. + */ + function useSmoothScroll() { + + if (browser.tizen) { + return true; + } + + return false; + }; + + /** + * Returns true if scroll manager is enabled. + */ + var isEnabled = function() { + + if (!layoutManager.tv) { + return false; + } + + if (browser.tizen) { + return true; + } + + if (browser.web0s) { + return true; + } + + return false; + }; + + /** + * Scrolls the document to a given position or element. + * + * @param {Object} options scroll options + * @param {number} [options.x] horizontal coordinate + * @param {number} [options.y] vertical coordinate + * @param {HTMLElement} [options.element] target element of scroll task + * @param {boolean} [options.smooth=false] smooth scrolling + */ + var scrollTo = function(options) { + + var element = options.element; + var smooth = !!options.smooth; + + var xScroller; + var yScroller; + var scrollX; + var scrollY; + + // Scroller is document itself by default + xScroller = yScroller = getScrollableParent(null, false); + + if (options.element !== undefined) { + var scrollCenterX = true; + var scrollCenterY = true; + + var offsetParent = element.offsetParent; + + var isFixed = offsetParent && !offsetParent.offsetParent; + + // Scroll fixed elements to nearest edge (or do not scroll at all) + if (isFixed) { + scrollCenterX = scrollCenterY = false; + } + + xScroller = getScrollableParent(element, false); + + var elementRect = element.getBoundingClientRect(); + + var xScrollerData = getScrollerData(xScroller, false); + var yScrollerData = getScrollerData(yScroller, true); + + var xPos = getScrollerChildPos(xScroller, element, false); + var yPos = getScrollerChildPos(yScroller, element, true); + + scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX); + scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY); + + // HACK: Scroll to top for top menu because it is hidden + // FIXME: Need a marker to scroll top/bottom + if (isFixed && elementRect.bottom < 0) { + scrollY = 0; + } + + // HACK: Ensure we are at the top + // FIXME: Need a marker to scroll top/bottom + if (scrollY < minimumScrollY()) { + scrollY = 0; + } + } else { + scrollX = (options.x !== undefined ? Math.round(options.x) : xScroller.scrollLeft); + scrollY = (options.y !== undefined ? Math.round(options.y) : yScroller.scrollTop); + + var xScrollerData = getScrollerData(xScroller, false); + var yScrollerData = getScrollerData(yScroller, true); + + scrollX = clamp(scrollX, 0, xScrollerData.scrollSize - xScrollerData.clientSize); + scrollY = clamp(scrollY, 0, yScrollerData.scrollSize - yScrollerData.clientSize); + } + + doScroll(xScroller, scrollX, yScroller, scrollY, smooth); + } + + if (isEnabled()) { + dom.addEventListener(window, "focusin", function(e) { + setTimeout(function() { + scrollTo({element: e.target, smooth: useSmoothScroll()}); + }, 0); + }, {capture: true}); + } + + return { + isEnabled: isEnabled, + scrollTo: scrollTo + }; +}); diff --git a/src/scripts/site.js b/src/scripts/site.js index e05712c076..d0dd0eaae3 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -477,6 +477,7 @@ var AppInfo = {}; require(["keyboardnavigation"], function(keyboardnavigation) { keyboardnavigation.enable(); }); + require(["focusPreventScroll"]); }); }); } @@ -839,6 +840,7 @@ var AppInfo = {}; }); define("slideshow", [componentsPath + "/slideshow/slideshow"], returnFirstDependency); define("objectassign", [componentsPath + "/polyfills/objectassign"], returnFirstDependency); + define("focusPreventScroll", [componentsPath + "/polyfills/focusPreventScroll"], returnFirstDependency); define("userdataButtons", [componentsPath + "/userdatabuttons/userdatabuttons"], returnFirstDependency); define("emby-playstatebutton", [componentsPath + "/userdatabuttons/emby-playstatebutton"], returnFirstDependency); define("emby-ratingbutton", [componentsPath + "/userdatabuttons/emby-ratingbutton"], returnFirstDependency); @@ -863,6 +865,7 @@ var AppInfo = {}; define("serverNotifications", [componentsPath + "/serverNotifications/serverNotifications"], returnFirstDependency); define("skinManager", [componentsPath + "/skinManager"], returnFirstDependency); define("keyboardnavigation", [componentsPath + "/keyboardnavigation"], returnFirstDependency); + define("scrollManager", [componentsPath + "/scrollManager"], returnFirstDependency); define("connectionManager", [], function () { return ConnectionManager; }); From cbd64f6b4ea11a88d7a72580c4108c3a8ae5fc90 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sun, 20 Oct 2019 14:53:12 +0300 Subject: [PATCH 02/12] Add animated smooth scrolling --- src/components/scrollManager.js | 63 ++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 06c3f8ef1d..7b5fd77c07 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -1,6 +1,8 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManager) { "use strict"; + const ScrollTime = 200; + // FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers var _minimumScrollY = 0; /** @@ -197,6 +199,16 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage } } + var scrollTimer; + + /** + * Resets scroll timer to stop scrolling. + */ + function resetScrollTimer() { + cancelAnimationFrame(scrollTimer); + scrollTimer = undefined; + } + /** * Performs scroll. * @@ -208,13 +220,45 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage */ function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) { - var scrollBehavior = smooth ? "smooth" : "instant"; + resetScrollTimer(); - if (xScroller !== yScroller) { - scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); - scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); + if (smooth && useAnimatedScroll()) { + var start; + + function scrollAnim(currentTimestamp) { + start = start || currentTimestamp; + + var dx = scrollX - xScroller.scrollLeft; + var dy = scrollY - yScroller.scrollTop; + + if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) { + resetScrollTimer(); + xScroller.scrollLeft = scrollX; + yScroller.scrollTop = scrollY; + return; + } + + var k = Math.min(1, (currentTimestamp - start) / ScrollTime); + + dx = Math.round(dx*k); + dy = Math.round(dy*k); + + xScroller.scrollLeft += dx; + yScroller.scrollTop += dy; + + scrollTimer = requestAnimationFrame(scrollAnim); + }; + + scrollTimer = requestAnimationFrame(scrollAnim); } else { - scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); + var scrollBehavior = smooth ? "smooth" : "instant"; + + if (xScroller !== yScroller) { + scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); + scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); + } else { + scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); + } } } @@ -230,6 +274,15 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage return false; }; + /** + * Returns true if animated implementation of smooth scroll must be used. + */ + function useAnimatedScroll() { + // Add block to force using (or not) of animated implementation + + return !supportsSmoothScroll; + }; + /** * Returns true if scroll manager is enabled. */ From c0fbce32ce3b22eb24944883c6fc681cb2f92072 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Tue, 22 Oct 2019 22:19:43 +0300 Subject: [PATCH 03/12] Fix compatibility with older browsers (webOS 2/3) --- src/components/scrollManager.js | 68 +++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 7b5fd77c07..9d296b3f96 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -1,7 +1,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManager) { "use strict"; - const ScrollTime = 200; + var ScrollTime = 200; // FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers var _minimumScrollY = 0; @@ -209,6 +209,44 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage scrollTimer = undefined; } + /** + * Performs animated scroll. + * + * @param {HTMLElement} xScroller horizontal scroller + * @param {number} scrollX horizontal coordinate + * @param {HTMLElement} yScroller vertical scroller + * @param {number} scrollY vertical coordinate + */ + function animateScroll(xScroller, scrollX, yScroller, scrollY) { + var start; + + function scrollAnim(currentTimestamp) { + start = start || currentTimestamp; + + var dx = scrollX - xScroller.scrollLeft; + var dy = scrollY - yScroller.scrollTop; + + if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) { + resetScrollTimer(); + xScroller.scrollLeft = scrollX; + yScroller.scrollTop = scrollY; + return; + } + + var k = Math.min(1, (currentTimestamp - start) / ScrollTime); + + dx = Math.round(dx*k); + dy = Math.round(dy*k); + + xScroller.scrollLeft += dx; + yScroller.scrollTop += dy; + + scrollTimer = requestAnimationFrame(scrollAnim); + }; + + scrollTimer = requestAnimationFrame(scrollAnim); + } + /** * Performs scroll. * @@ -223,33 +261,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage resetScrollTimer(); if (smooth && useAnimatedScroll()) { - var start; - - function scrollAnim(currentTimestamp) { - start = start || currentTimestamp; - - var dx = scrollX - xScroller.scrollLeft; - var dy = scrollY - yScroller.scrollTop; - - if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) { - resetScrollTimer(); - xScroller.scrollLeft = scrollX; - yScroller.scrollTop = scrollY; - return; - } - - var k = Math.min(1, (currentTimestamp - start) / ScrollTime); - - dx = Math.round(dx*k); - dy = Math.round(dy*k); - - xScroller.scrollLeft += dx; - yScroller.scrollTop += dy; - - scrollTimer = requestAnimationFrame(scrollAnim); - }; - - scrollTimer = requestAnimationFrame(scrollAnim); + animateScroll(xScroller, scrollX, yScroller, scrollY); } else { var scrollBehavior = smooth ? "smooth" : "instant"; From fe87abc5a8e3bdbe8b5e1ad2de0b6ed5f99b4825 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Tue, 22 Oct 2019 23:46:21 +0300 Subject: [PATCH 04/12] Split 'scrollTo' function --- src/components/scrollManager.js | 104 ++++++++++++++++---------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 9d296b3f96..1aa2b7d62d 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -316,73 +316,74 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage }; /** - * Scrolls the document to a given position or element. + * Scrolls the document to a given position. * - * @param {Object} options scroll options - * @param {number} [options.x] horizontal coordinate - * @param {number} [options.y] vertical coordinate - * @param {HTMLElement} [options.element] target element of scroll task - * @param {boolean} [options.smooth=false] smooth scrolling + * @param {number} scrollX horizontal coordinate + * @param {number} scrollY vertical coordinate + * @param {boolean} [smooth=false] smooth scrolling */ - var scrollTo = function(options) { + var scrollTo = function(scrollX, scrollY, smooth) { - var element = options.element; - var smooth = !!options.smooth; - - var xScroller; - var yScroller; - var scrollX; - var scrollY; + smooth = !!smooth; // Scroller is document itself by default - xScroller = yScroller = getScrollableParent(null, false); + var scroller = getScrollableParent(null, false); - if (options.element !== undefined) { - var scrollCenterX = true; - var scrollCenterY = true; + var xScrollerData = getScrollerData(scroller, false); + var yScrollerData = getScrollerData(scroller, true); - var offsetParent = element.offsetParent; + scrollX = clamp(Math.round(scrollX), 0, xScrollerData.scrollSize - xScrollerData.clientSize); + scrollY = clamp(Math.round(scrollY), 0, yScrollerData.scrollSize - yScrollerData.clientSize); - var isFixed = offsetParent && !offsetParent.offsetParent; + doScroll(scroller, scrollX, scroller, scrollY, smooth); + } - // Scroll fixed elements to nearest edge (or do not scroll at all) - if (isFixed) { - scrollCenterX = scrollCenterY = false; - } + /** + * Scrolls the document to a given element. + * + * @param {HTMLElement} element target element of scroll task + * @param {boolean} [smooth=false] smooth scrolling + */ + var scrollToElement = function(element, smooth) { - xScroller = getScrollableParent(element, false); + smooth = !!smooth; - var elementRect = element.getBoundingClientRect(); + var scrollCenterX = true; + var scrollCenterY = true; - var xScrollerData = getScrollerData(xScroller, false); - var yScrollerData = getScrollerData(yScroller, true); + var offsetParent = element.offsetParent; - var xPos = getScrollerChildPos(xScroller, element, false); - var yPos = getScrollerChildPos(yScroller, element, true); + var isFixed = offsetParent && !offsetParent.offsetParent; - scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX); - scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY); + // Scroll fixed elements to nearest edge (or do not scroll at all) + if (isFixed) { + scrollCenterX = scrollCenterY = false; + } - // HACK: Scroll to top for top menu because it is hidden - // FIXME: Need a marker to scroll top/bottom - if (isFixed && elementRect.bottom < 0) { - scrollY = 0; - } + var xScroller = getScrollableParent(element, false); + var yScroller = getScrollableParent(element, true); - // HACK: Ensure we are at the top - // FIXME: Need a marker to scroll top/bottom - if (scrollY < minimumScrollY()) { - scrollY = 0; - } - } else { - scrollX = (options.x !== undefined ? Math.round(options.x) : xScroller.scrollLeft); - scrollY = (options.y !== undefined ? Math.round(options.y) : yScroller.scrollTop); + var elementRect = element.getBoundingClientRect(); - var xScrollerData = getScrollerData(xScroller, false); - var yScrollerData = getScrollerData(yScroller, true); + var xScrollerData = getScrollerData(xScroller, false); + var yScrollerData = getScrollerData(yScroller, true); - scrollX = clamp(scrollX, 0, xScrollerData.scrollSize - xScrollerData.clientSize); - scrollY = clamp(scrollY, 0, yScrollerData.scrollSize - yScrollerData.clientSize); + var xPos = getScrollerChildPos(xScroller, element, false); + var yPos = getScrollerChildPos(yScroller, element, true); + + var scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX); + var scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY); + + // HACK: Scroll to top for top menu because it is hidden + // FIXME: Need a marker to scroll top/bottom + if (isFixed && elementRect.bottom < 0) { + scrollY = 0; + } + + // HACK: Ensure we are at the top + // FIXME: Need a marker to scroll top/bottom + if (scrollY < minimumScrollY()) { + scrollY = 0; } doScroll(xScroller, scrollX, yScroller, scrollY, smooth); @@ -391,13 +392,14 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage if (isEnabled()) { dom.addEventListener(window, "focusin", function(e) { setTimeout(function() { - scrollTo({element: e.target, smooth: useSmoothScroll()}); + scrollToElement(e.target, useSmoothScroll()); }, 0); }, {capture: true}); } return { isEnabled: isEnabled, - scrollTo: scrollTo + scrollTo: scrollTo, + scrollToElement: scrollToElement }; }); From 0502e984ad5b7103acb439969d6b1a501b845898 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 23 Oct 2019 22:35:45 +0300 Subject: [PATCH 05/12] Fix animated scroll of "smooth scrolled" elements in browsers that support smooth scroll (Chrome/Firefox) --- src/components/scrollManager.js | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 1aa2b7d62d..5f446a0620 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -199,6 +199,26 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage } } + /** + * Performs built-in scroll. + * + * @param {HTMLElement} xScroller horizontal scroller + * @param {number} scrollX horizontal coordinate + * @param {HTMLElement} yScroller vertical scroller + * @param {number} scrollY vertical coordinate + * @param {boolean} smooth smooth scrolling + */ + function builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth) { + var scrollBehavior = smooth ? "smooth" : "instant"; + + if (xScroller !== yScroller) { + scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); + scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); + } else { + scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); + } + } + var scrollTimer; /** @@ -238,8 +258,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage dx = Math.round(dx*k); dy = Math.round(dy*k); - xScroller.scrollLeft += dx; - yScroller.scrollTop += dy; + builtinScroll(xScroller, xScroller.scrollLeft + dx, yScroller, yScroller.scrollTop + dy, false); scrollTimer = requestAnimationFrame(scrollAnim); }; @@ -263,14 +282,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage if (smooth && useAnimatedScroll()) { animateScroll(xScroller, scrollX, yScroller, scrollY); } else { - var scrollBehavior = smooth ? "smooth" : "instant"; - - if (xScroller !== yScroller) { - scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); - scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); - } else { - scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); - } + builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth); } } From 90f565166ea7f752a9d16ae328d59b19beccba7e Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 23 Oct 2019 22:38:01 +0300 Subject: [PATCH 06/12] Fix "fixed element" condition --- src/components/scrollManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 5f446a0620..3d033cb63e 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -365,7 +365,8 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage var offsetParent = element.offsetParent; - var isFixed = offsetParent && !offsetParent.offsetParent; + // In Firefox offsetParent.offsetParent is BODY + var isFixed = offsetParent && (!offsetParent.offsetParent || window.getComputedStyle(offsetParent).position === "fixed"); // Scroll fixed elements to nearest edge (or do not scroll at all) if (isFixed) { From e86058dc8c2f39e59d7cb73009c50f29aaa3314f Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Thu, 24 Oct 2019 21:28:14 +0300 Subject: [PATCH 07/12] Add document scroll wrapper to unify scrolling and to support for webOS 2/3 (browser only) --- src/components/scrollManager.js | 79 ++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 3d033cb63e..93b21e0c44 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -69,6 +69,70 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage return 0; }; + /** + * Document scroll wrapper helps to unify scrolling and fix issues of some browsers. + * + * webOS 2 Browser: scrolls documentElement (and window), but body has a scroll size + * + * webOS 3 Browser: scrolls body (and window) + * + * webOS 4 Native: scrolls body (and window); has a document.scrollingElement + * + * Tizen 4 Browser/Native: scrolls body (and window); has a document.scrollingElement + * + * Tizen 5 Browser/Native: scrolls documentElement (and window); has a document.scrollingElement + */ + function DocumentScroller() { + } + + DocumentScroller.prototype = { + get scrollLeft() { + return window.pageXOffset; + }, + set scrollLeft(val) { + window.scroll(val, window.pageYOffset); + }, + + get scrollTop() { + return window.pageYOffset; + }, + set scrollTop(val) { + window.scroll(window.pageXOffset, val); + }, + + get scrollWidth() { + return Math.max(document.documentElement.scrollWidth, document.body.scrollWidth); + }, + + get scrollHeight() { + return Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); + }, + + get clientWidth() { + return Math.min(document.documentElement.clientWidth, document.body.clientWidth); + }, + + get clientHeight() { + return Math.min(document.documentElement.clientHeight, document.body.clientHeight); + }, + + getBoundingClientRect: function() { + // Make valid viewport coordinates: documentElement.getBoundingClientRect returns rect of entire document relative to viewport + return { + left: 0, + top: 0, + width: this.clientWidth, + height: this.clientHeight + }; + }, + + scrollTo: function() { + window.scrollTo.apply(window, arguments); + } + }; + + var documentScroller = new DocumentScroller(); + /** * Returns parent element that can be scrolled. If no such, returns documentElement. * @@ -89,7 +153,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage } } - return document.scrollingElement || document.documentElement; + return documentScroller; } /** @@ -134,19 +198,10 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage var elementRect = element.getBoundingClientRect(); var scrollerRect = scroller.getBoundingClientRect(); - var scrollerLeft = scrollerRect.left; - var scrollerTop = scrollerRect.top; - - // documentElement scrolls itself - coordinates is changed relative to viewport - if (scroller === getScrollableParent(null, false)) { - scrollerLeft += scroller.scrollLeft; - scrollerTop += scroller.scrollTop; - } - if (!vertical) { - return scroller.scrollLeft + elementRect.left - scrollerLeft; + return scroller.scrollLeft + elementRect.left - scrollerRect.left; } else { - return scroller.scrollTop + elementRect.top - scrollerTop; + return scroller.scrollTop + elementRect.top - scrollerRect.top; } } From b3df632164c3c100014e5e8c345c63fa26fbb991 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sun, 10 Nov 2019 23:36:04 +0300 Subject: [PATCH 08/12] Add support for easing animated scroll. --- src/components/scrollManager.js | 44 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 93b21e0c44..cefe3903d8 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -1,7 +1,15 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManager) { "use strict"; - var ScrollTime = 200; + /** + * Scroll time in ms. + */ + var ScrollTime = 270; + + /** + * Epsilon for comparing values. + */ + var Epsilon = 1e-6; // FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers var _minimumScrollY = 0; @@ -69,6 +77,16 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage return 0; }; + /** + * Ease value. + * + * @param {number} t value in range [0, 1] + * @return {number} eased value in range [0, 1] + */ + function ease(t) { + return t*(2 - t); // easeOutQuad === ease-out + } + /** * Document scroll wrapper helps to unify scrolling and fix issues of some browsers. * @@ -293,27 +311,37 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage * @param {number} scrollY vertical coordinate */ function animateScroll(xScroller, scrollX, yScroller, scrollY) { + + var ox = xScroller.scrollLeft; + var oy = yScroller.scrollTop; + var dx = scrollX - ox; + var dy = scrollY - oy; + + if (Math.abs(dx) < Epsilon && Math.abs(dy) < Epsilon) { + return; + } + var start; function scrollAnim(currentTimestamp) { + start = start || currentTimestamp; - var dx = scrollX - xScroller.scrollLeft; - var dy = scrollY - yScroller.scrollTop; + var k = Math.min(1, (currentTimestamp - start) / ScrollTime); - if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) { + if (k === 1) { resetScrollTimer(); xScroller.scrollLeft = scrollX; yScroller.scrollTop = scrollY; return; } - var k = Math.min(1, (currentTimestamp - start) / ScrollTime); + k = ease(k); - dx = Math.round(dx*k); - dy = Math.round(dy*k); + var x = ox + dx*k; + var y = oy + dy*k; - builtinScroll(xScroller, xScroller.scrollLeft + dx, yScroller, yScroller.scrollTop + dy, false); + builtinScroll(xScroller, x, yScroller, y, false); scrollTimer = requestAnimationFrame(scrollAnim); }; From a37e0fb47a4539114952f0bdf5bf2973d24625a5 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Thu, 21 Nov 2019 21:48:35 +0300 Subject: [PATCH 09/12] Fix minimumScrollY hack --- src/components/scrollManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index cefe3903d8..7697132d66 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -478,7 +478,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage // HACK: Ensure we are at the top // FIXME: Need a marker to scroll top/bottom - if (scrollY < minimumScrollY()) { + if (scrollY < minimumScrollY() && yScroller === documentScroller) { scrollY = 0; } From 1d045d172b145e2c6548cbb1ad6e347d10283a3f Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:31:18 +0300 Subject: [PATCH 10/12] Update src/components/scrollManager.js Co-Authored-By: Vasily --- src/components/scrollManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 7697132d66..9f637baa6f 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -163,7 +163,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage while (parent) { if ((!vertical && parent.scrollWidth > parent.clientWidth && parent.classList.contains("scrollX")) || - (vertical && parent.scrollHeight > parent.clientHeight) && parent.classList.contains("scrollY")) { + (vertical && parent.scrollHeight > parent.clientHeight && parent.classList.contains("scrollY"))) { return parent; } From 3752db775b9c29ef7327cf94a3f9d288bc1fdfca Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 27 Nov 2019 14:13:16 +0300 Subject: [PATCH 11/12] Fix style --- src/components/polyfills/focusPreventScroll.js | 4 +++- src/components/scrollManager.js | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/polyfills/focusPreventScroll.js b/src/components/polyfills/focusPreventScroll.js index 0fff0e96e2..6511c0426c 100644 --- a/src/components/polyfills/focusPreventScroll.js +++ b/src/components/polyfills/focusPreventScroll.js @@ -18,7 +18,9 @@ if (HTMLElement.prototype.nativeFocus === undefined) { }); focusElem.focus(opts); - } catch(e) {} + } catch (e) { + console.log("error checking preventScroll support"); + } if (!supportsPreventScrollOption) { HTMLElement.prototype.nativeFocus = HTMLElement.prototype.focus; diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 9f637baa6f..6385549653 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -42,7 +42,9 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage }); elem.scrollTo(opts); - } catch(e) {} + } catch (e) { + console.log("error checking ScrollToOptions support"); + } /** * Returns value clamped by range [min, max]. @@ -75,7 +77,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage return delta2; } return 0; - }; + } /** * Ease value. @@ -344,7 +346,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage builtinScroll(xScroller, x, yScroller, y, false); scrollTimer = requestAnimationFrame(scrollAnim); - }; + } scrollTimer = requestAnimationFrame(scrollAnim); } @@ -379,7 +381,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage } return false; - }; + } /** * Returns true if animated implementation of smooth scroll must be used. @@ -388,7 +390,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage // Add block to force using (or not) of animated implementation return !supportsSmoothScroll; - }; + } /** * Returns true if scroll manager is enabled. From c677708819324c15c9a9512376ee813e0d21e059 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 27 Nov 2019 14:30:56 +0300 Subject: [PATCH 12/12] Apply suggestions from code review --- src/components/scrollManager.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index 6385549653..9f7035d528 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -12,7 +12,6 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage var Epsilon = 1e-6; // FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers - var _minimumScrollY = 0; /** * Returns minimum vertical scroll. * Scroll less than that value will be zeroed. @@ -20,13 +19,11 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage * @return {number} minimum vertical scroll */ function minimumScrollY() { - if (_minimumScrollY === 0) { - var topMenu = document.querySelector(".headerTop"); - if (topMenu) { - _minimumScrollY = topMenu.clientHeight; - } + var topMenu = document.querySelector(".headerTop"); + if (topMenu) { + return topMenu.clientHeight; } - return _minimumScrollY; + return 0; } var supportsSmoothScroll = "scrollBehavior" in document.documentElement.style; @@ -333,8 +330,7 @@ define(["dom", "browser", "layoutManager"], function (dom, browser, layoutManage if (k === 1) { resetScrollTimer(); - xScroller.scrollLeft = scrollX; - yScroller.scrollTop = scrollY; + builtinScroll(xScroller, scrollX, yScroller, scrollY, false); return; }