FEATURE: show timeline component when expanding post progress

- Show fullscreen timeline with title of topic in mobile
- Go to post # kb shortcut now unconditionally uses a modal
- Always show wrench on topics (was missing if progress bar was showing)
- Be smarter about rendering timeline even if composer is open (provided there is room)
This commit is contained in:
Sam 2016-10-19 14:29:43 +11:00
parent bd4f07b721
commit 1bf0b2a5f4
12 changed files with 222 additions and 163 deletions

View File

@ -1,17 +1,65 @@
import { observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
composerOpen: null, composerOpen: null,
classNameBindings: ['composerOpen'], info: Em.Object.create(),
showTimeline: null,
info: null,
_checkSize() { _checkSize() {
const renderTimeline = $(window).width() > 960; let info = this.get('info');
this.set('info', { renderTimeline, showTimeline: renderTimeline && !this.get('composerOpen') });
if (info.get('topicProgressExpanded')) {
info.setProperties({
renderTimeline: true,
renderAdminMenuButton: true
});
} else {
let renderTimeline = !this.site.mobileView;
if (renderTimeline) {
const width = $(window).width();
let height = $(window).height();
if (this.get('composerOpen')) {
height -= $('#reply-control').height();
}
renderTimeline = width > 960 && height > 520;
}
info.setProperties({
renderTimeline,
renderAdminMenuButton: !renderTimeline
});
}
},
// we need to store this so topic progress has something to init with
_topicScrolled(event) {
this.set('info.prevEvent', event);
},
@observes('info.topicProgressExpanded')
_expanded() {
if (this.get('info.topicProgressExpanded')) {
$(window).on('click.hide-fullscreen', (e) => {
if (!$(e.target).parents().is('.timeline-container, #topic-progress-wrapper')) {
this._collapseFullscreen();
}
});
} else {
$(window).off('click.hide-fullscreen');
}
this._checkSize();
}, },
composerOpened() { composerOpened() {
this.set('composerOpen', true); this.set('composerOpen', true);
this._checkSize(); // we need to do the check after animation is done
setTimeout(()=>this._checkSize(), 500);
}, },
composerClosed() { composerClosed() {
@ -19,25 +67,66 @@ export default Ember.Component.extend({
this._checkSize(); this._checkSize();
}, },
_collapseFullscreen() {
if (this.get('info.topicProgressExpanded')) {
$('.timeline-fullscreen').removeClass('show');
setTimeout(() => {
this.set('info.topicProgressExpanded', false);
this._checkSize();
},500);
}
},
keyboardTrigger(e) {
if(e.type === "jump") {
bootbox.prompt(I18n.t('topic.progress.jump_prompt_long'), postIndex => {
if (postIndex === null) { return; }
this.sendAction('jumpToIndex', postIndex);
});
// this is insanely hacky, for some reason shown event never fires,
// something is bust in bootbox
// TODO upgrade bootbox to see if this hack can be removed
setTimeout(()=>{
$('.bootbox.modal').trigger('shown');
},50);
}
},
didInsertElement() { didInsertElement() {
this._super(); this._super();
this.appEvents
.on('topic:current-post-scrolled', this, this._topicScrolled)
.on('topic:jump-to-post', this, this._collapseFullscreen)
.on('topic:keyboard-trigger', this, this.keyboardTrigger);
if (!this.site.mobileView) { if (!this.site.mobileView) {
$(window).on('resize.discourse-topic-navigation', () => this._checkSize()); $(window).on('resize.discourse-topic-navigation', () => this._checkSize());
this.appEvents.on('composer:will-open', this, this.composerOpened); this.appEvents.on('composer:will-open', this, this.composerOpened);
this.appEvents.on('composer:will-close', this, this.composerClosed); this.appEvents.on('composer:will-close', this, this.composerClosed);
this._checkSize(); $('#reply-control').on('div-resized.discourse-topic-navigation', () => this._checkSize());
} else {
this.set('info', null);
} }
this._checkSize();
}, },
willDestroyElement() { willDestroyElement() {
this._super(); this._super();
this.appEvents
.off('topic:current-post-scrolled', this, this._topicScrolled)
.off('topic:jump-to-post', this, this._collapseFullscreen)
.off('topic:keyboard-trigger', this, this.keyboardTrigger);
$(window).off('click.hide-fullscreen');
if (!this.site.mobileView) { if (!this.site.mobileView) {
$(window).off('resize.discourse-topic-navigation'); $(window).off('resize.discourse-topic-navigation');
this.appEvents.off('composer:will-open', this, this.composerOpened); this.appEvents.off('composer:will-open', this, this.composerOpened);
this.appEvents.off('composer:will-close', this, this.composerClosed); this.appEvents.off('composer:will-close', this, this.composerClosed);
$('#reply-control').off('div-resized.discourse-topic-navigation');
} }
} }
}); });

