1
0
mirror of https://github.com/flarum/core.git synced 2025-08-11 19:04:29 +02:00

Rejigging templates for redesign.

- Componentize more stuff, heading in the direction of Ember 2.0
- Start CSS from scratch
- Everything is broken atm!
This commit is contained in:
Toby Zerner
2015-01-01 20:08:38 +10:30
parent dc74288915
commit 690174297d
38 changed files with 181 additions and 2981 deletions

View File

@@ -0,0 +1,132 @@
import Ember from 'ember';
// A discussion 'item' represents one item in the post stream. In other words, a
// single item may represent a single post, or it may represent a gap of many
// posts which have not been loaded.
export default Ember.View.extend({
classNames: ['item'],
classNameBindings: ['item.gap:gap', 'loading', 'direction'],
attributeBindings: [
'start:data-start',
'end:data-end',
'time:data-time',
'number:data-number'
],
start: function() {
return this.get('item.indexStart');
}.property('item.indexStart'),
end: function() {
return this.get('item.indexEnd');
}.property('item.indexEnd'),
count: function() {
return this.get('end') - this.get('start') + 1;
}.property('start', 'end'),
time: function() {
return this.get('item.post.time');
}.property('item.post.time'),
number: function() {
return this.get('item.post.number');
}.property('item.post.number'),
loading: function() {
return this.get('item.loading');
}.property('item.loading'),
direction: function() {
return this.get('item.direction');
}.property(),
loadingChanged: function() {
this.rerender();
}.observes('loading'),
render: function(buffer) {
if (! this.get('item.gap')) {
return this._super(buffer);
}
buffer.push('<span>');
if (this.get('loading')) {
buffer.push('&nbsp;');
} else {
buffer.push(this.get('count')+' more post'+(this.get('count') != 1 ? 's' : ''));
}
buffer.push('</span>');
},
didInsertElement: function() {
if (! this.get('item.gap')) {
return;
}
if (this.get('loading')) {
var view = this;
Ember.run.scheduleOnce('afterRender', function() {
view.$().spin('small');
});
} else {
var self = this;
this.$().hover(function(e) {
var up = e.clientY > $(this).offset().top - $(document).scrollTop() + $(this).outerHeight(true) / 2;
self.set('direction', up ? 'up' : 'down');
});
}
},
load: function(relativeIndex) {
// If this item is not a gap, or if we're already loading its posts,
// then we don't need to do anything.
if (! this.get('item.gap') || this.get('loading')) {
return false;
}
// If new posts are being loaded in an upwards direction, then when they
// are rendered, the rest of the posts will be pushed down the page.
// However, we want to maintain the current scroll position relative to
// the content after the gap. To do this, we need to find item directly
// after the gap and use it as an anchor.
if (this.get('direction') === 'up') {
var anchor = this.$().nextAll('.item:first');
// Immediately after the posts have been loaded (but before they
// have been rendered,) we want to grab the distance from the top of
// the viewport to the top of the anchor element.
this.get('controller.postStream').one('postsLoaded', function() {
if (anchor.length) {
var scrollOffset = anchor.offset().top - $(document).scrollTop();
}
// After they have been rendered, we scroll back to a position
// so that the distance from the top of the viewport to the top
// of the anchor element is the same as before. If there is no
// anchor (i.e. this gap is terminal,) then we'll scroll to the
// bottom of the document.
Ember.run.scheduleOnce('afterRender', function() {
$('body').scrollTop(anchor.length
? anchor.offset().top - scrollOffset
: $('body').height());
});
});
}
// Tell the controller that we want to load the range of posts that this
// gap represents. We also specify which direction we want to load the
// posts from.
this.get('controller').send(
'loadRange',
this.get('start') + (relativeIndex || 0),
this.get('end'),
this.get('direction') === 'up'
);
},
click: function() {
this.load();
}
});

View File

@@ -0,0 +1,55 @@
import Ember from 'ember';
import Menu from '../utils/menu';
import MenuItem from '../components/menu-item';
export default Ember.View.extend({
tagName: 'article',
templateName: 'discussion-post',
controls: null,
contentComponent: function() {
return 'post-type-'+this.get('post.type');
}.property('post.type'),
classNames: ['post'],
classNameBindings: ['post.deleted', 'post.edited'],
construct: function() {
this.set('controls', Menu.create());
var post = this.get('post');
if (post.get('deleted')) {
this.addControl('restore', 'Restore', 'reply', 'canEdit');
this.addControl('delete', 'Delete', 'times', 'canDelete');
} else {
if (post.get('type') == 'comment') {
this.addControl('edit', 'Edit', 'pencil', 'canEdit');
this.addControl('hide', 'Delete', 'times', 'canEdit');
} else {
this.addControl('delete', 'Delete', 'times', 'canDelete');
}
}
}.on('init'),
didInsertElement: function() {
this.$().hide().fadeIn('slow');
},
addControl: function(tag, title, icon, permissionAttribute) {
if (permissionAttribute && ! this.get('post').get(permissionAttribute)) {
return;
}
var self = this;
var action = function(post) {
self.get('controller').send(actionName, post);
};
var item = MenuItem.extend({title: title, icon: icon, action: action});
this.get('controls').addItem(tag, item);
}
});

