2 var Mutant = require('mutantjs');
3 var _ = require('lodash');
4 var rafUtils = require('./raf-utils');
5 var passiveEventListener = require('./passive-event-listener');
6 var raf = require('./raf');
8 module.exports = (function() {
9 /** @const */ var TRACK_BOTTOM = 1;
10 /** @const */ var STABLE = 3;
12 /** Number of pixels we need to be within before we say we're at the bottom */
13 /** @const */ var BOTTOM_MARGIN = 10;
14 /** Number of pixels to show above a message that we scroll to. Context FTW!
15 /** @const */ var TOP_OFFSET = 300;
17 /* Put your scrolling panels on rollers */
18 function Rollers(target, childContainer, options) {
19 options = options || {};
21 this._target = target;
22 this._childContainer = childContainer || target;
23 this._mutationHandlers = {};
24 this._mutationHandlers[TRACK_BOTTOM] = this.updateTrackBottom.bind(this);
25 this._mutationHandlers[STABLE] = this.updateStableTracking.bind(this);
27 this._stableElement = null;
29 if (options.doNotTrack) {
32 this.initTrackingMode();
35 var adjustScroll = this.adjustScroll.bind(this);
37 this.mutant = new Mutant(target, adjustScroll, {
39 observers: { attributes: false, characterData: false },
48 //ignoreFilter: function(mutationRecords) {
49 // var filter = mutationRecords.reduce(function(accum, r) {
50 // var v = r.type === 'attributes' && r.attributeName === 'class' && r.target.id === 'chat-container';
51 // accum = accum && v;
59 function trackLocationAnimationFrame() {
65 var _trackLocation = _.throttle(trackLocationAnimationFrame, 100);
66 passiveEventListener.addEventListener(target, 'scroll', _trackLocation);
67 window.addEventListener('resize', adjustScroll, false);
68 window.addEventListener('focusin', adjustScroll, false);
69 window.addEventListener('focusout', adjustScroll, false);
73 adjustScroll: function() {
74 this._mutationHandlers[this._mode]();
75 this._postMutateTop = this._target.scrollTop;
79 adjustScrollContinuously: function(ms) {
80 rafUtils.intervalUntil(this.adjustScroll.bind(this), ms);
83 initTrackingMode: function() {
84 if (this.isScrolledToBottom()) {
85 this._mode = TRACK_BOTTOM;
87 // Default to stable mode
92 stable: function(stableElement) {
93 var target = this._target;
96 this._stableElement = stableElement || this.getBottomMostVisibleElement();
98 // nothing to stabilize (no content)
99 if (!this._stableElement) return;
101 // TODO: check that the element is within the targets DOM hierarchy
102 var scrollBottom = target.scrollTop + target.clientHeight;
103 var stableElementTop = this._stableElement.offsetTop - target.offsetTop;
105 // Calculate an record the distance of the stable element to the bottom of the view
106 this._stableElementFromBottom = scrollBottom - stableElementTop;
109 setModeLocked: function(value) {
110 this.modeLocked = value;
112 this.trackLocation();
116 disableTrackBottom: function() {
117 this.disableTrackBottom = true;
120 enableTrackBottom: function() {
121 this.disableTrackBottom = true;
122 if (this.isScrolledToBottom()) {
123 this.trackLocation();
127 isScrolledToBottom: function() {
128 var target = this._target;
129 var atBottom = target.scrollTop >= target.scrollHeight - target.clientHeight - BOTTOM_MARGIN;
134 * Update the scroll position to follow the bottom of the scroll pane
136 updateTrackBottom: function() {
137 var target = this._target;
138 var scrollTop = target.scrollHeight - target.clientHeight;
139 this.scroll(scrollTop);
142 startTransition: function(element, maxTimeMs) {
143 this.mutant.startTransition(element, maxTimeMs);
146 endTransition: function(element) {
147 this.mutant.endTransition(element);
151 * Scroll to the bottom and switch the mode to TRACK_BOTTOM
153 scrollToBottom: function() {
154 var target = this._target;
155 var scrollTop = target.scrollHeight - target.clientHeight;
156 this.scroll(scrollTop);
158 delete this._stableElement;
159 delete this._stableElementFromBottom;
160 this._mode = TRACK_BOTTOM;
161 this._postMutateTop = scrollTop;
165 * Scroll to the bottom and switch the mode to TRACK_BOTTOM
167 scrollToElement: function(element, options) {
168 var target = this._target;
171 if (options && options.centre) {
172 // Centre the element in the viewport
173 var elementHeight = element.offsetHeight;
174 var viewportHeight = target.clientHeight;
175 if (elementHeight < viewportHeight) {
176 scrollTop = Math.floor(element.offsetTop + elementHeight / 2 - viewportHeight / 2);
181 scrollTop = element.offsetTop - TOP_OFFSET;
184 if (scrollTop < 0) scrollTop = 0;
186 this.scroll(scrollTop);
188 this.stable(element);
192 * Scroll to the bottom and switch the mode to TRACK_BOTTOM
194 scrollToBottomContinuously: function(ms) {
195 rafUtils.intervalUntil(this.scrollToBottom.bind(this), ms);
198 /* Update the scrollTop to adjust for reflow when in STABLE mode */
199 updateStableTracking: function() {
200 if (!this._stableElement) return;
201 var target = this._target;
203 var stableElementTop = this._stableElement.offsetTop - target.offsetTop;
204 var top = stableElementTop - target.clientHeight + this._stableElementFromBottom;
209 /* Track current position */
210 trackLocation: function() {
211 var target = this._target;
212 if (this._postMutateTop === target.scrollTop) {
216 var atBottom = target.scrollTop >= target.scrollHeight - target.clientHeight - BOTTOM_MARGIN;
218 if (!this.modeLocked) {
220 if (this._mode != TRACK_BOTTOM) {
221 this._mode = TRACK_BOTTOM;
224 if (this._mode != STABLE) {
230 if (this._mode === STABLE) {
231 this._stableElement = this.getBottomMostVisibleElement();
233 if (!this._stableElement) return;
235 // TODO: check that the element is within the targets DOM hierarchy
236 var scrollBottom = target.scrollTop + target.clientHeight;
237 var stableElementTop = this._stableElement.offsetTop - target.offsetTop;
239 // Calculate an record the distance of the stable element to the bottom of the view
240 this._stableElementFromBottom = scrollBottom - stableElementTop;
246 /* Get the Y coordinate of the bottom of the viewport */
247 getScrollBottom: function() {
248 var scrollTop = this._target.scrollTop;
249 return this._target.clientHeight + scrollTop;
252 /* Get the element at the bottom of the viewport */
253 getBottomMostVisibleElement: function() {
254 var scrollTop = this._target.scrollTop;
255 var clientHeight = this._target.clientHeight;
256 var max = scrollTop + clientHeight;
257 var children = this._childContainer.children;
259 for (var i = children.length - 1; i >= 0; i--) {
260 var child = children[i];
261 if (child.offsetTop < max) {
269 /* Get the element in the centre of the viewport */
270 getMostCenteredElement: function() {
271 var scrollTop = this._target.scrollTop;
272 var clientHeight = this._target.clientHeight;
273 var max = scrollTop + clientHeight;
274 var children = this._childContainer.children;
276 for (var i = children.length - 1; i >= 0; i--) {
277 var child = children[i];
278 var middle = clientHeight / 2;
279 var pos = max - child.offsetTop;
288 scroll: function(pixelsFromTop) {
289 this._target.scrollTop = pixelsFromTop;