mirror of
https://github.com/flarum/framework.git
synced 2024-12-11 13:05:50 +08:00
238 lines
9.9 KiB
JavaScript
238 lines
9.9 KiB
JavaScript
|
import Ember from 'ember';
|
||
|
|
||
|
import Menu from '../utils/menu';
|
||
|
import MenuItem from '../components/menu-item';
|
||
|
import PostStreamMixin from '../mixins/post-stream';
|
||
|
|
||
|
export default Ember.View.extend(Ember.Evented, PostStreamMixin, {
|
||
|
|
||
|
// Set up a new menu view that will contain controls to be shown in the
|
||
|
// footer. The template will only render these controls if the last post is
|
||
|
// showing.
|
||
|
construct: function() {
|
||
|
// this.set('footerControls', this.createChildView(Menu));
|
||
|
// this.set('footerControls.controller', this.get('controller'));
|
||
|
// console.log(this.get('controller'));
|
||
|
}.on('init'),
|
||
|
|
||
|
// Whenever the model's title changes, we want to update that document's
|
||
|
// title the reflect the new title.
|
||
|
updateTitle: function() {
|
||
|
this.set('controller.controllers.application.pageTitle', this.get('controller.model.title'));
|
||
|
}.observes('controller.model.title'),
|
||
|
|
||
|
didInsertElement: function() {
|
||
|
|
||
|
this.set('footerControls', Menu.create());
|
||
|
|
||
|
// We've just inserted the discussion view. Let's start off by
|
||
|
// populating the footer controls menu object.
|
||
|
this.trigger('populateControls', this.get('footerControls'));
|
||
|
|
||
|
// Whenever the window's scroll position changes, we want to check to
|
||
|
// see if any terminal 'gaps' are in the viewport and trigger their
|
||
|
// loading mechanism if they are. We also want to update the
|
||
|
// controller's 'start' query param with the current position.
|
||
|
$(window).on('scroll', {view: this}, this.windowWasScrolled);
|
||
|
|
||
|
// We need to listen for some events on the controller. Whenever the
|
||
|
// controller says that it's loading or has loaded posts near a certain
|
||
|
// post number, we want to scroll down to this post (or the gap which
|
||
|
// the post is in) and highlight it.
|
||
|
var controller = this.get('controller');
|
||
|
controller.on('loadingNumber', this, this.loadingNumber);
|
||
|
controller.on('loadedNumber', this, this.loadedNumber);
|
||
|
controller.on('loadingIndex', this, this.loadingIndex);
|
||
|
controller.on('loadedIndex', this, this.loadedIndex);
|
||
|
},
|
||
|
|
||
|
willDestroyElement: function() {
|
||
|
$(window).off('scroll', this.windowWasScrolled);
|
||
|
|
||
|
var controller = this.get('controller');
|
||
|
controller.off('loadingNumber', this, this.loadingNumber);
|
||
|
controller.off('loadedNumber', this, this.loadedNumber);
|
||
|
controller.off('loadingIndex', this, this.loadingIndex);
|
||
|
controller.off('loadedIndex', this, this.loadedIndex);
|
||
|
},
|
||
|
|
||
|
// By default, we just populate the footer controls with a 'reply' button.
|
||
|
addDefaultControls: function(controls) {
|
||
|
var view = this;
|
||
|
var ReplyItem = MenuItem.extend({
|
||
|
title: 'Reply',
|
||
|
icon: 'reply',
|
||
|
className: 'btn btn-primary',
|
||
|
classNameBindings: ['className', 'replying:disabled'],
|
||
|
replying: function() {
|
||
|
return this.get('parentController.controllers.composer.showing');
|
||
|
}.property('parentController.controllers.composer.showing'),
|
||
|
action: function() {
|
||
|
var lastPost = $('.posts .item:last');
|
||
|
$('html, body').animate({scrollTop: lastPost.offset().top + lastPost.outerHeight() - $(window).height() + $('.composer').height() + 19}, 'fast');
|
||
|
view.get('controller').send('reply');
|
||
|
},
|
||
|
parentController: this.get('controller'),
|
||
|
});
|
||
|
controls.addItem('reply', ReplyItem);
|
||
|
}.on('populateControls'),
|
||
|
|
||
|
// This function handles the window's scroll event. We check to see if any
|
||
|
// terminal 'gaps' are in the viewport and trigger their loading mechanism
|
||
|
// if they are. We also update the controller's 'start' query param with the
|
||
|
// current position.
|
||
|
windowWasScrolled: function(event) {
|
||
|
var view = event.data.view;
|
||
|
|
||
|
if (! view.get('controller.loaded') || $(window).data('disableScrollHandler')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var posts = view.$().find('.posts'),
|
||
|
$this = $(this),
|
||
|
scrollTop = $this.scrollTop(),
|
||
|
viewportHeight = $this.height(),
|
||
|
firstItem = posts.find('.item[data-start=0]'),
|
||
|
firstItemOffset = firstItem.length ? firstItem.offset().top : 0,
|
||
|
currentNumber;
|
||
|
|
||
|
// 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.
|
||
|
posts.find('.item').each(function() {
|
||
|
var $this = $(this),
|
||
|
top = $this.offset().top - firstItemOffset,
|
||
|
height = $this.outerHeight();
|
||
|
|
||
|
// If this item is above the top of the viewport, skip to the
|
||
|
// next one. If it's below the bottom of the viewport, break
|
||
|
// out of the loop.
|
||
|
if (top + height < scrollTop) {
|
||
|
return;
|
||
|
}
|
||
|
if (top > scrollTop + viewportHeight) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Now we know that this item is in the viewport. If we haven't
|
||
|
// yet stored a post's number, then this item must be the FIRST
|
||
|
// item in the viewport. Therefore, we'll grab its post number
|
||
|
// so we can update the controller's state later.
|
||
|
! currentNumber && (currentNumber = $this.data('number'));
|
||
|
|
||
|
// If this item is a gap, then we may proceed to check if it's
|
||
|
// a *terminal* gap and trigger its loading mechanism.
|
||
|
var gapView;
|
||
|
if ($this.hasClass('gap') && (gapView = Ember.View.views[$this.attr('id')])) {
|
||
|
if ($this.is(':first-of-type')) {
|
||
|
gapView.set('direction', 'up').load();
|
||
|
}
|
||
|
else if ($this.is(':last-of-type')) {
|
||
|
gapView.set('direction', 'down').load();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Finally, we want to update the controller's state with regards to the
|
||
|
// current viewing position of the discussion. However, we don't want to
|
||
|
// do this on every single scroll event as it will slow things down. So,
|
||
|
// let's do it at a minimum of 250ms by clearing and setting a timeout.
|
||
|
clearTimeout(this.updateStateTimeout);
|
||
|
this.updateStateTimeout = setTimeout(function() {
|
||
|
view.get('controller').set('start', currentNumber || 1);
|
||
|
}, 250);
|
||
|
},
|
||
|
|
||
|
loadingNumber: function(number) {
|
||
|
// The post with this number is being loaded. We want to scroll to where
|
||
|
// we think it will appear. We may be scrolling to the edge of the page,
|
||
|
// but we don't want to trigger any terminal post gaps to load by doing
|
||
|
// that. So, we'll disable the window's scroll handler for now.
|
||
|
$(window).data('disableScrollHandler', true);
|
||
|
|
||
|
this.jumpToNumber(number);
|
||
|
},
|
||
|
|
||
|
loadedNumber: function(number) {
|
||
|
// The post with this number has been loaded. After we scroll to this
|
||
|
// post, we want to resume scroll events.
|
||
|
var view = this;
|
||
|
this.jumpToNumber(number, function() {
|
||
|
$(window).data('disableScrollHandler', false).scroll();
|
||
|
});
|
||
|
},
|
||
|
|
||
|
// Scroll down to a certain post (or the gap which the post is in) and
|
||
|
// highlight it.
|
||
|
jumpToNumber: function(number, finish) {
|
||
|
// Clear the highlight class from all posts, and attempt to find and
|
||
|
// highlight a post with the specified number.
|
||
|
var item = this.$()
|
||
|
.find('.posts .item').removeClass('highlight')
|
||
|
.filter('[data-number='+number+']');
|
||
|
|
||
|
if (number > 1) {
|
||
|
item.addClass('highlight');
|
||
|
}
|
||
|
|
||
|
// If we didn't have any luck, then a post with this number either
|
||
|
// doesn't exist, or it hasn't been loaded yet. We'll find the item
|
||
|
// that's closest to the post with this number and scroll to that
|
||
|
// instead.
|
||
|
if (! item.length) {
|
||
|
item = this.findNearestToNumber(number);
|
||
|
}
|
||
|
|
||
|
// We have an item to scroll to now. Let's get its position and animate
|
||
|
// a scroll-down!
|
||
|
if (item.length) {
|
||
|
$('html, body').stop(true).animate({scrollTop: number > 1 ? item.offset().top : 0});
|
||
|
}
|
||
|
if (finish) {
|
||
|
$('html, body').promise().done(finish);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
loadingIndex: function(index) {
|
||
|
// The post at this index is being loaded. We want to scroll to where we
|
||
|
// think it will appear. We may be scrolling to the edge of the page,
|
||
|
// but we don't want to trigger any terminal post gaps to load by doing
|
||
|
// that. So, we'll disable the window's scroll handler for now.
|
||
|
$(window).data('disableScrollHandler', true);
|
||
|
|
||
|
this.jumpToIndex(index);
|
||
|
},
|
||
|
|
||
|
loadedIndex: function(index) {
|
||
|
// The post at this index has been loaded. After we scroll to this post,
|
||
|
// we want to resume scroll events.
|
||
|
var view = this;
|
||
|
this.jumpToIndex(index, function() {
|
||
|
$(window).data('disableScrollHandler', false).scroll();
|
||
|
});
|
||
|
},
|
||
|
|
||
|
jumpToIndex: function(index, finish) {
|
||
|
var item = this.findNearestToIndex(index);
|
||
|
|
||
|
// We have an item to scroll to now. Let's get its position and animate
|
||
|
// a scroll-down!
|
||
|
if (item.length) {
|
||
|
$('html, body').stop(true).animate({scrollTop: index > 0 ? item.offset().top : 0});
|
||
|
}
|
||
|
if (finish) {
|
||
|
$('html, body').promise().done(finish);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// 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() {
|
||
|
if (this.get('controller.loaded')) {
|
||
|
Ember.run.scheduleOnce('afterRender', function() {
|
||
|
$(window).scroll();
|
||
|
});
|
||
|
}
|
||
|
}.observes('controller.loaded')
|
||
|
});
|