View File

@ -2,13 +2,11 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor
export default Ember.Component.extend({ export default Ember.Component.extend({
elementId: 'topic-progress-wrapper', elementId: 'topic-progress-wrapper',
classNameBindings: ['docked', 'hidden'], classNameBindings: ['docked'],
expanded: false, expanded: false,
toPostIndex: null,
docked: false, docked: false,
progressPosition: null, progressPosition: null,
postStream: Ember.computed.alias('topic.postStream'), postStream: Ember.computed.alias('topic.postStream'),
userWantsToJump: null,
_streamPercentage: null, _streamPercentage: null,
init() { init() {
@ -16,26 +14,6 @@ export default Ember.Component.extend({
(this.get('delegated') || []).forEach(m => this.set(m, m)); (this.get('delegated') || []).forEach(m => this.set(m, m));
}, },
@computed('userWantsToJump', 'showTimeline')
hidden(userWantsToJump, showTimeline) {
return !userWantsToJump && showTimeline;
},
@observes('hidden')
visibilityChanged() {
if (!this.get('hidden')) {
this._updateBar();
}
},
keyboardTrigger(kbdEvent) {
if (kbdEvent.type === 'jump') {
this.set('expanded', true);
this.set('userWantsToJump', true);
Ember.run.scheduleOnce('afterRender', () => this.$('.jump-form input').focus());
}
},
@computed('progressPosition') @computed('progressPosition')
jumpTopDisabled(progressPosition) { jumpTopDisabled(progressPosition) {
return progressPosition <= 3; return progressPosition <= 3;
@ -83,10 +61,15 @@ export default Ember.Component.extend({
.on("composer:resized", this, this._dock) .on("composer:resized", this, this._dock)
.on('composer:closed', this, this._dock) .on('composer:closed', this, this._dock)
.on("topic:scrolled", this, this._dock) .on("topic:scrolled", this, this._dock)
.on('topic:current-post-scrolled', this, this._topicScrolled) .on('topic:current-post-scrolled', this, this._topicScrolled);
.on('topic-progress:keyboard-trigger', this, this.keyboardTrigger);
const prevEvent = this.get('prevEvent');
if (prevEvent) {
this._topicScrolled(prevEvent);
} else {
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar); Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
}
Ember.run.scheduleOnce('afterRender', this, this._dock);
}, },
willDestroyElement() { willDestroyElement() {
@ -95,12 +78,11 @@ export default Ember.Component.extend({
.off("composer:resized", this, this._dock) .off("composer:resized", this, this._dock)
.off('composer:closed', this, this._dock) .off('composer:closed', this, this._dock)
.off('topic:scrolled', this, this._dock) .off('topic:scrolled', this, this._dock)
.off('topic:current-post-scrolled', this, this._topicScrolled) .off('topic:current-post-scrolled', this, this._topicScrolled);
.off('topic-progress:keyboard-trigger');
}, },
_updateProgressBar() { _updateProgressBar() {
if (this.isDestroyed || this.isDestroying || this.get('hidden')) { return; } if (this.isDestroyed || this.isDestroying) { return; }
const $topicProgress = this.$('#topic-progress'); const $topicProgress = this.$('#topic-progress');
// speeds up stuff, bypass jquery slowness and extra checks // speeds up stuff, bypass jquery slowness and extra checks
@ -127,6 +109,10 @@ export default Ember.Component.extend({
offset = window.pageYOffset || $('html').scrollTop(), offset = window.pageYOffset || $('html').scrollTop(),
topicProgressHeight = $('#topic-progress').height(); topicProgressHeight = $('#topic-progress').height();
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) {
return;
}
let isDocked = false; let isDocked = false;
if (maximumOffset) { if (maximumOffset) {
const threshold = maximumOffset.top; const threshold = maximumOffset.top;
@ -156,71 +142,11 @@ export default Ember.Component.extend({
} }
}, },
keyDown(e) {
if (this.get('expanded')) {
if (e.keyCode === 13) {
this.$('input').blur();
this.send('jumpPost');
} else if (e.keyCode === 27) {
this.send('toggleExpansion');
this.set('userWantsToJump', false);
}
}
},
_jumpTo(postIndex) {
postIndex = parseInt(postIndex, 10);
// Validate the post index first
if (isNaN(postIndex) || postIndex < 1) {
postIndex = 1;
}
if (postIndex > this.get('postStream.filteredPostsCount')) {
postIndex = this.get('postStream.filteredPostsCount');
}
this.set('toPostIndex', postIndex);
this._beforeJump();
this.sendAction('jumpToIndex', postIndex);
},
actions: { actions: {
toggleExpansion(opts) { toggleExpansion() {
this.toggleProperty('expanded'); this.toggleProperty('expanded');
if (this.get('expanded')) {
this.set('userWantsToJump', false);
this.set('toPostIndex', this.get('progressPosition'));
if (opts && opts.highlight) {
Ember.run.next(() => $('.jump-form input').select().focus());
}
if (!this.site.mobileView && !this.capabilities.isIOS) {
Ember.run.schedule('afterRender', () => this.$('input').focus());
}
} }
}, },
jumpPrompt() {
const postIndex = prompt(I18n.t('topic.progress.jump_prompt_long'));
if (postIndex === null) { return; }
this._jumpTo(postIndex);
},
jumpPost() {
this._jumpTo(this.get('toPostIndex'));
},
jumpTop() {
this._beforeJump();
this.sendAction('jumpTop');
},
jumpBottom() {
this._beforeJump();
this.sendAction('jumpBottom');
}
},
_beforeJump() {
this.set('expanded', false);
this.set('userWantsToJump', false);
}
}); });

View File

@ -10,12 +10,28 @@ export default MountWidget.extend(Docking, {
dockAt: null, dockAt: null,
buildArgs() { buildArgs() {
return { topic: this.get('topic'), let attrs = {
topic: this.get('topic'),
topicTrackingState: this.topicTrackingState, topicTrackingState: this.topicTrackingState,
enteredIndex: this.get('enteredIndex'), enteredIndex: this.get('enteredIndex'),
dockAt: this.dockAt, dockBottom: this.dockBottom,
top: this.dockAt || FIXED_POS, mobileView: this.get('site.mobileView')
dockBottom: this.dockBottom }; };
let event = this.get('prevEvent');
if (event) {
attrs.enteredIndex = event.postIndex-1;
}
if (this.get('fullscreen')) {
attrs.fullScreen = true;
attrs.addShowClass = this.get('addShowClass');
} else {
attrs.dockAt = this.dockAt;
attrs.top = this.dockAt || FIXED_POS;
}
return attrs;
}, },
@observes('topic.highest_post_number', 'loading') @observes('topic.highest_post_number', 'loading')
@ -54,6 +70,14 @@ export default MountWidget.extend(Docking, {
didInsertElement() { didInsertElement() {
this._super(); this._super();
if (this.get('fullscreen') && !this.get('addShowClass')) {
Em.run.next(()=>{
this.set('addShowClass', true);
this.queueRerender();
});
}
this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea'); this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea');
this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button'); this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button');
} }

View File

@ -670,6 +670,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return; return;
} }
this.appEvents.trigger('topic:jump-to-post', postId);
const topic = this.get('model'); const topic = this.get('model');
const postStream = topic.get('postStream'); const postStream = topic.get('postStream');
const post = postStream.findLoadedPost(postId); const post = postStream.findLoadedPost(postId);

View File

@ -4,7 +4,7 @@ import { scrollTopFor } from 'discourse/lib/offset-calculator';
const bindings = { const bindings = {
'!': {postAction: 'showFlags'}, '!': {postAction: 'showFlags'},
'#': {handler: 'toggleProgress', anonymous: true}, '#': {handler: 'goToPost', anonymous: true},
'/': {handler: 'toggleSearch', anonymous: true}, '/': {handler: 'toggleSearch', anonymous: true},
'=': {handler: 'toggleHamburgerMenu', anonymous: true}, '=': {handler: 'toggleHamburgerMenu', anonymous: true},
'?': {handler: 'showHelpModal', anonymous: true}, '?': {handler: 'showHelpModal', anonymous: true},
@ -170,8 +170,8 @@ export default {
this.container.lookup('controller:topic').togglePinnedState(); this.container.lookup('controller:topic').togglePinnedState();
}, },
toggleProgress() { goToPost() {
this.appEvents.trigger('topic-progress:keyboard-trigger', { type: 'jump' }); this.appEvents.trigger('topic:keyboard-trigger', { type: 'jump' });
}, },
toggleSearch(event) { toggleSearch(event) {

View File

@ -1,33 +1,8 @@
{{#unless hidden}} <nav id='topic-progress' title="{{i18n 'topic.progress.title'}}" class="{{if hideProgress 'hidden'}}">
{{#if expanded}}
<nav id='topic-progress-expanded'>
{{d-button action="jumpTop"
disabled=jumpTopDisabled
class="full no-text"
icon="caret-up"
label="topic.progress.go_top"}}
{{#unless capabilities.isIOS}}
<div class='jump-form'>
{{input value=toPostIndex}}
{{d-button action="jumpPost" label="topic.progress.go"}}
</div>
{{else}}
{{d-button action="jumpPrompt" class="full jump-prompt" label="topic.progress.jump_prompt"}}
{{/unless}}
{{d-button action="jumpBottom"
disabled=jumpBottomDisabled
class="full no-text jump-bottom"
icon="caret-down"
label="topic.progress.go_bottom"}}
</nav>
{{/if}}
<nav id='topic-progress' title="{{i18n 'topic.progress.title'}}" class="{{if hideProgress 'hidden'}}">
<div class='nums'> <div class='nums'>
<h4>{{progressPosition}}</h4><span class="{{if hugeNumberOfPosts 'hidden'}}"> <h4>{{progressPosition}}</h4><span class="{{if hugeNumberOfPosts 'hidden'}}">
<span>/</span> <span>/</span>
<h4>{{postStream.filteredPostsCount}}</h4></span> <h4>{{postStream.filteredPostsCount}}</h4></span>
</div> </div>
<i class="fa {{unless expanded 'fa-sort'}}"></i> <i class="fa {{unless expanded 'fa-sort'}}"></i>
</nav> </nav>
{{/unless}}

View File

@ -66,17 +66,26 @@
<div class='selected-posts {{unless multiSelect 'hidden'}}'> <div class='selected-posts {{unless multiSelect 'hidden'}}'>
{{partial "selected-posts"}} {{partial "selected-posts"}}
</div> </div>
{{#topic-navigation as |info|}}
{{#if info.renderTimeline}} {{#topic-navigation jumpToIndex="jumpToIndex" as |info|}}
{{topic-timeline topic=model
enteredIndex=enteredIndex {{#if info.renderAdminMenuButton}}
loading=model.postStream.loading
delegated=topicDelegated}}
{{else}}
{{topic-admin-menu-button topic=model fixed="true" delegated=topicDelegated}} {{topic-admin-menu-button topic=model fixed="true" delegated=topicDelegated}}
{{/if}} {{/if}}
{{topic-progress topic=model delegated=topicDelegated showTimeline=info.showTimeline}} {{#if info.renderTimeline}}
{{topic-timeline topic=model
prevEvent=info.prevEvent
fullscreen=info.topicProgressExpanded
enteredIndex=enteredIndex
loading=model.postStream.loading
delegated=topicDelegated}}
{{else}}
{{topic-progress prevEvent=info.prevEvent topic=model delegated=topicDelegated expanded=info.topicProgressExpanded}}
{{/if}}
{{/topic-navigation}} {{/topic-navigation}}
<div class="row"> <div class="row">

View File

@ -2,6 +2,7 @@ import { createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom'; import { h } from 'virtual-dom';
import { relativeAge } from 'discourse/lib/formatter'; import { relativeAge } from 'discourse/lib/formatter';
import { iconNode } from 'discourse/helpers/fa-icon-node'; import { iconNode } from 'discourse/helpers/fa-icon-node';
import RawHtml from 'discourse/widgets/raw-html';
const SCROLLAREA_HEIGHT = 300; const SCROLLAREA_HEIGHT = 300;
const SCROLLER_HEIGHT = 50; const SCROLLER_HEIGHT = 50;
@ -182,6 +183,13 @@ createWidget('timeline-scrollarea', {
createWidget('topic-timeline-container', { createWidget('topic-timeline-container', {
tagName: 'div.timeline-container', tagName: 'div.timeline-container',
buildClasses(attrs) { buildClasses(attrs) {
if (attrs.fullScreen) {
if (attrs.addShowClass) {
return 'timeline-fullscreen show';
} else {
return 'timeline-fullscreen';
}
}
if (attrs.dockAt) { if (attrs.dockAt) {
const result = ['timeline-docked']; const result = ['timeline-docked'];
if (attrs.dockBottom) { if (attrs.dockBottom) {
@ -192,7 +200,9 @@ createWidget('topic-timeline-container', {
}, },
buildAttributes(attrs) { buildAttributes(attrs) {
if (attrs.top) {
return { style: `top: ${attrs.top}px` }; return { style: `top: ${attrs.top}px` };
}
}, },
html(attrs) { html(attrs) {
@ -209,9 +219,15 @@ export default createWidget('topic-timeline', {
const stream = attrs.topic.get('postStream.stream'); const stream = attrs.topic.get('postStream.stream');
const { currentUser } = this; const { currentUser } = this;
let result = []; let result = [];
if (currentUser && currentUser.get('canManageTopic')) {
if (attrs.mobileView) {
const titleHTML = new RawHtml({ html: `<span>${topic.get('fancyTitle')}</span>` });
result.push(h('h3.title', titleHTML));
}
if (!attrs.fullScreen && currentUser && currentUser.get('canManageTopic')) {
result.push(h('div.timeline-controls', this.attach('topic-admin-menu-button', { topic }))); result.push(h('div.timeline-controls', this.attach('topic-admin-menu-button', { topic })));
} }
@ -234,7 +250,7 @@ export default createWidget('topic-timeline', {
if (currentUser) { if (currentUser) {
const controls = []; const controls = [];
if (attrs.topic.get('details.can_create_post')) { if (!attrs.fullScreen && attrs.topic.get('details.can_create_post')) {
controls.push(this.attach('button', { controls.push(this.attach('button', {
className: 'btn create', className: 'btn create',
icon: 'reply', icon: 'reply',
@ -243,7 +259,7 @@ export default createWidget('topic-timeline', {
})); }));
} }
if (currentUser) { if (currentUser && !attrs.fullScreen) {
controls.push(this.attach('topic-notifications-button', { topic })); controls.push(this.attach('topic-notifications-button', { topic }));
} }
result.push(h('div.timeline-footer-controls', controls)); result.push(h('div.timeline-footer-controls', controls));

View File

@ -12,3 +12,4 @@
@import "common/printer-friendly"; @import "common/printer-friendly";
@import "common/base/*"; @import "common/base/*";
@import "common/d-editor"; @import "common/d-editor";
@import "common/topic-timeline";

View File

@ -2,14 +2,6 @@
width: 900px; width: 900px;
} }
.composer-open {
.topic-timeline {
opacity: 0;
pointer-events: none;
cursor: default;
}
}
.timeline-container { .timeline-container {
box-sizing: border-box; box-sizing: border-box;
z-index: 499; z-index: 499;
@ -37,6 +29,31 @@
} }
} }
&.timeline-fullscreen.show {
max-height: 700px;
transition: max-height 0.4s ease-out;
}
&.timeline-fullscreen {
max-height: 0;
transition: max-height 0.3s ease-in;
position: fixed;
margin-left: 0;
background-color: $secondary;
bottom: 0;
left: 0;
right: 0;
border-top: 1px solid dark-light-choose(scale-color($primary, $lightness: 90%), scale-color($secondary, $lightness: 90%));
padding-top: 15px;
z-index: 100000;
.topic-timeline {
width: auto;
margin-left: 1.5em;
margin-right: 1.5em;
}
}
.topic-timeline { .topic-timeline {
margin-left: 3em; margin-left: 3em;
width: 150px; width: 150px;

View File

@ -13,7 +13,6 @@
@import "desktop/category-list"; @import "desktop/category-list";
@import "desktop/topic-list"; @import "desktop/topic-list";
@import "desktop/topic-post"; @import "desktop/topic-post";
@import "desktop/topic-timeline";
@import "desktop/topic"; @import "desktop/topic";
@import "desktop/upload"; @import "desktop/upload";
@import "desktop/user"; @import "desktop/user";

View File

@ -67,6 +67,7 @@ endDrag = function(e, opts) {
if (typeof opts.resize === "function") { if (typeof opts.resize === "function") {
opts.resize(); opts.resize();
} }
$(div).trigger("div-resized");
div = null; div = null;
}; };