ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
432 lines (423 loc) • 13.7 kB
JavaScript
/**
_enyo.Scroller_ is a scroller suitable for use in both desktop and mobile
applications.
In some mobile environments, a default scrolling solution is not implemented for
DOM elements. In such cases, _enyo.Scroller_ implements a touch-based scrolling
solution, which may be opted into either globally (by setting
_enyo.Scroller.touchScrolling_ to _true_) or on a per-instance basis (by
specifying a _strategyKind_ of _"TouchScrollStrategy"_).
For more information, see the documentation on
[Scrollers](building-apps/layout/scrollers.html) in the Enyo Developer Guide.
*/
enyo.kind({
name: "enyo.Scroller",
published: {
/**
Specifies how to horizontally scroll. Acceptable values are
"scroll", "auto," "hidden," and "default". The precise
effect of the setting is determined by the scroll strategy.
*/
horizontal: "default",
/**
Specifies how to vertically scroll. Acceptable values are "scroll",
"auto," "hidden," and "default". The precise effect of the setting
is determined by the scroll strategy.
*/
vertical: "default",
/**
The vertical scroll position
*/
scrollTop: 0,
/**
The horizontal scroll position
*/
scrollLeft: 0,
/**
Maximum height of the scroll content
*/
maxHeight: null,
/**
Set to true to make this scroller select a platform-appropriate
touch-based scrolling strategy. Note that if you specify a value for
_strategyKind_, that will take precedence over this setting.
*/
touch: false,
/**
Specifies a type of scrolling. The scroller will attempt to
automatically select a strategy compatible with the runtime
environment. Alternatively, you may choose to use a specific
strategy:
* <a href="#enyo.ScrollStrategy">ScrollStrategy</a> is the default
and implements no scrolling, relying instead on the environment
to scroll properly.
* <a href="#enyo.TouchScrollStrategy">TouchScrollStrategy</a>
implements a touch scrolling mechanism.
* <a href="#enyo.TranslateScrollStrategy">TranslateScrollStrategy</a>
implements a touch scrolling mechanism using translations; it is
currently recommended only for Android 3 and 4 & Windows Phone 8.
* <a href="#enyo.TransitionScrollStrategy">TransitionScrollStrategy</a>
implements a touch scrolling mechanism using CSS transitions; it is
currently recommended only for iOS 5 and later.
*/
strategyKind: "ScrollStrategy",
//* Set to true to display a scroll thumb in touch scrollers
thumb: true,
//* Use mouse wheel to move scroller
useMouseWheel: true
},
events: {
//* Fires when a scrolling action starts.
//* Includes scrollBounds field with current values of getScrollBounds
onScrollStart: "",
//* Fires while a scrolling action is in progress.
//* Includes scrollBounds field with current values of getScrollBounds
onScroll: "",
//* Fires when a scrolling action stops.
//* Includes scrollBounds field with current values of getScrollBounds
onScrollStop: ""
},
/**
If true (the default) and a touch scroller, the scroller will overscroll
and bounce back at the edges
*/
touchOverscroll: true,
/**
If true (the default), the scroller will not propagate _dragstart_
events that cause it to start scrolling
*/
preventDragPropagation: true,
/**
If true, the scroller will not propagate scroll events
*/
preventScrollPropagation: true,
//* @protected
// needed to allow global mods to enyo.Scroller.touchScrolling
noDefer: true,
handlers: {
onscroll: "domScroll",
onScrollStart: "scrollStart",
onScroll: "scroll",
onScrollStop: "scrollStop"
},
classes: "enyo-scroller",
statics: {
osInfo: [
{os: "android", version: 3},
{os: "androidChrome", version: 18},
{os: "androidFirefox", version: 16},
{os: "firefoxOS", version: 16},
{os: "ios", version: 5},
{os: "webos", version: 1e9},
{os: "blackberry", version:1e9},
{os: "tizen", version: 2}
],
//* Returns true if platform should have touch events.
hasTouchScrolling: function() {
for (var i=0, t; (t=this.osInfo[i]); i++) {
if (enyo.platform[t.os]) {
return true;
}
}
// special detection for IE10+ on touch devices
if ((enyo.platform.ie >= 10 || enyo.platform.windowsPhone >= 8) && enyo.platform.touch) {
return true;
}
},
/**
Returns true if the platform has native div scrollers (desktop
browsers always have them).
*/
hasNativeScrolling: function() {
for (var i=0, t; (t=this.osInfo[i]); i++) {
if (enyo.platform[t.os] < t.version) {
return false;
}
}
return true;
},
getTouchStrategy: function() {
return (enyo.platform.android >= 3) || (enyo.platform.windowsPhone === 8) || (enyo.platform.webos >= 4)
? "TranslateScrollStrategy"
: "TouchScrollStrategy";
}
},
controlParentName: "strategy",
create: enyo.inherit(function (sup) {
return function() {
sup.apply(this, arguments);
this.horizontalChanged();
this.verticalChanged();
this.useMouseWheelChanged();
};
}),
importProps: enyo.inherit(function (sup) {
return function(inProps) {
sup.apply(this, arguments);
// allow global overriding of strategy kind
if (inProps && inProps.strategyKind === undefined && (enyo.Scroller.touchScrolling || this.touch)) {
this.strategyKind = enyo.Scroller.getTouchStrategy();
}
};
}),
initComponents: enyo.inherit(function (sup) {
return function() {
this.strategyKindChanged();
sup.apply(this, arguments);
};
}),
teardownChildren: enyo.inherit(function (sup) {
return function() {
this.cacheScrollPosition();
sup.apply(this, arguments);
};
}),
rendered: enyo.inherit(function (sup) {
return function() {
sup.apply(this, arguments);
this.restoreScrollPosition();
};
}),
strategyKindChanged: function() {
if (this.$.strategy) {
this.$.strategy.destroy();
this.controlParent = null;
}
// note: createComponents automatically updates controlParent.
this.createStrategy();
if (this.hasNode()) {
this.render();
}
},
createStrategy: function() {
this.createComponents([{name: "strategy", maxHeight: this.maxHeight,
kind: this.strategyKind, thumb: this.thumb,
preventDragPropagation: this.preventDragPropagation,
overscroll:this.touchOverscroll, isChrome: true}]);
},
getStrategy: function() {
return this.$.strategy;
},
maxHeightChanged: function() {
this.$.strategy.setMaxHeight(this.maxHeight);
},
showingChanged: enyo.inherit(function (sup) {
return function() {
if (!this.showing) {
this.cacheScrollPosition();
this.setScrollLeft(0);
this.setScrollTop(0);
}
sup.apply(this, arguments);
if (this.showing) {
this.restoreScrollPosition();
}
};
}),
thumbChanged: function() {
this.$.strategy.setThumb(this.thumb);
},
cacheScrollPosition: function() {
this.cachedPosition = {left: this.getScrollLeft(), top: this.getScrollTop()};
},
restoreScrollPosition: function() {
if (this.cachedPosition) {
var cp = this.cachedPosition;
if (cp.top || cp.left) {
this.setScrollLeft(cp.left);
this.setScrollTop(cp.top);
this.cachedPosition = null;
}
}
},
horizontalChanged: function() {
this.$.strategy.setHorizontal(this.horizontal);
},
verticalChanged: function() {
this.$.strategy.setVertical(this.vertical);
},
// FIXME: these properties are virtual; property changed methods are fired only if
// property value changes, not if getter changes.
//* Sets scroll position along horizontal axis.
setScrollLeft: function(inLeft) {
this.scrollLeft = inLeft;
this.$.strategy.setScrollLeft(this.scrollLeft);
},
//* Sets scroll position along vertical axis.
setScrollTop: function(inTop) {
this.scrollTop = inTop;
this.$.strategy.setScrollTop(inTop);
},
//* Gets scroll position along horizontal axis.
getScrollLeft: function() {
// sync our internal property
this.scrollLeft = this.$.strategy.getScrollLeft();
return this.scrollLeft;
},
//* Gets scroll position along vertical axis.
getScrollTop: function() {
// sync our internal property
this.scrollTop = this.$.strategy.getScrollTop();
return this.scrollTop;
},
//* @public
/**
Returns an object describing the scroll boundaries with these properties:
* _left_, _top_: current left/top scroll position
* _maxLeft_, _maxTop_: maximum value for left/top (minimum is always 0)
* _clientHeight_, _clientWidth_: size of the scroller on screen
* _width_, _height_: size of the full area of the scrolled region
* _xDir, yDir_: either 1, -1, or 0 indicated positive/negative movement along
along the axis or none at all, respectively
*/
getScrollBounds: function() {
var bounds = this.$.strategy.getScrollBounds();
if (
(bounds.xDir !== -1 && bounds.xDir !== 0 && bounds.xDir !== 1) ||
(bounds.yDir !== -1 && bounds.yDir !== 0 && bounds.yDir !== 1)
) {
this.decorateBounds(bounds);
}
// keep our properties synchronized always and without extra calls
this.scrollTop = bounds.top;
this.scrollLeft = bounds.left;
return bounds;
},
/**
Scrolls the given control (_inControl_) into view. If _inAlignWithTop_
is true, _inControl_ is aligned with the top of the scroller.
*/
scrollIntoView: function(inControl, inAlignWithTop) {
this.$.strategy.scrollIntoView(inControl, inAlignWithTop);
},
//* Scrolls to the position specified by _inX_ and _inY_ in pixel units.
scrollTo: function(inX, inY) {
this.$.strategy.scrollTo(inX, inY);
},
/**
Ensures that the given control is visible in the scroller's viewport.
Unlike _scrollIntoView_, which uses DOM's _scrollIntoView_, this only
affects the current scroller.
*/
scrollToControl: function(inControl, inAlignWithTop) {
this.scrollToNode(inControl.hasNode(), inAlignWithTop);
},
//* Ensures that the given node is visible in the scroller's viewport.
scrollToNode: function(inNode, inAlignWithTop) {
this.$.strategy.scrollToNode(inNode, inAlignWithTop);
},
//* @protected
//* Adds current values of getScrollBounds to event
decorateScrollEvent: function(inEvent) {
var bounds = inEvent.scrollBounds = inEvent.scrollBounds || this.$.strategy._getScrollBounds();
// in the off chance that the event already had scrollBounds then we need
// to make sure they are decorated
if (
(bounds.xDir !== -1 && bounds.xDir !== 0 && bounds.xDir !== 1) ||
(bounds.yDir !== -1 && bounds.yDir !== 0 && bounds.yDir !== 1)
) {
this.decorateBounds(bounds);
}
// keep our properties synchronized always and without extra calls
this.scrollTop = bounds.top;
this.scrollLeft = bounds.left;
},
decorateBounds: function (bounds) {
var x = this.scrollLeft - bounds.left,
y = this.scrollTop - bounds.top;
bounds.xDir = (x < 0? 1: x > 0? -1: 0);
bounds.yDir = (y < 0? 1: y > 0? -1: 0);
// we update our current bounds properties so we don't have to unnecessarily
// call getScrollTop/getScrollLeft because we already have the current data
this.scrollLeft = bounds.left;
this.scrollTop = bounds.top;
},
//* Normalizes scroll event to _onScroll_.
domScroll: function(inSender, e) {
// if a scroll event originated here, pass it to our strategy to handle
if (this.$.strategy.domScroll && e.originator == this) {
this.$.strategy.domScroll(inSender, e);
}
this.decorateScrollEvent(e);
this.doScroll(e);
return true;
},
/**
Returns true if the current scroll event should be stopped; false if it
should be allowed to propagate.
*/
shouldStopScrollEvent: function(inEvent) {
return (this.preventScrollPropagation &&
inEvent.originator.owner != this.$.strategy);
},
/**
Calls _shouldStopScrollEvent_ to determine whether current scroll event
should be stopped.
*/
scrollStart: function(inSender, inEvent) {
if (!this.shouldStopScrollEvent(inEvent)) {
this.decorateScrollEvent(inEvent);
return false;
}
return true;
},
//* Either propagates or stops the current scroll event.
scroll: function(inSender, inEvent) {
// note: scroll event can be native dom or generated.
var stop;
if (inEvent.dispatchTarget) {
// allow a dom event if it orignated with this scroller or its strategy
stop = this.preventScrollPropagation && !(inEvent.originator == this ||
inEvent.originator.owner == this.$.strategy);
} else {
stop = this.shouldStopScrollEvent(inEvent);
}
if (!stop) {
this.decorateScrollEvent(inEvent);
return false;
}
return true;
},
/**
Calls _shouldStopScrollEvent_ to determine whether current scroll event
should be stopped.
*/
scrollStop: function(inSender, inEvent) {
if (!this.shouldStopScrollEvent(inEvent)) {
this.decorateScrollEvent(inEvent);
return false;
}
return true;
},
//* @public
//* Scroll to the top of the scrolling region.
scrollToTop: function() {
this.setScrollTop(0);
},
//* Scroll to the bottom of the scrolling region.
scrollToBottom: function() {
this.setScrollTop(this.getScrollBounds().maxTop);
},
//* Scroll to the right edge of the scrolling region.
scrollToRight: function() {
this.setScrollLeft(this.getScrollBounds().maxLeft);
},
//* Scroll to the left edge of the scrolling region.
scrollToLeft: function() {
this.setScrollLeft(0);
},
//* Ensures scroll position is in bounds.
stabilize: function() {
var s = this.getStrategy();
if (s.stabilize) {
s.stabilize();
}
},
//* Send the useMouseWheel propert to the scroll strategy
useMouseWheelChanged: function() {
this.$.strategy.setUseMouseWheel(this.useMouseWheel);
}
});
// provide a touch scrolling solution by default when the environment is mobile
if (enyo.Scroller.hasTouchScrolling()) {
enyo.Scroller.prototype.strategyKind = enyo.Scroller.getTouchStrategy();
}