gecko/mobile/chrome/content/browser.js

841 lines
26 KiB
JavaScript
Raw Normal View History

let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm")
// TODO: Take into account ppi in these units?
// The ratio of velocity that is retained every ms.
const kPanDeceleration = 0.999;
// The number of ms to consider events over for a swipe gesture.
const kSwipeLength = 500;
// The number of pixels to move before we consider a drag to be more than
// just a click with jitter.
const kDragThreshold = 10;
// The number of pixels to move to break out of axis-lock
const kLockBreakThreshold = 100;
// Minimum speed to move during kinetic panning. 0.015 pixels/ms is roughly
// equivalent to a pixel every 4 frames at 60fps.
const kMinKineticSpeed = 0.015;
// Maximum kinetic panning speed. 9 pixels/ms is equivalent to 150 pixels per
// frame at 60fps.
const kMaxKineticSpeed = 9;
// The maximum magnitude of disparity allowed between axes acceleration. If
// it's larger than this, lock the slow-moving axis.
const kAxisLockRatio = 5;
function dump(a) {
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
}
2011-10-13 13:47:29 -07:00
function sendMessageToJava(aMessage) {
let bridge = Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge);
return bridge.handleGeckoMessage(JSON.stringify(aMessage));
2009-07-17 16:17:57 -07:00
}
function resolveGeckoURI(aURI) {
if (aURI.indexOf("chrome://") == 0) {
let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
} else if (aURI.indexOf("resource://") == 0) {
let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
return handler.resolveURI(Services.io.newURI(aURI, null, null));
}
return aURI;
}
var BrowserApp = {
_tabs: [],
_selectedTab: null,
deck: null,
startup: function startup() {
window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
this.deck = document.getElementById("browsers");
BrowserEventHandler.init();
Services.obs.addObserver(this, "Tab:Add", false);
Services.obs.addObserver(this, "Tab:Load", false);
Services.obs.addObserver(this, "Tab:Select", false);
Services.obs.addObserver(this, "Tab:Close", false);
2011-10-13 11:13:41 -07:00
Services.obs.addObserver(this, "session-back", false);
Services.obs.addObserver(this, "session-reload", false);
let uri = "about:support";
if ("arguments" in window && window.arguments[0])
uri = window.arguments[0];
// XXX maybe we don't do this if the launch was kicked off from external
Services.io.offline = false;
let newTab = this.addTab(uri);
newTab.active = true;
2011-10-13 11:13:41 -07:00
},
shutdown: function shutdown() {
2011-10-13 11:13:41 -07:00
},
get tabs() {
return this._tabs;
},
get selectedTab() {
return this._selectedTab;
},
set selectedTab(aTab) {
this._selectedTab = aTab;
if (!aTab)
return;
this.deck.selectedPanel = aTab.browser;
},
2009-08-05 14:06:38 -07:00
get selectedBrowser() {
if (this._selectedTab)
return this._selectedTab.browser;
return null;
},
getTabForId: function getTabForId(aId) {
let tabs = this._tabs;
for (let i=0; i < tabs.length; i++) {
if (tabs[i].id == aId)
return tabs[i];
}
return null;
},
getTabForBrowser: function getTabForBrowser(aBrowser) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser == aBrowser)
return tabs[i];
}
return null;
},
getBrowserForWindow: function getBrowserForWindow(aWindow) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser.contentWindow == aWindow)
return tabs[i].browser;
}
return null;
},
getBrowserForDocument: function getBrowserForDocument(aDocument) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser.contentDocument == aDocument)
return tabs[i].browser;
}
return null;
},
loadURI: function loadURI(aURI, aParams) {
let browser = this.selectedBrowser;
if (!browser)
return;
let flags = aParams.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
let charset = "charset" in aParams ? aParams.charset : null;
browser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
},
addTab: function addTab(aURI) {
let newTab = new Tab(aURI);
this._tabs.push(newTab);
return newTab;
},
closeTab: function closeTab(aTab) {
if (aTab == this.selectedTab)
this.selectedTab = null;
aTab.destroy();
this._tabs.splice(this._tabs.indexOf(aTab), 1);
},
selectTab: function selectTab(aTab) {
if (aTab != null) {
this.selectedTab = aTab;
aTab.active = true;
let message = {
gecko: {
type: "Tab:Selected",
tabID: aTab.id
}
};
sendMessageToJava(message);
2011-10-14 23:22:25 -07:00
}
},
observe: function(aSubject, aTopic, aData) {
let browser = this.selectedBrowser;
if (!browser)
return;
if (aTopic == "session-back")
browser.goBack();
else if (aTopic == "session-reload")
browser.reload();
else if (aTopic == "Tab:Add") {
let newTab = this.addTab(aData);
newTab.active = true;
} else if (aTopic == "Tab:Load")
browser.loadURI(aData);
else if (aTopic == "Tab:Select")
this.selectTab(this.getTabForId(parseInt(aData)));
else if (aTopic == "Tab:Close")
this.closeTab(this.getTabForId(parseInt(aData)));
}
}
function nsBrowserAccess() {
}
nsBrowserAccess.prototype = {
openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
dump("nsBrowserAccess::openURI");
let browser = BrowserApp.selectedBrowser;
if (!browser)
browser = BrowserApp.addTab("about:blank").browser;
// Why does returning the browser.contentWindow not work here?
Services.io.offline = false;
browser.loadURI(aURI.spec, null, null);
return null;
},
openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
dump("nsBrowserAccess::openURIInFrame");
return null;
},
isTabContentWindow: function(aWindow) {
return BrowserApp.getBrowserForWindow(aWindow) != null;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow])
};
2011-10-14 23:22:25 -07:00
let gTabIDFactory = 0;
function Tab(aURL) {
this.browser = null;
2011-10-14 23:22:25 -07:00
this.id = 0;
this.create(aURL);
}
Tab.prototype = {
create: function(aURL) {
if (this.browser)
return;
this.browser = document.createElement("browser");
this.browser.setAttribute("type", "content");
BrowserApp.deck.appendChild(this.browser);
this.browser.stop();
let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
Ci.nsIWebProgress.NOTIFY_LOCATION |
Ci.nsIWebProgress.NOTIFY_PROGRESS;
this.browser.addProgressListener(this, flags);
this.browser.loadURI(aURL);
2011-10-14 23:22:25 -07:00
this.id = ++gTabIDFactory;
let message = {
gecko: {
type: "Tab:Added",
2011-10-14 23:22:25 -07:00
tabID: this.id,
uri: aURL
}
};
sendMessageToJava(message);
},
destroy: function() {
if (!this.browser)
return;
this.browser.removeProgressListener(this);
BrowserApp.deck.removeChild(this.browser);
this.browser = null;
let message = {
gecko: {
type: "Tab:Closed",
tabID: this.id
}
};
sendMessageToJava(message);
},
set active(aActive) {
if (!this.browser)
return;
if (aActive) {
this.browser.setAttribute("type", "content-primary");
this.browser.focus();
BrowserApp.selectedTab = this;
} else {
this.browser.setAttribute("type", "content");
}
},
get active() {
if (!this.browser)
return false;
return this.browser.getAttribute("type") == "content-primary";
},
onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
let browser = BrowserApp.getBrowserForWindow(aWebProgress.DOMWindow);
let windowID = aWebProgress.DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
let uri = "";
if (browser)
uri = browser.currentURI.spec;
let message = {
gecko: {
type: "onStateChange",
2011-10-14 23:22:25 -07:00
tabID: this.id,
windowID: windowID,
uri: uri,
state: aStateFlags
}
};
2011-10-13 13:47:29 -07:00
sendMessageToJava(message);
},
onLocationChange: function(aWebProgress, aRequest, aLocationURI) {
let browser = BrowserApp.getBrowserForWindow(aWebProgress.DOMWindow);
let uri = browser.currentURI.spec;
let windowID = aWebProgress.DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
let message = {
gecko: {
type: "onLocationChange",
2011-10-14 23:22:25 -07:00
tabID: this.id,
windowID: windowID,
uri: uri
}
};
sendMessageToJava(message);
},
onSecurityChange: function(aBrowser, aWebProgress, aRequest, aState) {
dump("progressListener.onSecurityChange");
},
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
let windowID = aWebProgress.DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
let message = {
gecko: {
type: "onProgressChange",
2011-10-14 23:22:25 -07:00
tabID: this.id,
windowID: windowID,
current: aCurTotalProgress,
total: aMaxTotalProgress
}
};
2011-10-13 13:47:29 -07:00
sendMessageToJava(message);
},
onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
//dump("progressListener.onStatusChange");
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
};
var BrowserEventHandler = {
init: function init() {
window.addEventListener("click", this, true);
window.addEventListener("mousedown", this, true);
window.addEventListener("mouseup", this, true);
window.addEventListener("mousemove", this, true);
BrowserApp.deck.addEventListener("MozMagnifyGestureStart", this, true);
BrowserApp.deck.addEventListener("MozMagnifyGestureUpdate", this, true);
BrowserApp.deck.addEventListener("DOMContentLoaded", this, true);
BrowserApp.deck.addEventListener("DOMLinkAdded", this, true);
BrowserApp.deck.addEventListener("DOMTitleChanged", this, true);
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "DOMContentLoaded": {
let browser = BrowserApp.getBrowserForDocument(aEvent.target);
browser.focus();
let tabID = BrowserApp.getTabForBrowser(browser).id;
2011-10-13 13:47:29 -07:00
sendMessageToJava({
gecko: {
type: "DOMContentLoaded",
tabID: tabID,
windowID: 0,
uri: browser.currentURI.spec,
title: browser.contentTitle
}
});
break;
}
case "DOMLinkAdded": {
let target = aEvent.originalTarget;
if (!target.href || target.disabled)
return;
let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);
let tabID = BrowserApp.getTabForBrowser(browser).id;
let json = {
type: "DOMLinkAdded",
tabID: tabID,
windowId: target.ownerDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID,
href: resolveGeckoURI(target.href),
charset: target.ownerDocument.characterSet,
title: target.title,
rel: target.rel
};
// rel=icon can also have a sizes attribute
if (target.hasAttribute("sizes"))
json.sizes = target.getAttribute("sizes");
sendMessageToJava({ gecko: json });
break;
}
case "DOMTitleChanged": {
let browser = BrowserApp.getBrowserForDocument(aEvent.target);
let tabID = BrowserApp.getTabForBrowser(browser).id;
sendMessageToJava({
gecko: {
type: "DOMTitleChanged",
tabID: tabID,
title: aEvent.target.title
}
});
break;
}
case "click":
if (this.blockClick) {
aEvent.stopPropagation();
aEvent.preventDefault();
}
break;
case "mousedown":
this.startX = aEvent.clientX;
this.startY = aEvent.clientY;
this.blockClick = false;
this.firstMovement = true;
this.edx = 0;
this.edy = 0;
this.lockXaxis = false;
this.lockYaxis = false;
this.motionBuffer = [];
this._updateLastPosition(aEvent.clientX, aEvent.clientY, 0, 0);
this.panElement = this._findScrollableElement(aEvent.originalTarget,
true);
if (this.panElement)
this.panning = true;
break;
case "mousemove":
aEvent.stopPropagation();
aEvent.preventDefault();
if (!this.panning)
break;
this.edx += aEvent.clientX - this.lastX;
this.edy += aEvent.clientY - this.lastY;
// If this is the first panning motion, check if we can
// move in the given direction. If we can't, find an
// ancestor that can.
// We do this per-axis, as often the very first movement doesn't
// reflect the direction of movement for both axes.
if (this.firstMovement &&
(Math.abs(this.edx) > kDragThreshold ||
Math.abs(this.edy) > kDragThreshold)) {
this.firstMovement = false;
let originalElement = this.panElement;
// Decide if we want to lock an axis while scrolling
if (Math.abs(this.edx) > Math.abs(this.edy) * kAxisLockRatio)
this.lockYaxis = true;
else if (Math.abs(this.edy) > Math.abs(this.edx) * kAxisLockRatio)
this.lockXaxis = true;
// See if we've reached the extents of this element and if so,
// find an element above it that can be scrolled.
while (this.panElement &&
!this._elementCanScroll(this.panElement,
-this.edx,
-this.edy)) {
this.panElement =
this._findScrollableElement(this.panElement, false);
}
// If there are no scrollable elements above the element whose
// extents we've reached, let that element be scrolled.
if (!this.panElement)
this.panElement = originalElement;
}
// Only scroll once we've moved past the drag threshold
if (!this.firstMovement) {
this._scrollElementBy(this.panElement,
this.lockXaxis ? 0 : -this.edx,
this.lockYaxis ? 0 : -this.edy);
// Note, it's important that this happens after the scrollElementBy,
// as this will modify the clientX/clientY to be relative to the
// correct element.
this._updateLastPosition(aEvent.clientX, aEvent.clientY,
this.lockXaxis ? 0 : this.edx,
this.lockYaxis ? 0 : this.edy);
// Allow breaking out of axis lock if we move past a certain threshold
if (this.lockXaxis) {
if (Math.abs(this.edx) > kLockBreakThreshold)
this.lockXaxis = false;
} else {
this.edx = 0;
}
if (this.lockYaxis) {
if (Math.abs(this.edy) > kLockBreakThreshold)
this.lockYaxis = false;
} else {
this.edy = 0;
}
}
break;
case "mouseup":
if (!this.panning)
break;
this.panning = false;
if (Math.abs(aEvent.clientX - this.startX) > kDragThreshold ||
Math.abs(aEvent.clientY - this.startY) > kDragThreshold) {
this.blockClick = true;
aEvent.stopPropagation();
aEvent.preventDefault();
// Calculate a regression line for the last few motion events in
// the same direction to estimate the velocity. This ought to do a
// reasonable job of accounting for jitter/bad events.
this.panLastTime = Date.now();
// Variables required for calculating regression on each axis
// 'p' will be sum of positions
// 't' will be sum of times
// 'tp' will be sum of times * positions
// 'tt' will be sum of time^2's
// 'n' is the number of data-points
let xSums = { p: 0, t: 0, tp: 0, tt: 0, n: 0 };
let ySums = { p: 0, t: 0, tp: 0, tt: 0, n: 0 };
let lastDx = 0;
let lastDy = 0;
// Variables to find the absolute x,y (relative to the first event)
let edx = 0; // Sum of x changes
let edy = 0; // Sum of y changes
// For convenience
let mb = this.motionBuffer;
// First collect the variables necessary to calculate the line
for (let i = 0; i < mb.length; i++) {
// Sum up total movement so far
let dx = edx + mb[i].dx;
let dy = edy + mb[i].dy;
edx += mb[i].dx;
edy += mb[i].dy;
// Don't consider events before direction changes
if ((xSums.n > 0) &&
((mb[i].dx < 0 && lastDx > 0) ||
(mb[i].dx > 0 && lastDx < 0))) {
xSums = { p: 0, t: 0, tp: 0, tt: 0, n: 0 };
}
if ((ySums.n > 0) &&
((mb[i].dy < 0 && lastDy > 0) ||
(mb[i].dy > 0 && lastDy < 0))) {
ySums = { p: 0, t: 0, tp: 0, tt: 0, n: 0 };
}
if (mb[i].dx != 0)
lastDx = mb[i].dx;
if (mb[i].dy != 0)
lastDy = mb[i].dy;
// Only consider events that happened in the last kSwipeLength ms
let timeDelta = this.panLastTime - mb[i].time;
if (timeDelta > kSwipeLength)
continue;
xSums.p += dx;
xSums.t += timeDelta;
xSums.tp += timeDelta * dx;
xSums.tt += timeDelta * timeDelta;
xSums.n ++;
ySums.p += dy;
ySums.t += timeDelta;
ySums.tp += timeDelta * dy;
ySums.tt += timeDelta * timeDelta;
ySums.n ++;
}
// If we don't have enough usable motion events, bail out
if (xSums.n < 2 && ySums.n < 2)
break;
// Calculate the slope of the regression line.
// The intercept of the regression line is commented for reference.
let sx = 0;
if (xSums.n > 1)
sx = ((xSums.n * xSums.tp) - (xSums.t * xSums.p)) /
((xSums.n * xSums.tt) - (xSums.t * xSums.t));
//let ix = (xSums.p - (sx * xSums.t)) / xSums.n;
let sy = 0;
if (ySums.n > 1)
sy = ((ySums.n * ySums.tp) - (ySums.t * ySums.p)) /
((ySums.n * ySums.tt) - (ySums.t * ySums.t));
//let iy = (ySums.p - (sy * ySums.t)) / ySums.n;
// The slope of the regression line is the projected acceleration
this.panX = -sx;
this.panY = -sy;
if (Math.abs(this.panX) > kMaxKineticSpeed)
this.panX = (this.panX > 0) ? kMaxKineticSpeed : -kMaxKineticSpeed;
if (Math.abs(this.panY) > kMaxKineticSpeed)
this.panY = (this.panY > 0) ? kMaxKineticSpeed : -kMaxKineticSpeed;
// If we have (near) zero acceleration, bail out.
if (Math.abs(this.panX) < kMinKineticSpeed &&
Math.abs(this.panY) < kMinKineticSpeed)
break;
// Check if this element can scroll - if not, let's bail out.
if (!this.panElement || !this._elementCanScroll(
this.panElement, -this.panX, -this.panY))
break;
// Fire off the first kinetic panning event
this._scrollElementBy(this.panElement, -this.panX, -this.panY);
// TODO: Use the kinetic scrolling prefs
this.panAccumulatedDeltaX = 0;
this.panAccumulatedDeltaY = 0;
let self = this;
let panElement = this.panElement;
let callback = {
onBeforePaint: function kineticPanCallback(timeStamp) {
if (self.panning || self.panElement != panElement)
return;
let timeDelta = timeStamp - self.panLastTime;
self.panLastTime = timeStamp;
// Adjust deceleration
self.panX *= Math.pow(kPanDeceleration, timeDelta);
self.panY *= Math.pow(kPanDeceleration, timeDelta);
// Calculate panning motion
let dx = self.panX * timeDelta;
let dy = self.panY * timeDelta;
// We only want to set integer scroll coordinates
self.panAccumulatedDeltaX += dx - Math.floor(dx);
self.panAccumulatedDeltaY += dy - Math.floor(dy);
dx = Math.floor(dx);
dy = Math.floor(dy);
if (Math.abs(self.panAccumulatedDeltaX) >= 1.0) {
let adx = Math.floor(self.panAccumulatedDeltaX);
dx += adx;
self.panAccumulatedDeltaX -= adx;
}
if (Math.abs(self.panAccumulatedDeltaY) >= 1.0) {
let ady = Math.floor(self.panAccumulatedDeltaY);
dy += ady;
self.panAccumulatedDeltaY -= ady;
}
self._scrollElementBy(panElement, -dx, -dy);
if (Math.abs(self.panX) >= kMinKineticSpeed ||
Math.abs(self.panY) >= kMinKineticSpeed)
window.mozRequestAnimationFrame(this);
}
};
// If one axis is moving a lot slower than the other, lock it.
if (Math.abs(this.panX) < Math.abs(this.panY) / kAxisLockRatio)
this.panX = 0;
else if (Math.abs(this.panY) < Math.abs(this.panX) / kAxisLockRatio)
this.panY = 0;
// Start the panning animation
window.mozRequestAnimationFrame(callback);
}
break;
case "MozMagnifyGestureStart":
this._pinchDelta = 0;
this.zoomCallbackFired = true;
break;
case "MozMagnifyGestureUpdate":
if (!aEvent.delta)
break;
this._pinchDelta += aEvent.delta;
if ((Math.abs(this._pinchDelta) >= 1) && this.zoomCallbackFired) {
// pinchDelta is the difference in pixels since the last call, so can
// be viewed as the number of extra/fewer pixels visible.
//
// We can work out the new zoom level by looking at the window width
// and height, and the existing zoom level.
let currentZoom = BrowserApp.selectedBrowser.markupDocumentViewer.fullZoom;
let currentSize = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
let newZoom = ((currentSize * currentZoom) + this._pinchDelta) / currentSize;
let self = this;
let callback = {
onBeforePaint: function zoomCallback(timeStamp) {
BrowserApp.selectedBrowser.markupDocumentViewer.fullZoom = newZoom;
self.zoomCallbackFired = true;
}
};
this._pinchDelta = 0;
// Use mozRequestAnimationFrame to stop from flooding fullZoom
this.zoomCallbackFired = false;
window.mozRequestAnimationFrame(callback);
}
break;
}
},
_updateLastPosition: function(x, y, dx, dy) {
this.lastX = x;
this.lastY = y;
this.lastTime = Date.now();
this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
},
_findScrollableElement: function(elem, checkElem) {
// Walk the DOM tree until we find a scrollable element
let scrollable = false;
while (elem) {
/* Element is scrollable if its scroll-size exceeds its client size, and:
* - It has overflow 'auto' or 'scroll'
* - It's a textarea
* - It's an HTML/BODY node
*/
if (checkElem) {
if (((elem.scrollHeight > elem.clientHeight) ||
(elem.scrollWidth > elem.clientWidth)) &&
(elem.style.overflow == 'auto' ||
elem.style.overflow == 'scroll' ||
elem.localName == 'textarea' ||
elem.localName == 'html' ||
elem.localName == 'body')) {
scrollable = true;
break;
}
} else {
checkElem = true;
}
// Propagate up iFrames
if (!elem.parentNode && elem.documentElement &&
elem.documentElement.ownerDocument)
elem = elem.documentElement.ownerDocument.defaultView.frameElement;
else
elem = elem.parentNode;
}
if (!scrollable)
return null;
return elem;
},
_scrollElementBy: function(elem, x, y) {
elem.scrollTop = elem.scrollTop + y;
elem.scrollLeft = elem.scrollLeft + x;
},
_elementCanScroll: function(elem, x, y) {
let scrollX = true;
let scrollY = true;
if (x < 0) {
if (elem.scrollLeft <= 0) {
scrollX = false;
}
} else if (elem.scrollLeft >= (elem.scrollWidth - elem.clientWidth)) {
scrollX = false;
}
if (y < 0) {
if (elem.scrollTop <= 0) {
scrollY = false;
}
} else if (elem.scrollTop >= (elem.scrollHeight - elem.clientHeight)) {
scrollY = false;
}
return scrollX || scrollY;
}
};