framework/ember/app/components/discussion-scrollbar.js
2015-01-02 11:46:40 +10:30

281 lines
9.3 KiB
JavaScript

import Ember from 'ember';
import MenuItem from '../components/menu-item';
import Scrollbar from '../utils/scrollbar';
import PostStreamMixin from '../mixins/post-stream';
export default Ember.View.extend(PostStreamMixin, {
/**
@property templateName
@type String
*/
templateName: 'components/discussion-scrollbar',
classNames: ['scrubber', 'discussion-scrubber'],
// An object which represents/ecapsulates the scrollbar.
scrollbar: null,
// Right after the controller finished loading a discussion, we want to
// trigger a scroll event on the window so the interface is kept up-to-date.
loadedChanged: function() {
this.scrollbar.setDisabled(! this.get('controller.loaded'));
}.observes('controller.loaded'),
countChanged: function() {
this.scrollbar.setCount(this.get('controller.postStream.count'));
}.observes('controller.postStream.count'),
windowWasResized: function(event) {
var view = event.data.view;
// view.scrollbar.$.height($('#sidebar-content').height() + $('#sidebar-content').offset().top - view.scrollbar.$.offset().top - 80);
view.scrollbar.update();
},
windowWasScrolled: function(event) {
var view = event.data.view,
$window = $(window);
if (! view.get('controller.loaded') || $window.data('disableScrollHandler')) {
return;
}
var scrollTop = $window.scrollTop(),
windowHeight = $window.height();
// Before looping through all of the posts, we reset the scrollbar
// properties to a 'default' state. These values reflect what would be
// seen if the browser were scrolled right up to the top of the page,
// and the viewport had a height of 0.
var index = $('.posts .item:first').data('end');
var visiblePosts = 0;
var period = '';
var first = $('.posts .item[data-start=0]');
var offsetTop = first.length ? first.offset().top : 0;
// Now loop through each of the items in the discussion. An 'item' is
// either a single post or a 'gap' of one or more posts that haven't
// been loaded yet.
// @todo cache item top positions to speed this up?
$('.posts .item').each(function(k) {
var $this = $(this),
top = $this.offset().top - offsetTop,
height = $this.outerHeight();
// If this item is above the top of the viewport, skip to the next
// post. If it's below the bottom of the viewport, break out of the
// loop.
if (top + height < scrollTop) {
return;
}
if (top > scrollTop + windowHeight) {
return false;
}
// If the bottom half of this item is visible at the top of the
// viewport, then add the visible proportion to the visiblePosts
// counter, and set the scrollbar index to whatever the visible
// proportion represents. For example, if a gap represents indexes
// 0-9, and the bottom 50% of the gap is visible in the viewport,
// then the scrollbar index will be 5.
if (top <= scrollTop && top + height > scrollTop) {
visiblePosts = (top + height - scrollTop) / height;
index = parseFloat($this.data('end')) + 1 - visiblePosts;
}
// If the top half of this item is visible at the bottom of the
// viewport, then add the visible proportion to the visiblePosts
// counter.
else if (top + height >= scrollTop + windowHeight) {
visiblePosts += (scrollTop + windowHeight - top) / height;
}
// If the whole item is visible in the viewport, then increment the
// visiblePosts counter.
else {
visiblePosts++;
}
// If this item has a time associated with it, then set the
// scrollbar's current period to a formatted version of this time.
if ($this.data('time')) {
period = $this.data('time');
}
});
// Now that we've looped through all of the items and have worked out
// the scrollbar's current index and the number of posts visible in the
// viewport, we can update the scrollbar.
view.scrollbar.setIndex(index);
view.scrollbar.setVisible(visiblePosts);
view.scrollbar.update();
view.scrollbar.$.find('.index').text(Math.ceil(index + visiblePosts));
view.scrollbar.$.find('.description').text(moment(period).format('MMMM YYYY'));
},
mouseWasMoved: function(event) {
var view = event.data.view;
if ( ! event.data.handle) {
return;
}
var offsetPixels = event.clientY - event.data.mouseStart;
var offsetPercent = offsetPixels / view.scrollbar.$.outerHeight() * 100;
var offsetIndex = offsetPercent / view.scrollbar.percentPerPost().index;
var newIndex = Math.max(0, Math.min(event.data.indexStart + offsetIndex, view.scrollbar.count - 1));
view.scrollToIndex(newIndex);
},
mouseWasReleased: function(event) {
var view = event.data.view;
if (! event.data.handle) {
return;
}
event.data.mouseStart = 0;
event.data.indexStart = 0;
event.data.handle = null;
$(window).data('disableScrollHandler', false);
view.get('controller').send('jumpToIndex', Math.floor(view.scrollbar.index));
$(window).scroll();
$('body').css('cursor', '');
},
didInsertElement: function() {
var view = this;
// Set up scrollbar object
this.scrollbar = new Scrollbar($('.discussion-scrubber .scrollbar'));
this.scrollbar.setDisabled(true);
this.countChanged();
this.loadedChanged();
// Whenever the window is resized, adjust the height of the scrollbar
// so that it fills the height of the sidebar.
$(window).on('resize', {view: this}, this.windowWasResized).resize();
// Define a handler to update the state of the scrollbar to reflect the
// current scroll position of the page.
$(window).on('scroll', {view: this}, this.windowWasScrolled);
this.get('controller').on('loadingIndex', this, this.loadingIndex);
// Now we want to make the scrollbar handle draggable. Let's start by
// preventing default browser events from messing things up.
this.scrollbar.$
.css('user-select', 'none')
.bind('dragstart mousedown', function(e) {
e.preventDefault();
});
// When the mouse is pressed on the scrollbar handle, we need to capture
// some information about the current position.
var scrollData = {
view: this,
mouseStart: 0,
indexStart: 0,
handle: null
};
this.scrollbar.$.find('.scrollbar-slider').css('cursor', 'move').mousedown(function(e) {
scrollData.mouseStart = e.clientY;
scrollData.indexStart = view.scrollbar.index;
scrollData.handle = $(this);
$(window).data('disableScrollHandler', true);
$('body').css('cursor', 'move');
});
// When the mouse moves,
$(document)
.on('mousemove', scrollData, this.mouseWasMoved)
.on('mouseup', scrollData, this.mouseWasReleased);
// When any part of the whole scrollbar is clicked, we want to jump to
// that position.
this.scrollbar.$.click(function(e) {
// Calculate the index which we want to jump to.
// @todo document how this complexity works.
var offsetPixels = e.clientY - view.scrollbar.$.offset().top + $('body').scrollTop();
var offsetPercent = offsetPixels / view.scrollbar.$.outerHeight() * 100;
var handleHeight = parseFloat(view.scrollbar.$.find('.scrollbar-slider')[0].style.height);
var offsetIndex = (offsetPercent - handleHeight / 2) / view.scrollbar.percentPerPost().index;
var newIndex = Math.max(0, Math.min(view.scrollbar.count - 1, offsetIndex));
view.get('controller').send('jumpToIndex', Math.floor(newIndex));
})
// Exempt the scrollbar handle from this 'jump to' click event.
this.scrollbar.$.find('.scrollbar-slider').click(function(e) {
e.stopPropagation();
});
},
actions: {
firstPost: function() {
this.get('controller').send('jumpToIndex', 0);
},
lastPost: function() {
this.get('controller').send('jumpToIndex', this.scrollbar.count - 1);
}
},
loadingIndex: function(index) {
this.scrollToIndex(index, true);
},
// Instantly scroll to a certain index in the discussion. The index doesn't
// have to be an integer; any fraction of a post will be scrolled to.
scrollToIndex: function(index, animate) {
index = Math.max(0, Math.min(index, this.scrollbar.count - 1));
var indexFloor = Math.floor(index);
// Find
var nearestItem = this.findNearestToIndex(indexFloor);
var first = $('.posts .item[data-start=0]');
var offsetTop = first.length ? first.offset().top : 0;
var pos = nearestItem.offset().top - offsetTop;
if (! nearestItem.is('.gap')) {
pos += nearestItem.outerHeight() * (index - indexFloor);
} else {
nearestItem.addClass('active');
}
$('.posts .item.gap').not(nearestItem).removeClass('active');
if (animate) {
// $('html, body').animate({scrollTop: pos});
} else {
$('html, body').scrollTop(pos);
}
this.scrollbar.setIndex(index);
this.scrollbar.update(animate);
},
willDestroyElement: function() {
$(window)
.off('resize', this.windowWasResized)
.off('scroll', this.windowWasScrolled);
$(document)
.off('mousemove', this.mouseWasMoved)
.off('mouseup', this.mouseWasReleased);
this.get('controller').off('loadingIndex', this, this.loadingIndex);
}
});