View File

@@ -0,0 +1,280 @@
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: '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);
}
});

View File

@@ -0,0 +1,154 @@
import Ember from 'ember';
import Menu from '../utils/menu';
import MenuItem from '../components/menu-item';
export default Ember.View.extend({
_init: function() {
this.set('controls', Menu.create());
}.on('init'),
tagName: 'li',
attributeBindings: ['discussionId:data-id'],
classNameBindings: [
'discussion.unread:unread',
'discussion.sticky:sticky',
'discussion.locked:locked',
'discussion.following:following',
'active'
],
templateName: 'discussions-result',
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.@each.active'),
discussionId: function() {
return this.get('discussion.id');
}.property('discussion.id'),
relevantPosts: function() {
if (this.get('controller.show') != 'posts') return [];
if (this.get('controller.searchQuery')) {
return this.get('discussion.relevantPosts');
} else if (this.get('controller.sort') == 'newest' || this.get('controller.sort') == 'oldest') {
return [this.get('discussion.startPost')];
} else {
return [this.get('discussion.lastPost')];
}
}.property('discussion.relevantPosts', 'discussion.startPost', 'discussion.lastPost'),
icon: function() {
if (this.get('discussion.unread')) return 'circle';
}.property('discussion.unread'),
iconAction: function() {
if (this.get('discussion.unread')) return function() {
};
}.property('discussion.unread'),
categoryClass: function() {
return 'category-'+this.get('discussion.category').toLowerCase();
}.property('discussion.category'),
didInsertElement: function() {
this.$().hide().fadeIn('slow');
this.$().find('.terminal-post a').tooltip();
var view = this;
this.$().find('a.info, .terminal-post a').click(function() {
view.set('controller.paneShowing', false);
});
// https://github.com/nolimits4web/Framework7/blob/master/src/js/swipeout.js
this.$().find('.discussion').on('touchstart mousedown', function(e) {
var isMoved = false;
var isTouched = true;
var isScrolling = undefined;
var touchesStart = {
x: e.type === 'touchstart' ? e.originalEvent.targetTouches[0].pageX : e.pageX,
y: e.type === 'touchstart' ? e.originalEvent.targetTouches[0].pageY : e.pageY,
};
var touchStartTime = (new Date()).getTime();
$(this).on('touchmove mousemove', function(e) {
if (! isTouched) return;
$(this).find('a.info').removeClass('pressed');
var touchesNow = {
x: e.type === 'touchmove' ? e.originalEvent.targetTouches[0].pageX : e.pageX,
y: e.type === 'touchmove' ? e.originalEvent.targetTouches[0].pageY : e.pageY,
};
if (typeof isScrolling === 'undefined') {
isScrolling = !!(isScrolling || Math.abs(touchesNow.y - touchesStart.y) > Math.abs(touchesNow.x - touchesStart.x));
}
if (isScrolling) {
isTouched = false;
return;
}
isMoved = true;
e.preventDefault();
var diffX = touchesNow.x - touchesStart.x;
var translate = diffX;
var actionsRightWidth = 150;
if (translate < -actionsRightWidth) {
translate = -actionsRightWidth - Math.pow(-translate - actionsRightWidth, 0.8);
}
$(this).css('left', translate);
});
$(this).on('touchend mouseup', function(e) {
$(this).off('touchmove mousemove touchend mouseup');
$(this).find('a.info').removeClass('pressed');
if (!isTouched || !isMoved) {
isTouched = false;
isMoved = false;
return;
}
isTouched = false;
// isMoved = false;
if (isMoved) {
e.preventDefault();
$(this).animate({left: -150});
}
});
$(this).find('a.info').addClass('pressed').on('click', function(e) {
if (isMoved) {
e.preventDefault();
e.stopImmediatePropagation();
}
$(this).off('click');
});
});
var discussion = this.get('discussion');
var controls = this.get('controls');
controls.addItem('sticky', MenuItem.extend({title: 'Sticky', icon: 'thumb-tack', action: 'sticky'}));
controls.addItem('lock', MenuItem.extend({title: 'Lock', icon: 'lock', action: 'lock'}));
controls.addSeparator();
controls.addItem('delete', MenuItem.extend({title: 'Delete', icon: 'times', className: 'delete', action: function() {
// this.get('controller').send('delete', discussion);
var discussion = view.$().slideUp().find('.discussion');
discussion.css('position', 'relative').animate({left: -discussion.width()});
}}));
},
actions: {
icon: function() {
this.get('iconAction')();
}
}
});