UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

628 lines (616 loc) 20.6 kB
/** _enyo.TransitionScrollStrategy_ is a helper kind that extends [enyo.TouchScrollStrategy](#enyo.TouchScrollStrategy), optimizing it for scrolling environments in which effecting scroll changes with transforms using CSS transitions is fastest. _enyo.TransitionScrollStrategy_ is not typically created in application code. Instead, it is specified as the value of the _strategyKind_ property of an [enyo.Scroller](#enyo.Scroller) or [enyo.List](#enyo.List), or is used by the framework implicitly. */ enyo.kind({ name: "enyo.TransitionScrollStrategy", kind: "enyo.TouchScrollStrategy", //* @protected components: [ {name: "clientContainer", classes: "enyo-touch-scroller", components: [ {name: "client"} ]} ], events: { onScrollStart: "", onScroll: "", onScrollStop: "" }, handlers: { ondown: "down", ondragfinish: "dragfinish", onwebkitTransitionEnd: "transitionComplete" }, //* No scrollMath tool for this strategy tools: [ {name: "vthumb", kind: "ScrollThumb", axis: "v", showing: true}, {name: "hthumb", kind: "ScrollThumb", axis: "h", showing: false} ], //* Scalar applied to 'flick' event velocity kFlickScalar: 600, //* Top snap boundary, generally 0 topBoundary: 0, //* Right snap boundary, generally (viewport width - content width) rightBoundary: 0, //* Bottom snap boundary, generally (viewport height - content height) bottomBoundary: 0, //* Left snap boundary, generally 0 leftBoundary: 0, //* Flag to specify whether scrolling is in progress scrolling: false, //* Event listener for webkit transition completion listener: null, //* X Distance to scroll into overscroll space before bouncing back boundaryX:0, //* Y Distance to scroll into overscroll space before bouncing back boundaryY:0, //* Timeout used to stop scrolling on mousedown stopTimeout: null, //* MS delay used to stop scrolling on mousedown stopTimeoutMS: 80, //* Interval used to update scroll values and bubble scroll events during scroll animation scrollInterval: null, //* MS delay used to update scroll values and bubble scroll events during scroll animation scrollIntervalMS: 50, //* Transition animations transitions: { //* None - used for dragging, etc. none : "", //* Scroll - basic scrolling behavior scroll : "3.8s cubic-bezier(.19,1,.28,1.0) 0s", //* Bounce - overscroll bounceback behavior bounce : "0.5s cubic-bezier(0.06,.5,.5,.94) 0s" }, //* @public //* Sets the left scroll position within the scroller. setScrollLeft: function(inLeft) { var prevLeft = this.scrollLeft; this.stop(); this.scrollLeft = inLeft; if(this.isInLeftOverScroll() || this.isInRightOverScroll()) { this.scrollLeft = prevLeft; } this.effectScroll(); }, //* Sets the top scroll position within the scroller. setScrollTop: function(inTop) { var prevTop = this.scrollTop; this.stop(); this.scrollTop = inTop; if(this.isInTopOverScroll() || this.isInBottomOverScroll()) { this.scrollTop = prevTop; } this.effectScroll(); }, setScrollX: function(inLeft) { this.scrollLeft = -1*inLeft; }, setScrollY: function(inTop) { this.scrollTop = -1*inTop; }, //* Gets the left scroll position within the scroller. getScrollLeft: function() { return this.scrollLeft; }, //* Gets the top scroll position within the scroller. getScrollTop: function() { return this.scrollTop; }, //* @protected // apply initial transform so we're always composited create: enyo.inherit(function (sup) { return function() { sup.apply(this, arguments); enyo.dom.transformValue(this.$.client, this.translation, "0,0,0"); }; }), destroy: enyo.inherit(function (sup) { return function() { this.clearCSSTransitionInterval(); sup.apply(this, arguments); }; }), getScrollSize: function() { var n = this.$.client.hasNode(); return {width: n ? n.scrollWidth : 0, height: n ? n.scrollHeight : 0}; }, horizontalChanged: function() { if(this.horizontal == "hidden") { this.scrollHorizontal = false; } }, verticalChanged: function() { if(this.vertical == "hidden") { this.scrollVertical = false; } }, intervalChanged: function() { // TODO: Implement variable speed implementation if (this.interval != enyo.TransitionScrollStrategy.prototype.interval) { this.warn("'interval' not implemented in TransitionScrollStrategy"); } }, calcScrollNode: function() { return this.$.clientContainer.hasNode(); }, calcBoundaries: function() { var b = this._getScrollBounds(); this.bottomBoundary = b.clientHeight - b.height; this.rightBoundary = b.clientWidth - b.width; }, maxHeightChanged: function() { // content should cover scroller at a minimum if there's no max-height. this.$.client.applyStyle("min-height", this.maxHeight ? null : "100%"); this.$.client.applyStyle("max-height", this.maxHeight); this.$.clientContainer.addRemoveClass("enyo-scrollee-fit", !this.maxHeight); }, calcAutoScrolling: function() { var b = this.getScrollBounds(); if(this.vertical) { this.scrollVertical = b.height > b.clientHeight; } if(this.horizontal) { this.scrollHorizontal = b.width > b.clientWidth; } }, isInOverScroll: function() { return (this.isInTopOverScroll() || this.isInBottomOverScroll() || this.isInLeftOverScroll() || this.isInRightOverScroll()); }, isInLeftOverScroll: function() { return (this.getScrollLeft() < this.leftBoundary); }, isInRightOverScroll: function() { if(this.getScrollLeft <= 0) { return false; } else { return (this.getScrollLeft()*-1 < this.rightBoundary); } }, isInTopOverScroll: function() { return (this.getScrollTop() < this.topBoundary); }, isInBottomOverScroll: function() { if(this.getScrollTop() <= 0) { return false; } else { return (this.getScrollTop()*-1 < this.bottomBoundary); } }, calcStartInfo: function() { var sb = this.getScrollBounds(), y = this.getScrollTop(), x = this.getScrollLeft(); this.startEdges = { top: y === 0, bottom: y === sb.maxTop, left: x === 0, right: x === sb.maxLeft }; }, mousewheel: function(inSender, e) { if (!this.dragging && this.useMouseWheel) { this.calcBoundaries(); this.syncScrollMath(); this.stabilize(); var dy = this.vertical ? e.wheelDeltaY || e.wheelDelta : 0; var y = parseFloat(this.getScrollTop()) + -1*parseFloat(dy); y = (y*-1 < this.bottomBoundary) ? -1*this.bottomBoundary : (y < this.topBoundary) ? this.topBoundary : y; this.setScrollTop(y); this.doScroll(); e.preventDefault(); return true; } }, // Update thumbs, recalculate boundaries, and bubble scroll event scroll: function() { if(this.thumb) { this.updateThumbs(); } this.calcBoundaries(); this.doScroll(); }, // Scroll to current x,y coordinates and bubble scrollstart event start: function() { this.startScrolling(); this.doScrollStart(); }, // If scrolling, stop. Hide thumbs and bubble scrollstop event. stop: function() { if(this.isScrolling()) { this.stopScrolling(); } if (this.thumb) { this.delayHideThumbs(100); } this.doScrollStop(); }, // Set scroll x value to the current computed style updateX: function() { var x = window.getComputedStyle(this.$.client.node,null).getPropertyValue(enyo.dom.getCssTransformProp()).split('(')[1]; x = (x === undefined) ? 0 : x.split(')')[0].split(',')[4]; if(-1*parseFloat(x) === this.scrollLeft) { return false; } this.scrollLeft = -1*parseFloat(x); return true; }, // Set scroll y value to the current computed style updateY: function() { var y = window.getComputedStyle(this.$.client.node,null).getPropertyValue(enyo.dom.getCssTransformProp()).split('(')[1]; y = (y === undefined) ? 0 : y.split(')')[0].split(',')[5]; if(-1*parseFloat(y) === this.scrollTop) { return false; } this.scrollTop = -1*parseFloat(y); return true; }, // Apply transform to scroll the scroller effectScroll: function() { var o = (-1*this.scrollLeft) + "px, " + (-1*this.scrollTop) + "px" + (this.accel ? ", 0" : ""); enyo.dom.transformValue(this.$.client, this.translation, o); }, // On touch, stop transition by setting transform values to current computed style, and // changing transition time to 0s. TODO down: function() { var _this = this; if (this.isScrolling() && !this.isOverscrolling()) { this.stopTimeout = setTimeout(function() { _this.stop(); }, this.stopTimeoutMS); return true; } }, // Special synthetic DOM events served up by the Gesture system dragstart: function(inSender, inEvent) { if(this.stopTimeout) { clearTimeout(this.stopTimeout); } // Ignore drags sent from multi-touch events if(!this.dragDuringGesture && inEvent.srcEvent.touches && inEvent.srcEvent.touches.length > 1) { return true; } this.shouldDrag(inEvent); this.dragging = (inEvent.dragger == this || (!inEvent.dragger && inEvent.boundaryDragger == this)); if (this.dragging) { if(this.isScrolling()) { this.stopScrolling(); } if(this.thumb) { this.showThumbs(); } inEvent.preventDefault(); this.prevY = inEvent.pageY; this.prevX = inEvent.pageX; if (this.preventDragPropagation) { return true; } } }, // Determine if we should allow dragging shouldDrag: function(e) { this.calcStartInfo(); this.calcBoundaries(); this.calcAutoScrolling(); if(!this.scrollHorizontal) { return this.shouldDragVertical(e); } else { if(!this.scrollVertical) { return this.shouldDragHorizontal(e); } else { return (this.shouldDragVertical(e) || this.shouldDragHorizontal(e)); } } }, shouldDragVertical: function(e) { var canV = this.canDragVertical(e); var oobV = this.oobVertical(e); // scroll if not at a boundary if (!e.boundaryDragger && canV) { e.boundaryDragger = this; } // include boundary exclusion if (!oobV && canV) { e.dragger = this; return true; } }, shouldDragHorizontal: function(e) { var canH = this.canDragHorizontal(e); var oobH = this.oobHorizontal(e); // scroll if not at a boundary if (!e.boundaryDragger && canH) { e.boundaryDragger = this; } // include boundary exclusion if (!oobH && canH) { e.dragger = this; return true; } }, canDragVertical: function(e) { return (this.scrollVertical && e.vertical); }, canDragHorizontal: function(e) { return (this.scrollHorizontal && !e.vertical); }, oobVertical: function(e) { var down = e.dy < 0; return (!down && this.startEdges.top || down && this.startEdges.bottom); }, oobHorizontal: function(e) { var right = e.dx < 0; return (!right && this.startEdges.left || right && this.startEdges.right); }, // Move scroller based on user's dragging drag: function(inSender, e) { // if the list is doing a reorder, don't scroll if(this.listReordering) { return false; } // if shouldDrag() set this.dragging to true if(this.dragging) { e.preventDefault(); // calculate new scroll values this.scrollLeft = this.scrollHorizontal ? this.calculateDragDistance(parseInt(this.getScrollLeft(), 10), (-1*(e.pageX-this.prevX)), this.leftBoundary, this.rightBoundary) : this.getScrollLeft(); this.scrollTop = this.scrollVertical ? this.calculateDragDistance(this.getScrollTop(), (-1*(e.pageY-this.prevY)), this.topBoundary, this.bottomBoundary) : this.getScrollTop(); // apply new scroll values this.effectScroll(); this.scroll(); // save values for next drag this.prevY = e.pageY; this.prevX = e.pageX; this.resetBoundaryX(); this.resetBoundaryY(); } }, //* Figure how far the drag should go based on pointer movement (delta) calculateDragDistance: function(currentPosition, delta, aBoundary, bBoundary) { var newPosition = currentPosition + delta; return this.overscrollDragDamping(currentPosition, newPosition,delta,aBoundary,bBoundary); }, //* Provides resistance against dragging into overscroll overscrollDragDamping: function(currentPosition, newPosition, delta, aBoundary, bBoundary) { if(newPosition < aBoundary || newPosition*-1 < bBoundary) { delta /= 2; newPosition = currentPosition + delta; } return newPosition; }, resetBoundaryX: function() { this.boundaryX = 0; }, resetBoundaryY: function() { this.boundaryY = 0; }, // When user releases the drag, set this.dragging to false, bounce overflow back, and hide scrim. dragfinish: function(inSender, inEvent) { if (this.dragging) { inEvent.preventTap(); this.dragging = false; if(!this.isScrolling()) { this.correctOverflow(); } if (this.scrim) { this.$.scrim.hide(); } } }, // Bounce back from overscroll region correctOverflow: function() { if(this.isInOverScroll()) { var x = (this.scrollHorizontal) ? this.correctOverflowX() : false; var y = (this.scrollVertical) ? this.correctOverflowY() : false; if(x !== false && y !== false) { this.scrollLeft = (x !== false) ? x : this.getScrollLeft(); this.scrollTop = (y !== false) ? y : this.getScrollTop(); this.startOverflowScrolling(); } else if(x !== false) { this.scrollLeft = x; this.scrollTop = this.targetScrollTop || this.scrollTop; this.targetScrollLeft = this.getScrollLeft(); if(!this.vertical) { this.startOverflowScrolling(); } else { this.startScrolling(); } } else if(y !== false) { this.scrollTop = y; this.scrollLeft = this.targetScrollLeft || this.scrollLeft; this.targetScrollTop = this.getScrollTop(); if(!this.scrollHorizontal) { this.startOverflowScrolling(); } else { this.startScrolling(); } } } }, // Determine if we're overscrolled on the x axis and if so return proper edge value correctOverflowX: function() { if(this.isInLeftOverScroll()) { if(this.beyondBoundary(this.getScrollLeft(), this.leftBoundary, this.boundaryX)) { return this.leftBoundary; } } else if(this.isInRightOverScroll()) { if(this.beyondBoundary(this.getScrollLeft(), this.rightBoundary, this.boundaryX)) { return -1*this.rightBoundary; } } return false; }, // Determine if we're overscrolled on the y axis and if so return proper edge value correctOverflowY: function() { if(this.isInTopOverScroll()) { if(this.beyondBoundary(this.getScrollTop(), this.topBoundary, this.boundaryY)) { return this.topBoundary; } } else if(this.isInBottomOverScroll()) { if(this.beyondBoundary(this.getScrollTop(), this.bottomBoundary, this.boundaryY)) { return -1*this.bottomBoundary; } } return false; }, // If we've crossed the determined delta, bounce back beyondBoundary: function(current,boundary,max) { return (Math.abs(Math.abs(boundary) - Math.abs(current)) > Math.abs(max)); }, // When user flicks/throws scroller, figure the distance to be travelled and whether we will end up // in the overscroll region. flick: function(inSender, e) { if(this.dragging && this.flickOnEnabledAxis(e)) { this.scrollLeft = this.scrollHorizontal ? this.calculateFlickDistance(this.scrollLeft, -1*e.xVelocity) : this.getScrollLeft(); this.scrollTop = this.scrollVertical ? this.calculateFlickDistance(this.scrollTop, -1*e.yVelocity) : this.getScrollTop(); this.targetScrollLeft = this.scrollLeft; this.targetScrollTop = this.scrollTop; this.boundaryX = null; this.boundaryY = null; // if flick will put the x axis into overscroll, figure where we should bounce back (boundary) if(this.isInLeftOverScroll()) { this.boundaryX = this.figureBoundary(this.getScrollLeft()); } else if(this.isInRightOverScroll()) { this.boundaryX = this.figureBoundary(-1*this.bottomBoundary - this.getScrollLeft()); } // if flick will put the y axis into overscroll, figure where we should bounce back (boundary) if(this.isInTopOverScroll()) { this.boundaryY = this.figureBoundary(this.getScrollTop()); } else if(this.isInBottomOverScroll()) { this.boundaryY = this.figureBoundary(-1*this.bottomBoundary - this.getScrollTop()); } // kickoff scrolling animation this.startScrolling(); return this.preventDragPropagation; } }, flickOnEnabledAxis: function(e) { return Math.abs(e.xVelocity) > Math.abs(e.yVelocity) ? this.scrollHorizontal : this.scrollVertical; }, calculateFlickDistance: function(currentPosition, flickVelocity) { return (currentPosition + (flickVelocity * this.kFlickScalar)); }, // Apply the 'scroll' transition, apply new transform based on x and y, and begin // this.scrollInterval to update the scrollTop/Left values while scrolling startScrolling: function() { this.applyTransition("scroll"); this.effectScroll(); this.setCSSTransitionInterval(); this.scrolling = true; }, // Apply the 'bounce' transition, apply new transform based on x and y, and begin // this.scrollInterval to update the scrollTop/Left values while scrolling startOverflowScrolling: function() { this.applyTransition("bounce"); this.effectScroll(); this.setOverflowTransitionInterval(); this.scrolling = true; }, // Apply the given transition to this.$.client applyTransition: function(which) { this.$.client.applyStyle("-webkit-transition", this.transitions[which]); }, // Turn off CSS transition and clear this.scrollInterval stopScrolling: function() { this.resetCSSTranslationVals(); this.clearCSSTransitionInterval(); this.scrolling = false; }, // Create an interval to: update the x/y values while scrolling is happening, check for // crossing into the overflow region, and bubble a scroll event setCSSTransitionInterval: function() { this.clearCSSTransitionInterval(); this.scrollInterval = setInterval(this.bindSafely(function() { this.updateScrollPosition(); this.correctOverflow(); }), this.scrollIntervalMS); }, // Create an interval to: update the x/y values while scrolling is happening, and bubble // a scroll event (don't check for crossing into overflow since we're there already) setOverflowTransitionInterval: function() { this.clearCSSTransitionInterval(); this.scrollInterval = setInterval(this.bindSafely(function() { this.updateScrollPosition(); }), this.scrollIntervalMS); }, // Save current x/y position and bubble scroll event updateScrollPosition: function() { var yChanged = this.updateY(); var xChanged = this.updateX(); this.scroll(); if(!yChanged && !xChanged) { this.stop(); } }, // Clear this.scrollInterval clearCSSTransitionInterval: function() { if(this.scrollInterval) { clearInterval(this.scrollInterval); this.scrollInterval = null; } }, // Set scroller translation to current position and turn transition off. This effectively // stops scrolling resetCSSTranslationVals: function() { var prop = enyo.dom.getCssTransformProp(); var transformStyle = window.getComputedStyle(this.$.client.node,null).getPropertyValue(prop).split('(')[1].split(')')[0].split(','); this.applyTransition("none"); this.scrollLeft = -1*transformStyle[4]; this.scrollTop = -1*transformStyle[5]; this.effectScroll(); }, // Figure how far into the overscroll region we should go before bouncing back figureBoundary: function(target) { var absTarget = Math.abs(target); var retVal = absTarget - absTarget/Math.pow(absTarget,0.02); retVal = target < 0 ? -1*retVal : retVal; return retVal; }, // When transition animation is complete, check if we need to bounce back from overscroll // region. If not, stop. transitionComplete: function(inSender, inEvent) { // Only process transition complete if sent from this container if(inEvent.originator !== this.$.client) { return; } var posChanged = false; if(this.isInTopOverScroll()) { posChanged = true; this.scrollTop = this.topBoundary; } else if (this.isInBottomOverScroll()) { posChanged = true; this.scrollTop = -1*this.bottomBoundary; } if(this.isInLeftOverScroll()) { posChanged = true; this.scrollLeft = this.leftBoundary; } else if (this.isInRightOverScroll()) { posChanged = true; this.scrollLeft = -1*this.rightBoundary; } if(posChanged) { this.startOverflowScrolling(); } else { this.stop(); } }, //* Scroll to the specified x and y coordinates scrollTo: function(inX, inY) { this.setScrollTop(inY); this.setScrollLeft(inX); this.start(); }, //* Returns the values of _overleft_ and _overtop_, if any. getOverScrollBounds: function() { return { overleft: Math.min(this.leftBoundary + this.scrollLeft, 0) || Math.max(this.rightBoundary + this.scrollLeft, 0), overtop: Math.min(this.topBoundary + this.scrollTop, 0) || Math.max(this.bottomBoundary + this.scrollTop, 0) }; } });