diff --git a/framework/core/ember/app/components/back-button.js b/framework/core/ember/app/components/back-button.js new file mode 100755 index 000000000..e36306575 --- /dev/null +++ b/framework/core/ember/app/components/back-button.js @@ -0,0 +1,26 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + classNames: ['back-button'], + classNameBindings: ['active'], + active: Ember.computed.or('target.paneShowing', 'target.panePinned'), + + mouseEnter: function() { + this.get('target').send('showPane'); + }, + + mouseLeave: function() { + this.get('target').send('hidePane'); + }, + + actions: { + back: function() { + this.get('target').send('transitionFromBackButton'); + this.set('target', null); + }, + togglePinned: function() { + this.get('target').send('togglePinned'); + } + } + +}); diff --git a/framework/core/ember/app/components/discussions/discussion-listing.js b/framework/core/ember/app/components/discussions/discussion-listing.js index ff4880908..3dc433d90 100755 --- a/framework/core/ember/app/components/discussions/discussion-listing.js +++ b/framework/core/ember/app/components/discussions/discussion-listing.js @@ -2,20 +2,18 @@ import Ember from 'ember'; import TaggedArray from '../../utils/tagged-array'; import ActionButton from '../ui/controls/action-button'; +import SeparatorItem from '../ui/items/separator-item'; export default Ember.Component.extend({ - _init: function() { - // this.set('controls', Menu.create()); - }.on('init'), + terminalPostType: 'last', + countType: 'unread', tagName: 'li', attributeBindings: ['discussionId:data-id'], + classNames: ['discussion-summary'], classNameBindings: [ - 'discussion.unread:unread', - 'discussion.sticky:sticky', - 'discussion.locked:locked', - 'discussion.following:following', + 'discussion.isUnread:unread', 'active' ], layoutName: 'components/discussions/discussion-listing', @@ -24,9 +22,19 @@ export default Ember.Component.extend({ return this.get('childViews').anyBy('active'); }.property('childViews.@each.active'), - discussionId: function() { - return this.get('discussion.id'); - }.property('discussion.id'), + displayUnread: function() { + return this.get('countType') == 'unread' && this.get('discussion.isUnread'); + }.property('countType', 'discussion.isUnread'), + + displayLastPost: function() { + return this.get('terminalPostType') == 'last' && this.get('discussion.repliesCount'); + }.property('terminalPostType', 'discussion.repliesCount'), + + start: function() { + return this.get('discussion.isUnread') ? this.get('discussion.readNumber') + 1 : 1; + }.property('discussion.isUnread', 'discussion.readNumber'), + + discussionId: Ember.computed.alias('discussion.id'), relevantPosts: function() { if (this.get('controller.show') != 'posts') return []; @@ -39,115 +47,106 @@ export default Ember.Component.extend({ 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'); + var $this = this.$().css({opacity: 0}); - this.$().find('.terminal-post a').tooltip(); + setTimeout(function() { + $this.animate({opacity: 1}, 'fast'); + }, 100); - var view = this; - this.$().find('a.info, .terminal-post a').click(function() { - view.set('controller.paneShowing', false); - }); + if (this.get('discussion.isUnread')) { + this.$().find('.count').tooltip(); + } + + // var view = this; + // this.$().find('a.info').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.$().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; - } + // $(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(); + // isMoved = true; + // e.preventDefault(); - var diffX = touchesNow.x - touchesStart.x; - var translate = diffX; - var actionsRightWidth = 150; + // var diffX = touchesNow.x - touchesStart.x; + // var translate = diffX; + // var actionsRightWidth = 150; - if (translate < -actionsRightWidth) { - translate = -actionsRightWidth - Math.pow(-translate - actionsRightWidth, 0.8); - } + // if (translate < -actionsRightWidth) { + // translate = -actionsRightWidth - Math.pow(-translate - actionsRightWidth, 0.8); + // } - $(this).css('left', translate); - }); + // $(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; + // $(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'); - }); - }); + // 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()}); - // }})); + this.set('controls', TaggedArray.create()); }, + populateControlsDefault: function(controls) { + controls.pushObjectWithTag(ActionButton.create({ + label: 'Delete', + icon: 'times', + className: 'delete' + }), 'delete'); + }.on('populateControls'), + actions: { - icon: function() { - this.get('iconAction')(); + populateControls: function() { + if ( ! this.get('controls.length')) { + this.trigger('populateControls', this.get('controls')); + } } } diff --git a/framework/core/ember/app/components/discussions/post-content-comment.js b/framework/core/ember/app/components/discussions/post-content-comment.js new file mode 100644 index 000000000..7ed7d64b6 --- /dev/null +++ b/framework/core/ember/app/components/discussions/post-content-comment.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'article', + layoutName: 'components/discussions/post-content-comment', + + editDescription: '' +}); diff --git a/framework/core/ember/app/components/discussions/post-full.js b/framework/core/ember/app/components/discussions/post-wrapper.js similarity index 50% rename from framework/core/ember/app/components/discussions/post-full.js rename to framework/core/ember/app/components/discussions/post-wrapper.js index a42a018d4..09e63d298 100644 --- a/framework/core/ember/app/components/discussions/post-full.js +++ b/framework/core/ember/app/components/discussions/post-wrapper.js @@ -5,10 +5,12 @@ import ActionButton from '../ui/controls/action-button'; export default Ember.Component.extend({ tagName: 'article', - layoutName: 'components/discussions/post-full', + layoutName: 'components/discussions/post-wrapper', // controls: null, + post: Ember.computed.alias('content'), + contentComponent: function() { return 'discussions/post-content-'+this.get('post.type'); }.property('post.type'), @@ -16,26 +18,31 @@ export default Ember.Component.extend({ classNames: ['post'], classNameBindings: ['post.deleted', 'post.edited'], - construct: function() { - // this.set('controls', Menu.create()); + // construct: function() { + // // this.set('controls', Menu.create()); - // var post = this.get('post'); + // // 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'), + // // 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'); + var $this = this.$(); + $this.css({opacity: 0}); + + setTimeout(function() { + $this.animate({opacity: 1}, 'fast'); + }, 100); }, addControl: function(tag, title, icon, permissionAttribute) { diff --git a/framework/core/ember/app/components/discussions/stream-content.js b/framework/core/ember/app/components/discussions/stream-content.js new file mode 100644 index 000000000..29d0b5590 --- /dev/null +++ b/framework/core/ember/app/components/discussions/stream-content.js @@ -0,0 +1,258 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + + classNames: ['stream'], + + // The stream object. + stream: null, + + // Pause window scroll event listeners. This is set to true while loading + // posts, because we don't want a scroll event to trigger another block of + // posts to be loaded. + paused: false, + + // Whether or not the stream's initial content has loaded. + loaded: Ember.computed.bool('stream.loadedCount'), + + // When the stream content is not "active", window scroll event listeners + // will be ignored. For the stream content to be active, its initial + // content must be loaded and it must not be "paused". + active: function() { + return this.get('loaded') && ! this.get('paused'); + }.property('loaded', 'paused'), + + didInsertElement: function() { + $(window).on('scroll', {view: this}, this.windowWasScrolled); + }, + + willDestroyElement: function() { + $(window).off('scroll', this.windowWasScrolled); + }, + + windowWasScrolled: function(event) { + event.data.view.update(); + }, + + // Run any checks/updates according to the window's current scroll + // position. 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. Note: this + // observes the 'active' property, so if the stream is 'unpaused', then an + // update will be triggered. + update: function() { + if (! this.get('active')) { + return; + } + + var $items = this.$().find('.item'), + $window = $(window), + marginTop = this.getMarginTop(), + scrollTop = $window.scrollTop() + marginTop, + viewportHeight = $window.height() - marginTop, + loadAheadDistance = 300, + currentNumber; + + // Loop through each of the items in the stream. An 'item' is either a + // single post or a 'gap' of one or more posts that haven't been + // loaded yet. + $items.each(function() { + var $this = $(this), + top = $this.offset().top, + height = $this.outerHeight(true); + + // If this item is above the top of the viewport (plus a bit of + // leeway for loading-ahead gaps), skip to the next one. If it's + // below the bottom of the viewport, break out of the loop. + if (top + height < scrollTop - loadAheadDistance) { + return; + } + if (top > scrollTop + viewportHeight + loadAheadDistance) { + return false; + } + + // If this item is a gap, then we may proceed to check if it's a + // *terminal* gap and trigger its loading mechanism. + if ($this.hasClass('gap')) { + var gapView = Ember.View.views[$this.attr('id')]; + if ($this.is(':first-child')) { + gapView.set('direction', 'up').load(); + } else if ($this.is(':last-child')) { + gapView.set('direction', 'down').load(); + } + } + + // Check if this item is in the viewport, minus the distance we + // allow for load-ahead gaps. 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. + if (top + height > scrollTop && ! currentNumber) { + currentNumber = $this.data('number'); + } + }); + + // 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. + var view = this; + clearTimeout(this.updateStateTimeout); + this.updateStateTimeout = setTimeout(function() { + view.sendAction('updateStart', currentNumber || 1); + }, 250); + }.observes('active'), + + 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. + this.set('paused', 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; + Ember.run.scheduleOnce('afterRender', function() { + view.jumpToNumber(number).done(function() { + view.set('paused', false); + }); + }); + }, + + 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. + this.set('paused', 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; + Ember.run.scheduleOnce('afterRender', function() { + view.jumpToIndex(index).done(function() { + view.set('paused', false); + }); + }); + }, + + // Scroll down to a certain post by number (or the gap which we think the + // post is in) and highlight it. + jumpToNumber: function(number) { + // Clear the highlight class from all posts, and attempt to find and + // highlight a post with the specified number. However, we don't apply + // the highlight to the first post in the stream because it's pretty + // obvious that it's the top one. + var $item = this.$('.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); + } + + return this.scrollToItem($item); + }, + + // Scroll down to a certain post by index (or the gap the post is in.) + jumpToIndex: function(index) { + var $item = this.findNearestToIndex(index); + return this.scrollToItem($item); + }, + + scrollToItem: function($item) { + var $container = $('html, body'); + if ($item.length) { + var marginTop = this.getMarginTop(); + var scrollTop = $item.is(':first-child') ? 0 : $item.offset().top - marginTop; + if (scrollTop != $(document).scrollTop()) { + $container.stop(true).animate({scrollTop: scrollTop}); + } + } + return $container.promise(); + }, + + // Find the DOM element of the item that is nearest to a post with a certain + // number. This will either be another post (if the requested post doesn't + // exist,) or a gap presumed to contain the requested post. + findNearestToNumber: function(number) { + var $nearestItem = $(); + this.$('.item').each(function() { + var $this = $(this); + if ($this.data('number') > number) { + return false; + } + $nearestItem = $this; + }); + return $nearestItem; + }, + + findNearestToIndex: function(index) { + var $nearestItem = this.$('.item[data-start='+index+'][data-end='+index+']'); + if (! $nearestItem.length) { + this.$('.item').each(function() { + $nearestItem = $(this); + if ($nearestItem.data('end') >= index) { + return false; + } + }); + } + return $nearestItem; + }, + + // Get the distance from the top of the viewport to the point at which we + // would consider a post to be the first one visible. + getMarginTop: function() { + return $('#header').outerHeight() + parseInt(this.$().css('margin-top')); + }, + + actions: { + goToNumber: function(number) { + number = Math.max(number, 1); + + // Let's start by telling our listeners that we're going to load + // posts near this number. Elsewhere we will listen and + // consequently scroll down to the appropriate position. + this.trigger('loadingNumber', number); + + // Now we have to actually make sure the posts around this new start + // position are loaded. We will tell our listeners when they are. + // Again, a listener will scroll down to the appropriate post. + var controller = this; + this.get('stream').loadNearNumber(number).then(function() { + controller.trigger('loadedNumber', number); + }); + }, + + goToIndex: function(index) { + // Let's start by telling our listeners that we're going to load + // posts at this index. Elsewhere we will listen and consequently + // scroll down to the appropriate position. + this.trigger('loadingIndex', index); + + // Now we have to actually make sure the posts around this index + // are loaded. We will tell our listeners when they are. Again, a + // listener will scroll down to the appropriate post. + var controller = this; + this.get('stream').loadNearIndex(index).then(function() { + controller.trigger('loadedIndex', index); + }); + }, + + loadRange: function(start, end, backwards) { + this.get('stream').loadRange(start, end, backwards); + } + } +}); \ No newline at end of file diff --git a/framework/core/ember/app/components/discussions/stream-item.js b/framework/core/ember/app/components/discussions/stream-item.js index 6ad077932..f51853b0a 100644 --- a/framework/core/ember/app/components/discussions/stream-item.js +++ b/framework/core/ember/app/components/discussions/stream-item.js @@ -14,34 +14,17 @@ export default Ember.Component.extend({ 'number:data-number' ], - start: function() { - return this.get('item.indexStart'); - }.property('item.indexStart'), - - end: function() { - return this.get('item.indexEnd'); - }.property('item.indexEnd'), + start: Ember.computed.alias('item.indexStart'), + end: Ember.computed.alias('item.indexEnd'), + time: Ember.computed.alias('item.content.time'), + number: Ember.computed.alias('item.content.number'), + loading: Ember.computed.alias('item.loading'), + direction: Ember.computed.alias('item.direction'), 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'), @@ -73,8 +56,10 @@ export default Ember.Component.extend({ } 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'); + if (! self.get('loading')) { + var up = e.clientY > $(this).offset().top - $(document).scrollTop() + $(this).outerHeight(true) / 2; + self.set('direction', up ? 'up' : 'down'); + } }); } }, @@ -97,7 +82,7 @@ export default Ember.Component.extend({ // 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() { + this.get('stream').one('postsLoaded', function() { if (anchor.length) { var scrollOffset = anchor.offset().top - $(document).scrollTop(); } diff --git a/framework/core/ember/app/components/discussions/stream-scrollbar.js b/framework/core/ember/app/components/discussions/stream-scrollbar.js deleted file mode 100644 index dae130106..000000000 --- a/framework/core/ember/app/components/discussions/stream-scrollbar.js +++ /dev/null @@ -1,275 +0,0 @@ -import Ember from 'ember'; - -import Scrollbar from '../../utils/scrollbar'; -import PostStreamMixin from '../../mixins/post-stream'; - -export default Ember.View.extend(PostStreamMixin, { - - layoutName: 'components/discussions/stream-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); - } -}); diff --git a/framework/core/ember/app/components/discussions/stream-scrubber.js b/framework/core/ember/app/components/discussions/stream-scrubber.js new file mode 100644 index 000000000..91e685918 --- /dev/null +++ b/framework/core/ember/app/components/discussions/stream-scrubber.js @@ -0,0 +1,389 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + layoutName: 'components/discussions/stream-scrubber', + classNames: ['scrubber', 'stream-scrubber'], + classNameBindings: ['disabled'], + + // The stream-content component to which this scrubber is linked. + streamContent: null, + stream: Ember.computed.alias('streamContent.stream'), + loaded: Ember.computed.alias('streamContent.loaded'), + count: Ember.computed.alias('stream.count'), + + // The current index of the stream visible at the top of the viewport, and + // the number of items visible within the viewport. These aren't + // necessarily integers. + index: -1, + visible: 1, + + // The integer index of the last item that is visible in the viewport. This + // is display on the scrubber (i.e. X of 100 posts). + visibleIndex: function() { + return Math.min(this.get('count'), Math.ceil(Math.max(0, this.get('index')) + this.get('visible'))); + }.property('index', 'visible'), + + // The description displayed alongside the index in the scrubber. This is + // set to the date of the first visible post in the scroll event. + description: '', + + // Disable the scrubber if the stream's initial content isn't loaded, or + // if all of the posts in the discussion are visible in the viewport. + disabled: function() { + return ! this.get('loaded') || this.get('visible') >= this.get('count'); + }.property('loaded', 'visible', 'count'), + + // Whenever the stream object changes to a new one (i.e. when + // transitioning to a different discussion,) reset some properties and + // update the scrollbar to a neutral state. + refresh: function() { + this.set('index', -1); + this.set('visible', 1); + this.updateScrollbar(); + }.observes('stream'), + + didInsertElement: function() { + var view = this; + + // When the stream-content component begins loading posts at a certain + // index, we want our scrubber scrollbar to jump to that position. + this.get('streamContent').on('loadingIndex', this, this.loadingIndex); + + // 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); + + // When any part of the whole scrollbar is clicked, we want to jump to + // that position. + this.$('.scrubber-scrollbar') + .click(function(e) { + if (! view.get('streamContent.active')) { + return; + } + + // Calculate the index which we want to jump to based on the + // click position. + // 1. Get the offset of the click from the top of the + // scrollbar, as a percentage of the scrollbar's height. + var $this = $(this), + offsetPixels = e.clientY - $this.offset().top + $('body').scrollTop(), + offsetPercent = offsetPixels / $this.outerHeight() * 100; + + // 2. We want the handle of the scrollbar to end up centered + // on the click position. Thus, we calculate the height of + // the handle in percent and use that to find a new + // offset percentage. + offsetPercent = offsetPercent - parseFloat($this.find('.scrubber-slider')[0].style.height) / 2; + + // 3. Now we can convert the percentage into an index, and + // tell the stream-content component to jump to that index. + var offsetIndex = offsetPercent / view.percentPerPost().index; + offsetIndex = Math.max(0, Math.min(view.get('count') - 1, offsetIndex)); + view.get('streamContent').send('goToIndex', Math.floor(offsetIndex)); + }); + + // Now we want to make the scrollbar handle draggable. Let's start by + // preventing default browser events from messing things up. + this.$('.scrubber-scrollbar') + .css({ + cursor: 'pointer', + 'user-select': 'none' + }) + .bind('dragstart mousedown', function(e) { + e.preventDefault(); + }); + + // When the mouse is pressed on the scrollbar handle, we capture some + // information about its current position. We will store this + // information in an object and pass it on to the document's + // mousemove/mouseup events later. + var dragData = { + view: this, + mouseStart: 0, + indexStart: 0, + handle: null + }; + this.$('.scrubber-slider') + .css('cursor', 'move') + .mousedown(function(e) { + dragData.mouseStart = e.clientY; + dragData.indexStart = view.get('index'); + dragData.handle = $(this); + view.set('streamContent.paused', true); + $('body').css('cursor', 'move'); + }) + // Exempt the scrollbar handle from the 'jump to' click event. + .click(function(e) { + e.stopPropagation(); + }); + + // When the mouse moves and when it is released, we pass the + // information that we captured when the mouse was first pressed onto + // some event handlers. These handlers will move the scrollbar/stream- + // content as appropriate. + $(document) + .on('mousemove', dragData, this.mouseWasMoved) + .on('mouseup', dragData, this.mouseWasReleased); + + // Finally, we'll just make sure the scrollbar is in the correct + // position according to the values of this.index/visible. + this.updateScrollbar(true); + }, + + willDestroyElement: function() { + this.get('streamContent').off('loadingIndex', this, this.loadingIndex); + + $(window) + .off('resize', this.windowWasResized) + .off('scroll', this.windowWasScrolled); + + $(document) + .off('mousemove', this.mouseWasMoved) + .off('mouseup', this.mouseWasReleased); + }, + + // When the stream-content component begins loading posts at a certain + // index, we want our scrubber scrollbar to jump to that position. + loadingIndex: function(index) { + this.set('index', index); + this.updateScrollbar(true); + }, + + windowWasResized: function(event) { + var view = event.data.view; + view.windowWasScrolled(event); + + // Adjust the height of the scrollbar so that it fills the height of + // the sidebar and doesn't overlap the footer. + var scrollbar = view.$('.scrubber-scrollbar'); + scrollbar.css('max-height', $(window).height() - scrollbar.offset().top + $(window).scrollTop() - $('#footer').outerHeight(true)); + }, + + windowWasScrolled: function(event) { + var view = event.data.view; + if (view.get('streamContent.active')) { + view.update(); + view.updateScrollbar(); + } + }, + + mouseWasMoved: function(event) { + if (! event.data.handle) { + return; + } + var view = event.data.view; + + // Work out how much the mouse has moved by - first in pixels, then + // convert it to a percentage of the scrollbar's height, and then + // finally convert it into an index. Add this delta index onto + // the index at which the drag was started, and then scroll there. + var deltaPixels = event.clientY - event.data.mouseStart, + deltaPercent = deltaPixels / view.$('.scrubber-scrollbar').outerHeight() * 100, + deltaIndex = deltaPercent / view.percentPerPost().index, + newIndex = Math.min(event.data.indexStart + deltaIndex, view.get('count') - 1); + + view.set('index', Math.max(0, newIndex)); + view.updateScrollbar(); + view.scrollToIndex(newIndex); + }, + + mouseWasReleased: function(event) { + if (! event.data.handle) { + return; + } + event.data.mouseStart = 0; + event.data.indexStart = 0; + event.data.handle = null; + $('body').css('cursor', ''); + + var view = event.data.view; + view.set('streamContent.paused', false); + + // If the index we've landed on is in a gap, then tell the stream- + // content that we want to load those posts. + var intIndex = Math.floor(view.get('index')); + if (view.get('stream').findNearestToIndex(intIndex).gap) { + view.get('streamContent').send('goToIndex', intIndex); + } + }, + + // When the stream-content component resumes being 'active' (for example, + // after a bunch of posts have been loaded), then we want to update the + // scrubber scrollbar according to the window's current scroll position. + resume: function() { + if (this.get('streamContent.active')) { + this.update(); + this.updateScrollbar(true); + } + }.observes('streamContent.active'), + + // Update the index/visible/description properties according to the + // window's current scroll position. + update: function() { + if (! this.get('streamContent.active')) { + return; + } + + var $window = $(window), + marginTop = this.get('streamContent').getMarginTop(), + scrollTop = $window.scrollTop() + marginTop, + windowHeight = $window.height() - marginTop; + + // 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 $items = this.get('streamContent').$().find('.item'); + var index = $items.first().data('end') - 1; + var visible = 0; + var period = ''; + + // 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. + $items.each(function(k) { + var $this = $(this), + top = $this.offset().top, + height = $this.outerHeight(true); + + // 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) { + visible = (top + height - scrollTop) / height; + index = parseFloat($this.data('end')) + 1 - visible; + 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 visible + // 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) { + visible = (top + height - scrollTop) / height; + index = parseFloat($this.data('end')) + 1 - visible; + } + + // If the top half of this item is visible at the bottom of the + // viewport, then add the visible proportion to the visible + // counter. + else if (top + height >= scrollTop + windowHeight) { + visible += (scrollTop + windowHeight - top) / height; + } + + // If the whole item is visible in the viewport, then increment the + // visible counter. + else { + visible++; + } + + // 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'); + } + }); + + this.set('index', index); + this.set('visible', visible); + this.set('description', period ? moment(period).format('MMMM YYYY') : ''); + }, + + // Update the scrollbar's position to reflect the current values of the + // index/visible properties. + updateScrollbar: function(animate) { + var percentPerPost = this.percentPerPost(), + index = this.get('index'), + count = this.get('count'), + visible = this.get('visible'); + + var heights = {}; + heights.before = Math.max(0, percentPerPost.index * Math.min(index, count - visible)); + heights.slider = Math.min(100 - heights.before, percentPerPost.visible * visible); + heights.after = 100 - heights.before - heights.slider; + + var $scrubber = this.$(); + var func = animate ? 'animate' : 'css'; + for (var part in heights) { + var $part = $scrubber.find('.scrubber-'+part); + $part.stop(true, true)[func]({height: heights[part]+'%'}); + + // jQuery likes to put overflow:hidden, but because the scrollbar + // handle has a negative margin-left, we need to override. + if (func == 'animate') { + $part.css('overflow', 'visible'); + } + } + }, + + // 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) { + index = Math.min(index, this.get('count') - 1); + + // Find the item for this index, whether it's a post corresponding to + // the index, or a gap which the index is within. + var indexFloor = Math.max(0, Math.floor(index)), + $nearestItem = this.get('streamContent').findNearestToIndex(indexFloor); + + // Calculate the position of this item so that we can scroll to it. If + // the item is a gap, then we will mark it as 'active' to indicate to + // the user that it will expand if they release their mouse. + // Otherwise, we will add a proportion of the item's height onto the + // scroll position. + var pos = $nearestItem.offset().top - this.get('streamContent').getMarginTop(); + if ($nearestItem.is('.gap')) { + $nearestItem.addClass('active'); + } else { + if (index >= 0) { + pos += $nearestItem.outerHeight(true) * (index - indexFloor); + } else { + pos += $nearestItem.offset().top * index; + } + } + + // Remove the 'active' class from other gaps. + this.get('streamContent').$().find('.gap').not($nearestItem).removeClass('active'); + + $('html, body').scrollTop(pos); + }, + + percentPerPost: function() { + var count = this.get('count') || 1, + visible = this.get('visible'); + + // To stop the slider of the scrollbar from getting too small when there + // are many posts, we define a minimum percentage height for the slider + // calculated from a 50 pixel limit. From this, we can calculate the + // minimum percentage per visible post. If this is greater than the + // actual percentage per post, then we need to adjust the 'before' + // percentage to account for it. + var minPercentVisible = 50 / this.$('.scrubber-scrollbar').outerHeight() * 100; + var percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible); + var percentPerPost = count == visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible); + + return { + index: percentPerPost, + visible: percentPerVisiblePost + }; + }, + + actions: { + first: function() { + this.get('streamContent').send('goToIndex', 0); + }, + + last: function() { + this.get('streamContent').send('goToIndex', this.get('count') - 1); + } + } +}); diff --git a/framework/core/ember/app/components/ui/controls/action-button.js b/framework/core/ember/app/components/ui/controls/action-button.js index f24158268..d27e2032f 100644 --- a/framework/core/ember/app/components/ui/controls/action-button.js +++ b/framework/core/ember/app/components/ui/controls/action-button.js @@ -1,24 +1,28 @@ import Ember from 'ember'; export default Ember.Component.extend({ - title: '', + label: '', icon: '', className: '', action: null, divider: false, active: false, - classNames: ['btn', 'btn-default'], + classNames: [], tagName: 'a', - attributeBindings: ['href'], + attributeBindings: ['href', 'title'], classNameBindings: ['className'], href: '#', - layout: Ember.Handlebars.compile('{{#if icon}}{{fa-icon icon class="fa-fw"}} {{/if}}{{title}}'), + layout: Ember.Handlebars.compile('{{#if icon}}{{fa-icon icon class="fa-fw icon-glyph"}} {{/if}}{{label}}'), click: function(e) { e.preventDefault(); - // this.sendAction('action'); - this.get('action')(); + var action = this.get('action'); + if (typeof action == 'string') { + this.sendAction('action'); + } else if (typeof action == 'function') { + action(); + } } }); \ No newline at end of file diff --git a/framework/core/ember/app/components/ui/controls/dropdown-button.js b/framework/core/ember/app/components/ui/controls/dropdown-button.js index ff74c9287..e0748429e 100644 --- a/framework/core/ember/app/components/ui/controls/dropdown-button.js +++ b/framework/core/ember/app/components/ui/controls/dropdown-button.js @@ -4,12 +4,12 @@ export default Ember.Component.extend({ items: null, // TaggedArray layoutName: 'components/ui/controls/dropdown-button', classNames: ['dropdown', 'btn-group'], - classNameBindings: ['itemCountClass'], + classNameBindings: ['itemCountClass', 'class'], title: 'Controls', icon: 'ellipsis-v', - buttonClass: 'btn-default', - menuClass: 'pull-right', + buttonClass: 'btn btn-default', + menuClass: '', dropdownMenuClass: function() { return 'dropdown-menu '+this.get('menuClass'); @@ -17,5 +17,11 @@ export default Ember.Component.extend({ itemCountClass: function() { return 'item-count-'+this.get('items.length'); - }.property('items.length') + }.property('items.length'), + + actions: { + buttonClick: function() { + this.sendAction('buttonClick'); + } + } }); diff --git a/framework/core/ember/app/components/ui/controls/dropdown-select.js b/framework/core/ember/app/components/ui/controls/dropdown-select.js index 367cdccbf..eead02e6c 100644 --- a/framework/core/ember/app/components/ui/controls/dropdown-select.js +++ b/framework/core/ember/app/components/ui/controls/dropdown-select.js @@ -4,10 +4,10 @@ export default Ember.Component.extend({ items: [], layoutName: 'components/ui/controls/dropdown-select', classNames: ['dropdown', 'dropdown-select', 'btn-group'], - classNameBindings: ['itemCountClass'], + classNameBindings: ['itemCountClass', 'class'], - buttonClass: 'btn-default', - menuClass: 'pull-right', + buttonClass: 'btn btn-default', + menuClass: '', icon: 'ellipsis-v', mainButtonClass: function() { @@ -25,9 +25,4 @@ export default Ember.Component.extend({ activeItem: function() { return this.get('menu.childViews').findBy('active'); }.property('menu.childViews.@each.active') - -}).reopenClass({ - createWithItems: function(items) { - return this.create({items: items}); - } -}); +}); \ No newline at end of file diff --git a/framework/core/ember/app/components/ui/controls/dropdown-split.js b/framework/core/ember/app/components/ui/controls/dropdown-split.js index 099479e69..037165ca8 100644 --- a/framework/core/ember/app/components/ui/controls/dropdown-split.js +++ b/framework/core/ember/app/components/ui/controls/dropdown-split.js @@ -5,6 +5,7 @@ import DropdownButton from './dropdown-button'; export default DropdownButton.extend({ layoutName: 'components/ui/controls/dropdown-split', classNames: ['dropdown', 'dropdown-split', 'btn-group'], + menuClass: 'pull-right', mainButtonClass: function() { return 'btn '+this.get('buttonClass'); diff --git a/framework/core/ember/app/components/ui/controls/loading-indicator.js b/framework/core/ember/app/components/ui/controls/loading-indicator.js index fc3c97e22..e908f30f0 100644 --- a/framework/core/ember/app/components/ui/controls/loading-indicator.js +++ b/framework/core/ember/app/components/ui/controls/loading-indicator.js @@ -1,9 +1,10 @@ import Ember from 'ember'; export default Ember.Component.extend({ - classNames: ['loading'], + classNames: ['loading-indicator'], layout: Ember.Handlebars.compile(' '), + size: 'small', didInsertElement: function() { this.$().spin(this.get('size')); diff --git a/framework/core/ember/app/components/ui/controls/search-input.js b/framework/core/ember/app/components/ui/controls/search-input.js index dea3d1987..5edcbb21d 100644 --- a/framework/core/ember/app/components/ui/controls/search-input.js +++ b/framework/core/ember/app/components/ui/controls/search-input.js @@ -4,6 +4,8 @@ export default Ember.Component.extend({ classNames: ['search-input'], classNameBindings: ['active', 'value:clearable'], + layoutName: 'components/ui/controls/search-input', + didInsertElement: function() { var self = this; this.$().find('input').on('keydown', function(e) { @@ -21,7 +23,7 @@ export default Ember.Component.extend({ clear: function() { this.set('value', ''); - this.sendAction('action', ''); + this.send('search'); this.$().find('input').focus(); }, @@ -32,7 +34,7 @@ export default Ember.Component.extend({ actions: { search: function() { - this.sendAction('action', this.get('value')); + this.get('action')(this.get('value')); } } }); diff --git a/framework/core/ember/app/components/ui/controls/select-input.js b/framework/core/ember/app/components/ui/controls/select-input.js index 18c797501..52381dacb 100644 --- a/framework/core/ember/app/components/ui/controls/select-input.js +++ b/framework/core/ember/app/components/ui/controls/select-input.js @@ -2,6 +2,8 @@ import Ember from 'ember'; export default Ember.View.extend({ tagName: 'span', - classNames: ['select'], - layout: Ember.Handlebars.compile('{{view "select" content=view.content optionValuePath=view.optionValuePath optionLabelPath=view.optionLabelPath value=view.value}} {{fa-icon "sort"}}') + classNames: ['select-input'], + optionValuePath: 'content', + optionLabelPath: 'content', + layout: Ember.Handlebars.compile('{{view "select" content=view.content optionValuePath=view.optionValuePath optionLabelPath=view.optionLabelPath value=view.value class="form-control"}} {{fa-icon "sort"}}') }); diff --git a/framework/core/ember/app/components/ui/items/nav-item.js b/framework/core/ember/app/components/ui/items/nav-item.js index 76d585df9..432ddbfb7 100644 --- a/framework/core/ember/app/components/ui/items/nav-item.js +++ b/framework/core/ember/app/components/ui/items/nav-item.js @@ -2,7 +2,7 @@ import Ember from 'ember'; export default Ember.Component.extend({ icon: '', - title: '', + label: '', action: null, badge: '', @@ -22,7 +22,7 @@ export default Ember.Component.extend({ // }, layout: function() { - return Ember.Handlebars.compile('{{#link-to '+this.get('linkTo')+'}}'+this.get('iconTemplate')+' {{title}} {{badge}}{{/link-to}}'); + return Ember.Handlebars.compile('{{#link-to '+this.get('linkTo')+'}}'+this.get('iconTemplate')+' {{label}} {{badge}}{{/link-to}}'); }.property('linkTo', 'iconTemplate'), iconTemplate: function() { diff --git a/framework/core/ember/app/components/welcome-hero.js b/framework/core/ember/app/components/welcome-hero.js new file mode 100644 index 000000000..d9170f0fa --- /dev/null +++ b/framework/core/ember/app/components/welcome-hero.js @@ -0,0 +1,15 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + + tagName: 'header', + classNames: ['hero', 'welcome-hero'], + + didInsertElement: function() { + var hero = this.$(); + hero.find('.close').click(function() { + hero.slideUp(); + }); + } + +}); \ No newline at end of file diff --git a/framework/core/ember/app/controllers/application.js b/framework/core/ember/app/controllers/application.js index c5b925c80..3752856ca 100644 --- a/framework/core/ember/app/controllers/application.js +++ b/framework/core/ember/app/controllers/application.js @@ -1,48 +1,35 @@ import Ember from 'ember'; -// import NotificationMessage from '../models/notification-message'; - export default Ember.Controller.extend({ - needs: ['discussions'], - // The title of the forum. // TODO: Preload this value in the index.html payload from Laravel config. forumTitle: 'Ninetech Support Forum', - // forumTitle: ' TV Addicts', - // forumTitle: '', - // forumTitle: '  Med Students Forum', - pageTitle: '', - documentTitle: function() { - return this.get('pageTitle') || this.get('forumTitle'); - }.property('pageTitle', 'forumTitle'), - _updateTitle: function() { + // The title of the current page. This should be set as appropriate in + // controllers/views. + pageTitle: '', + + // When either the forum title or the page title changes, we want to + // refresh the document's title. + updateTitle: function() { var parts = [this.get('forumTitle')]; var pageTitle = this.get('pageTitle'); - if (pageTitle) parts.unshift(pageTitle); + if (pageTitle) { + parts.unshift(pageTitle); + } document.title = parts.join(' - '); }.observes('pageTitle', 'forumTitle'), + // Whether or not a pane is currently pinned to the side of the interface. + panePinned: false, + searchQuery: '', searchActive: false, - showDiscussionStream: false, - - // notificationMessage: NotificationMessage.create({text: 'Sorry, you do not have permission to do that!', class: 'message-warning'}), // currently displaying notification message object - - currentUser: null, - actions: { - - hideMessage: function() { - this.set('notificationMessage', null); - }, - search: function(query) { - this.transitionToRoute('discussions', {queryParams: {searchQuery: query, sort: query ? 'relevance' : 'recent'}}); - }, - + this.transitionToRoute('index', {queryParams: {searchQuery: query, sort: query ? 'relevance' : 'recent'}}); + } } - }); diff --git a/framework/core/ember/app/controllers/composer.js b/framework/core/ember/app/controllers/composer.js index 199a258fa..ed8fb90ae 100644 --- a/framework/core/ember/app/controllers/composer.js +++ b/framework/core/ember/app/controllers/composer.js @@ -2,7 +2,9 @@ import Ember from 'ember'; export default Ember.Controller.extend({ - needs: ['discussions'], + needs: ['index'], + + user: Ember.Object.create({avatarNumber: 1}), showing: false, diff --git a/framework/core/ember/app/controllers/discussion.js b/framework/core/ember/app/controllers/discussion.js index 2558c1864..dc38ff961 100644 --- a/framework/core/ember/app/controllers/discussion.js +++ b/framework/core/ember/app/controllers/discussion.js @@ -1,6 +1,6 @@ import Ember from 'ember'; -import Stream from '../models/stream'; +import PostStream from '../models/post-stream'; export default Ember.ObjectController.extend(Ember.Evented, { @@ -19,7 +19,7 @@ export default Ember.ObjectController.extend(Ember.Evented, { // Set up the post stream object. It needs to know about the discussion // its representing the posts for, and we also need to inject the Ember // data store. - var stream = Stream.create(); + var stream = PostStream.create(); stream.set('discussion', discussion); stream.set('store', this.get('store')); this.set('stream', stream); @@ -30,74 +30,25 @@ export default Ember.ObjectController.extend(Ember.Evented, { var promise = discussion.get('posts') ? Ember.RSVP.resolve(discussion) : discussion.reload(); // When we know we have the post IDs, we can set up the post stream with - // them. Then we're ready to load some posts! + // them. Then the view will trigger the stream to load as it sees fit. var controller = this; promise.then(function(discussion) { stream.setup(discussion.get('postIds')); controller.set('loaded', true); - controller.send('jumpToNumber', controller.get('start')); }); }, actions: { - reply: function() { this.set('controllers.composer.showing', true); this.set('controllers.composer.title', 'Replying to '+this.get('model.title')+''); }, - jumpToNumber: function(number) { - // In some instances, we might be given a placeholder start index - // value. We need to convert this into a numerical value. - switch (number) { - case 'last': - number = this.get('model.lastPostNumber'); - break; - - case 'unread': - number = this.get('model.readNumber') + 1; - break; - } - - number = Math.max(number, 1); - - // Let's start by telling our listeners that we're going to load - // posts near this number. The discussion view will listen and - // consequently scroll down to the appropriate position in the - // discussion. - this.trigger('loadingNumber', number); - - // Now we have to actually make sure the posts around this new start - // position are loaded. We will tell our listeners when they are. - // Again, the view will scroll down to the appropriate post. - var controller = this; - this.get('stream').loadNearNumber(number).then(function() { - Ember.run.scheduleOnce('afterRender', function() { - controller.trigger('loadedNumber', number); - }); - }); - }, - - jumpToIndex: function(index) { - // Let's start by telling our listeners that we're going to load - // posts at this index. The discussion view will listen and - // consequently scroll down to the appropriate position in the - // discussion. - this.trigger('loadingIndex', index); - - // Now we have to actually make sure the posts around this index are - // loaded. We will tell our listeners when they are. Again, the view - // will scroll down to the appropriate post. - var controller = this; - this.get('stream').loadNearIndex(index).then(function() { - Ember.run.scheduleOnce('afterRender', function() { - controller.trigger('loadedIndex', index); - }); - }); - }, - - loadRange: function(start, end, backwards) { - this.get('stream').loadRange(start, end, backwards); + // This action is called when the start position of the discussion + // currently being viewed changes (i.e. when the user scrolls up/down + // the post stream.) + updateStart: function(start) { + this.set('start', start); } } }); diff --git a/framework/core/ember/app/controllers/discussions.js b/framework/core/ember/app/controllers/index.js similarity index 67% rename from framework/core/ember/app/controllers/discussions.js rename to framework/core/ember/app/controllers/index.js index 1f8da14b0..c988f69ab 100644 --- a/framework/core/ember/app/controllers/discussions.js +++ b/framework/core/ember/app/controllers/index.js @@ -2,44 +2,16 @@ import Ember from 'ember'; import DiscussionResult from '../models/discussion-result'; import PostResult from '../models/post-result'; +import PaneableMixin from '../mixins/paneable'; -export default Ember.ArrayController.extend(Ember.Evented, { +export default Ember.ArrayController.extend(Ember.Evented, PaneableMixin, { needs: ['application', 'composer'], - paned: false, - paneShowing: false, - paneTimeout: null, - panePinned: false, - - current: null, - - index: function() { - var index = '?'; - var id = this.get('current.id'); - this.get('model').some(function(result, i) { - if (result.get('id') == id) { - index = i + 1; - return true; - } - }); - return index; - }.property('current', 'model.@each'), - count: function() { return this.get('model.length'); }.property('model.@each'), - previous: function() { - var result = this.get('model').objectAt(this.get('index') - 2); - return result && result.get('content'); - }.property('index'), - - next: function() { - var result = this.get('model').objectAt(this.get('index')); - return result && result.get('content'); - }.property('index'), - queryParams: ['sort', 'show', {searchQuery: 'q'}, 'filter'], sort: 'recent', show: 'discussions', @@ -55,8 +27,12 @@ export default Ember.ArrayController.extend(Ember.Evented, { {sort: 'oldest', label: 'Oldest'}, ], - displayStartUsers: function() { - return ['newest', 'oldest'].indexOf(this.get('sort')) != -1; + terminalPostType: function() { + return ['newest', 'oldest'].indexOf(this.get('sort')) != -1 ? 'start' : 'last'; + }.property('sort'), + + countType: function() { + return this.get('sort') == 'replies' ? 'replies' : 'unread'; }.property('sort'), discussionsCount: function() { @@ -80,10 +56,11 @@ export default Ember.ArrayController.extend(Ember.Evented, { var show = this.get('show'); var searchQuery = this.get('searchQuery'); - if (sort == 'newest') sort = 'created'; - else if (sort == 'oldest') { + if (sort == 'newest') { + sort = 'created'; + order = 'desc'; + } else if (sort == 'oldest') { sort = 'created'; - order = 'asc'; } else if (sort == 'recent') { sort = ''; @@ -124,37 +101,24 @@ export default Ember.ArrayController.extend(Ember.Evented, { }, actions: { - showDiscussionPane: function() { - this.set('paneShowing', true); - }, - - hideDiscussionPane: function() { - this.set('paneShowing', false); - }, - - togglePinned: function() { - this.set('panePinned', ! this.get('panePinned')); - }, - loadMore: function() { var self = this; this.set('start', this.get('length')); - this.set('loadingMore', true); + this.set('resultsLoading', true); this.getResults(this.get('start')).then(function(results) { self.get('model').addObjects(results); self.set('meta', results.get('meta')); - // self.set('moreResults', !! results.get('meta.moreUrl')); - self.set('loadingMore', false); + self.set('resultsLoading', false); }); }, - delete: function(discussion) { - alert('are you sure you want to delete discusn: '+discussion.get('title')); + transitionFromBackButton: function() { + this.transitionToRoute('index'); } }, - queryDidChange: function(q) { + searchQueryDidChange: function(q) { this.get('controllers.application').set('searchQuery', this.get('searchQuery')); this.get('controllers.application').set('searchActive', !! this.get('searchQuery')); diff --git a/framework/core/ember/app/controllers/discussions/index.js b/framework/core/ember/app/controllers/index/index.js similarity index 66% rename from framework/core/ember/app/controllers/discussions/index.js rename to framework/core/ember/app/controllers/index/index.js index 11f03b553..e2128002e 100644 --- a/framework/core/ember/app/controllers/discussions/index.js +++ b/framework/core/ember/app/controllers/index/index.js @@ -1,5 +1,5 @@ import Ember from 'ember'; export default Ember.ArrayController.extend({ - needs: ['application', 'composer'] + needs: ['application'] }); diff --git a/framework/core/ember/app/helpers/fa-icon.js b/framework/core/ember/app/helpers/fa-icon.js index b1b464acc..6c5375e9d 100644 --- a/framework/core/ember/app/helpers/fa-icon.js +++ b/framework/core/ember/app/helpers/fa-icon.js @@ -1,6 +1,6 @@ import Ember from 'ember'; export default Ember.Handlebars.makeBoundHelper(function(icon, options) { - return new Handlebars.SafeString(''); + return new Handlebars.SafeString(''); }); diff --git a/framework/core/ember/app/mixins/paneable.js b/framework/core/ember/app/mixins/paneable.js new file mode 100644 index 000000000..8d1dc076d --- /dev/null +++ b/framework/core/ember/app/mixins/paneable.js @@ -0,0 +1,47 @@ +import Ember from 'ember'; + +// This mixin defines a "paneable" controller - this is, one that has a +// portion of its interface that can be turned into a pane which slides out +// from the side of the screen. This is useful, for instance, when you have +// nested routes (index > discussion) and want to have the parent +// route's interface transform into a side pane when entering the child route. +export default Ember.Mixin.create({ + needs: ['application'], + + // Whether or not the "paneable" interface element is paned. + paned: false, + + // Whether or not the pane should be visible on screen. + paneShowing: false, + paneHideTimeout: null, + + // Whether or not the pane is always visible on screen, even when the + // mouse is taken away. + panePinned: false, + + actions: { + showPane: function() { + if (this.get('paned')) { + clearTimeout(this.get('paneHideTimeout')); + this.set('paneShowing', true); + } + }, + + hidePane: function(delay) { + var controller = this; + controller.set('paneHideTimeout', setTimeout(function() { + controller.set('paneShowing', false); + }, delay || 250)); + }, + + togglePinned: function() { + this.toggleProperty('panePinned'); + } + }, + + // Tell the application controller when we pin/unpin the pane so that + // other parts of the interface can respond appropriately. + panePinnedChanged: function() { + this.set('controllers.application.panePinned', this.get('paned') && this.get('panePinned')); + }.observes('paned', 'panePinned') +}); \ No newline at end of file diff --git a/framework/core/ember/app/mixins/post-stream.js b/framework/core/ember/app/mixins/post-stream.js deleted file mode 100644 index bce952d97..000000000 --- a/framework/core/ember/app/mixins/post-stream.js +++ /dev/null @@ -1,33 +0,0 @@ -export default Ember.Mixin.create({ - // Find the DOM element of the item that is nearest to a post with a certain - // number. This will either be another post (if the requested post doesn't - // exist,) or a gap presumed to container the requested post. - findNearestToNumber: function(number) { - var nearestItem = $(); - $('.posts .item').each(function() { - var $this = $(this), - thisNumber = $this.data('number'); - if (thisNumber > number) { - return false; - } - nearestItem = $this; - }); - return nearestItem; - }, - - findNearestToIndex: function(index) { - var nearestItem = $('.posts .item[data-start='+index+'][data-end='+index+']'); - - if (! nearestItem.length) { - $('.posts .item').each(function() { - var $this = $(this); - if ($this.data('start') <= index && $this.data('end') >= index) { - nearestItem = $this; - return false; - } - }); - } - - return nearestItem; - } -}); diff --git a/framework/core/ember/app/models/discussion-state.js b/framework/core/ember/app/models/discussion-state.js deleted file mode 100644 index d2cb576c6..000000000 --- a/framework/core/ember/app/models/discussion-state.js +++ /dev/null @@ -1,7 +0,0 @@ -import Ember from 'ember'; -import DS from 'ember-data'; - -export default DS.Model.extend({ - readTime: DS.attr('date'), - readNumber: DS.attr('number') -}); diff --git a/framework/core/ember/app/models/stream.js b/framework/core/ember/app/models/post-stream.js similarity index 81% rename from framework/core/ember/app/models/stream.js rename to framework/core/ember/app/models/post-stream.js index c4d19fd7d..9e3aa997e 100644 --- a/framework/core/ember/app/models/stream.js +++ b/framework/core/ember/app/models/post-stream.js @@ -6,8 +6,10 @@ import Ember from 'ember'; export default Ember.ArrayProxy.extend(Ember.Evented, { // An array of all of the post IDs, in chronological order, in the discussion. - ids: Em.A(), - content: Em.A(), + ids: null, + + content: null, + store: null, discussion: null, @@ -19,12 +21,14 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { setup: function(ids) { this.set('ids', ids); - this.clear(); + this.get('content').objectAt(0).set('indexEnd', this.get('count') - 1); }, - count: function() { - return this.get('ids.length'); - }.property('ids'), + count: Ember.computed.alias('ids.length'), + + loadedCount: function() { + return this.get('content').filterBy('content').length; + }.property('content.@each'), firstLoaded: function() { var first = this.objectAt(0); @@ -38,15 +42,15 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { // Clear the contents of the post stream, resetting it to one big gap. clear: function() { - var stream = this.get('content'); - stream.enumerableContentWillChange(); - stream.clear().pushObject(Em.Object.create({ + var content = Ember.A(); + content.clear().pushObject(Em.Object.create({ gap: true, indexStart: 0, indexEnd: this.get('count') - 1, loading: true })); - stream.enumerableContentDidChange(); + this.set('content', content); + this.set('ids', Ember.A()); }, loadRange: function(start, end, backwards) { @@ -92,8 +96,8 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { // request.) Or, if it's a gap, we'll switch on its loading flag. var item = this.findNearestToNumber(number); if (item) { - if (item.get('post.number') == number) { - return Ember.RSVP.resolve([item.get('post')]); + if (item.get('content.number') == number) { + return Ember.RSVP.resolve([item.get('content')]); } else if (item.gap) { item.set('direction', 'down').set('loading', true); } @@ -102,7 +106,8 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { var stream = this; return this.store.find('post', { discussions: this.get('discussion.id'), - near: number + near: number, + count: this.get('postLoadCount') }).then(function(posts) { stream.addPosts(posts); }); @@ -116,11 +121,11 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { var item = this.findNearestToIndex(index); if (item) { if (! item.gap) { - return Ember.RSVP.resolve([item.get('post')]); + return Ember.RSVP.resolve([item.get('content')]); } else { item.set('direction', 'down').set('loading', true); } - return this.loadRange(Math.max(item.indexStart, index - 10), item.indexEnd); + return this.loadRange(Math.max(item.indexStart, index - this.get('postLoadCount') / 2), item.indexEnd); } return Ember.RSVP.reject(); @@ -158,7 +163,7 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { newItems.push(Ember.Object.create({ indexStart: index, indexEnd: index, - post: post + content: post })); if (item.indexEnd > index) { newItems.push(Ember.Object.create({ @@ -167,35 +172,29 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { indexEnd: item.indexEnd })); } - content.enumerableContentWillChange(); content.replace(i, 1, newItems); - content.enumerableContentDidChange(); return true; } - }); + }); + }, + + findNearestTo: function(index, property) { + var nearestItem; + this.get('content').some(function(item) { + nearestItem = item; + if (item.get(property) > index) { + return true; + } + }); + return nearestItem; }, findNearestToNumber: function(number) { - var nearestItem; - this.get('content').some(function(item) { - var thisNumber = item.get('post.number'); - if (thisNumber > number) { - return true; - } - nearestItem = item; - }); - return nearestItem; + return this.findNearestTo(number, 'content.number'); }, findNearestToIndex: function(index) { - var nearestItem; - this.get('content').some(function(item) { - if (item.indexStart <= index && item.indexEnd >= index) { - nearestItem = item; - return true; - } - }); - return nearestItem; + return this.findNearestTo(index, 'indexEnd'); } }); diff --git a/framework/core/ember/app/router.js b/framework/core/ember/app/router.js index 7e5ad6637..86b1f3ac9 100644 --- a/framework/core/ember/app/router.js +++ b/framework/core/ember/app/router.js @@ -1,14 +1,13 @@ import Ember from 'ember'; import config from './config/environment'; -console.log(config.locationType); var Router = Ember.Router.extend({ location: config.locationType }); Router.map(function() { - this.resource('discussions', {path: '/'}, function() { + this.resource('index', {path: '/'}, function() { this.resource('discussion', {path: '/:id/:slug'}, function() { this.route('near', {path: '/:near'}); }); diff --git a/framework/core/ember/app/routes/discussion.js b/framework/core/ember/app/routes/discussion.js index c0dee2df9..e745e0496 100644 --- a/framework/core/ember/app/routes/discussion.js +++ b/framework/core/ember/app/routes/discussion.js @@ -16,40 +16,45 @@ export default Ember.Route.extend({ controller.set('start', '1'); controller.set('searchQuery', ''); controller.set('loaded', false); - controller.set('postStream', null); + controller.set('stream', null); }, setupController: function(controller, model) { controller.setup(model); - this.controllerFor('application').set('showDiscussionStream', true); - this.controllerFor('discussions').set('paned', true); - this.controllerFor('discussions').set('current', model); + // Tell the discussions controller that the discussions list should be + // displayed as a pane, hidden on the side of the screen. Also set the + // application back button's target as the discussions controller. + this.controllerFor('index').set('paned', true); + this.controllerFor('application').set('backButtonTarget', this.controllerFor('index')); }, actions: { queryParamsDidChange: function(params) { - // We're only interested in changes to the ?start param, and we're - // not interested if nothing has actually changed. If the start - // param has changed, we want to tell the controller to load posts - // near it. - if (! params.start || params.start == this.get('controller.start') || ! this.get('controller.loaded')) { - return; - } - this.get('controller').send('jumpToNumber', params.start); + // If the ?start param has changed, we want to tell the view to + // tell the streamContent component to jump to this start point. + // We postpone running this code until the next run loop because + // when transitioning directly from one discussion to another, + // queryParamsDidChange is fired before the controller is reset. + // Thus, controller.loaded would still be true and the + // startWasChanged event would be triggered inappropriately. + var controller = this.get('controller'), + oldStart = this.get('controller.start'); + Ember.run.next(function() { + if (! params.start || ! controller || ! controller.get('loaded') || params.start == oldStart) { + return; + } + controller.trigger('startWasChanged', params.start); + }); }, willTransition: function(transition) { - // If we're going to transition out, we need to abort any unfinished - // AJAX requests. We need to do this because sometimes a transition - // to another discussion will happen very rapidly (i.e. when using - // the arrow buttons on the result stream.) If a previous - // discussion's posts finish loading while displaying a new - // discussion, strange things will happen. - this.store.adapterFor('discussion').xhr.forEach(function(xhr) { - xhr.abort(); - }); + // When we transition away from this discussion, we want to hide + // the discussions list pane. This means that when the user + // selects a different discussion within the pane, the pane will + // slide away. + this.controllerFor('index').set('paneShowing', false); } } diff --git a/framework/core/ember/app/routes/discussions/index.js b/framework/core/ember/app/routes/discussions/index.js deleted file mode 100644 index 41b5f5b11..000000000 --- a/framework/core/ember/app/routes/discussions/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import Ember from 'ember'; - -export default Ember.Route.extend({ - - setupController: function(controller, model) { - this.controllerFor('discussions').set('paneShowing', false); - this.controllerFor('discussions').set('paned', false); - this.controllerFor('application').set('showDiscussionStream', false); - this._super(controller, model); - } - -}); diff --git a/framework/core/ember/app/routes/discussions.js b/framework/core/ember/app/routes/index.js similarity index 99% rename from framework/core/ember/app/routes/discussions.js rename to framework/core/ember/app/routes/index.js index b3f55afa8..ef31d3be6 100644 --- a/framework/core/ember/app/routes/discussions.js +++ b/framework/core/ember/app/routes/index.js @@ -8,7 +8,7 @@ export default Ember.Route.extend({ if ( ! model.get('length')) { controller.set('resultsLoading', true); - + controller.getResults().then(function(results) { controller .set('resultsLoading', false) diff --git a/framework/core/ember/app/routes/index/index.js b/framework/core/ember/app/routes/index/index.js new file mode 100644 index 000000000..8cd8adfc1 --- /dev/null +++ b/framework/core/ember/app/routes/index/index.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + + // When we enter the discussions list view, we no longer want the + // discussions list to be in pane mode. + setupController: function(controller, model) { + this.controllerFor('index').set('paned', false); + this.controllerFor('index').set('paneShowing', false); + this._super(controller, model); + } + +}); diff --git a/framework/core/ember/app/styles/app.less b/framework/core/ember/app/styles/app.less index cd63f717d..6ba7349be 100644 --- a/framework/core/ember/app/styles/app.less +++ b/framework/core/ember/app/styles/app.less @@ -1,12 +1,24 @@ +// This files is where our LESS journey begins. + +// We begin by importing our own configuration variables, which are used all +// throughout the stylesheets. These pertain to // @import "config-default.less"; @import "config.less"; -@flarum-base: "flarum/"; -@bootstrap-base: "../../bower_components/bootstrap/less/"; +@flarum-base: "flarum/"; +@bootstrap-base: "../../bower_components/bootstrap/less/"; @font-awesome-base: "../../bower_components/font-awesome/less/"; @import "bootstrap/bootstrap.less"; -@import "@{font-awesome-base}font-awesome.less"; +// We want to specify the @fa-font-path variable AFTER we import font awesome +// so that it overrides the default definition. +@import "@{font-awesome-base}font-awesome.less"; @fa-font-path: "../font-awesome/fonts"; +// Finally, with our vendor CSS loaded, we can import Flarum-specific stuff. +@import "@{flarum-base}components.less"; +@import "@{flarum-base}layout.less"; + +@import "@{flarum-base}index.less"; +@import "@{flarum-base}discussion.less"; \ No newline at end of file diff --git a/framework/core/ember/app/styles/bootstrap/bootstrap.less b/framework/core/ember/app/styles/bootstrap/bootstrap.less index 18e83472b..de196ca7f 100644 --- a/framework/core/ember/app/styles/bootstrap/bootstrap.less +++ b/framework/core/ember/app/styles/bootstrap/bootstrap.less @@ -25,19 +25,19 @@ // @import "@{bootstrap-base}navs.less"; // @import "@{bootstrap-base}navbar.less"; // @import "@{bootstrap-base}breadcrumbs.less"; -@import "@{bootstrap-base}pagination.less"; +// @import "@{bootstrap-base}pagination.less"; // @import "@{bootstrap-base}pager.less"; // @import "@{bootstrap-base}labels.less"; // @import "@{bootstrap-base}badges.less"; // @import "@{bootstrap-base}jumbotron.less"; // @import "@{bootstrap-base}thumbnails.less"; // @import "@{bootstrap-base}alerts.less"; -@import "@{bootstrap-base}progress-bars.less"; +// @import "@{bootstrap-base}progress-bars.less"; // @import "@{bootstrap-base}media.less"; // @import "@{bootstrap-base}list-group.less"; // @import "@{bootstrap-base}panels.less"; // @import "@{bootstrap-base}wells.less"; -@import "@{bootstrap-base}close.less"; +// @import "@{bootstrap-base}close.less"; // Components w/ JavaScript @import "@{bootstrap-base}modals.less"; diff --git a/framework/core/ember/app/styles/bootstrap/variables.less b/framework/core/ember/app/styles/bootstrap/variables.less index c25915906..9fa87c801 100644 --- a/framework/core/ember/app/styles/bootstrap/variables.less +++ b/framework/core/ember/app/styles/bootstrap/variables.less @@ -1 +1,19 @@ -@btn-primary-bg: @fl-primary-color; \ No newline at end of file +@brand-primary: @fl-primary-color; + +@body-bg: @fl-body-bg; +@text-color: @fl-body-color; +@font-size-base: 13px; + +@font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + +@padding-base-vertical: 8px; +@padding-base-horizontal: 13px; + +@btn-default-bg: @fl-body-control-bg; +@btn-default-color: @fl-body-control-color; + +@input-bg: @fl-body-control-bg; +@input-border-focus: darken(@fl-body-control-bg, 5%); +@input-border: @fl-body-control-bg; +@input-color: @fl-body-control-color; +@input-color-placeholder: @fl-body-control-color; \ No newline at end of file diff --git a/framework/core/ember/app/styles/config.less b/framework/core/ember/app/styles/config.less index 83426b9c4..bc1b75392 100644 --- a/framework/core/ember/app/styles/config.less +++ b/framework/core/ember/app/styles/config.less @@ -1,4 +1,4 @@ -@fl-primary-color: #4d698e; +@fl-primary-color: #4d698e;//#E7562E; @fl-secondary-color: #edf2f7; @fl-dark-hdr: false; @@ -7,7 +7,7 @@ @fl-body-bg: #fff; @fl-body-color: #444; -@fl-body-muted-color: hsl(hue(@fl-secondary-color), 18%, 72%); // todo +@fl-body-muted-color: hsv(hue(@fl-secondary-color), 18%, 72%); // todo @fl-body-muted-more-color: #bbb; @fl-body-heading-color: @fl-body-color; @@ -20,14 +20,24 @@ // --------------------------------- // HEADER -@fl-hdr-light-bg: @fl-body-bg; -@fl-hdr-light-color: @fl-primary-color; -@fl-hdr-light-muted-color: @fl-body-muted-color; -@fl-hdr-light-control-bg: @fl-body-control-bg; -@fl-hdr-light-control-color: @fl-body-control-color; +.define-hdr-variables(@fl-dark-hdr); +.define-hdr-variables(false) { + @fl-hdr-bg: @fl-body-bg; + @fl-hdr-color: @fl-primary-color; + @fl-hdr-muted-color: @fl-body-muted-color; + @fl-hdr-control-bg: @fl-body-control-bg; + @fl-hdr-control-color: @fl-body-control-color; -@fl-hdr-dark-bg: @fl-primary-color; -@fl-hdr-dark-color: #fff; -@fl-hdr-dark-muted-color: fade(#fff, 0.5); -@fl-hdr-dark-control-bg: fade(#000, 0.1); -@fl-hdr-dark-control-color: #fff; \ No newline at end of file + @fl-body-hero-bg: @fl-primary-color; + @fl-body-hero-color: #fff; +} +.define-hdr-variables(true) { + @fl-hdr-bg: @fl-primary-color; + @fl-hdr-color: #fff; + @fl-hdr-muted-color: fade(#fff, 0.5); + @fl-hdr-control-bg: fade(#000, 0.1); + @fl-hdr-control-color: #fff; + + @fl-body-hero-bg: @fl-secondary-color; + @fl-body-hero-color: @fl-body-muted-color; +} \ No newline at end of file diff --git a/framework/core/ember/app/styles/flarum/components.less b/framework/core/ember/app/styles/flarum/components.less new file mode 100644 index 000000000..1787924a8 --- /dev/null +++ b/framework/core/ember/app/styles/flarum/components.less @@ -0,0 +1,224 @@ +// ------------------------------------ +// Buttons + +.btn { + border: 0; + .box-shadow(none); + line-height: 20px; + + & .fa { + font-size: 14px; + } +} +.btn-group .btn + .btn { + margin-left: 1px; +} +.btn-icon { + padding-left: 9px; + padding-right: 9px; +} +.btn-link { + color: @fl-body-muted-color; + + &:hover, + &:focus { + text-decoration: none; + } +} +.btn-primary { + font-weight: bold; + & .icon-glyph { + display: none; + } +} + +// Redefine Bootstrap's mixin to make some general changes +.button-variant(@color; @background; @border) { + &:hover, + &:focus, + &.focus, + &:active, + &.active, + .open > .dropdown-toggle& { + background-color: darken(@background, 5%); + } + &.active { + .box-shadow(none); + } +} + +// Little round icon buttons +.btn-icon.btn-sm { + border-radius: 12px; + height: 24px; + width: 24px; + text-align: center; + padding: 3px 0; + + & .label, & .icon-caret { + display: none; + } + & .fa-ellipsis-v { + font-size: 17px; + vertical-align: middle; + } +} + +// Buttons that blend into the background +.btn-blend { + background: transparent; + &:hover { + background: @fl-body-control-bg; + } +} + +// ------------------------------------ +// Form Controls + +.form-control { + .box-shadow(none); + &:focus, + &.focus { + background-color: @input-border-focus; + .box-shadow(none); + } +} + +// Search inputs +// @todo Extract some of this into header-specific definitions +.search-input { + margin-right: 10px; + &:before { + .fa(); + content: @fa-var-search; + float: left; + margin-right: -36px; + width: 36px; + font-size: 14px; + text-align: center; + color: @fl-body-muted-color; + position: relative; + padding: @padding-base-vertical - 1 0; + line-height: @line-height-base; + pointer-events: none; + } +} +.search-input .form-control { + float: left; + width: 225px; + padding-left: 36px; + padding-right: 36px; + .transition(~"all 0.4s"); + + &:focus { + width: 400px; + } +} +.search-input .clear { + float: left; + margin-left: -36px; + vertical-align: top; + .scale(0.001); + .transition(~"transform 0.15s"); +} +.search-input.clearable .clear { + .scale(1); +} + +// Select inputs +.select-input { + display: inline-block; + vertical-align: middle; +} +.select-input select { + display: inline-block; + width: auto; + -webkit-appearance: none; + padding-right: @padding-base-horizontal + 16; + cursor: pointer; +} +.select-input .fa { + margin-left: -@padding-base-horizontal - 16; + pointer-events: none; + color: @fl-body-muted-color; +} + +// ------------------------------------ +// Dropdown Menus + +.dropdown-menu { + border: 0; + padding: 8px 0; + margin-top: 7px; + .box-shadow(0 2px 6px rgba(0, 0, 0, 0.25)); + + & > li > a { + padding: 8px 15px; + color: @fl-body-color; + &:hover, &:focus { + color: @fl-body-color; + background-color: @fl-secondary-color; + } + & .fa { + margin-right: 5px; + font-size: 14px; + } + } + & .divider { + margin: 10px 0; + background-color: darken(@fl-secondary-color, 2%); + } +} +.dropdown-split.item-count-1 { + & .btn { + border-radius: @border-radius-base !important; + } + & .dropdown-toggle { + display: none; + } +} + +// ------------------------------------ +// Tooltips + +.tooltip-inner { + padding: 5px 10px; +} + +// ------------------------------------ +// Loading Indicators + +.loading-indicator { + position: relative; + color: @fl-primary-color; +} +.loading-indicator-block { + height: 100px; +} + +// ------------------------------------ +// Avatars + +.avatar-size(@size) { + width: @size; + height: @size; + border-radius: @size / 2; + font-size: @size / 2; + line-height: @size; +} +.avatar { + display: inline-block; + color: #fff; + font-weight: 300; + text-align: center; + vertical-align: top; + .avatar-size(48px); + + & img { + display: inline-block; + width: 100%; + height: 100%; + border-radius: 100%; + vertical-align: top; + } +} \ No newline at end of file diff --git a/framework/core/ember/app/styles/flarum/composer.less b/framework/core/ember/app/styles/flarum/composer.less new file mode 100644 index 000000000..f33c744b2 --- /dev/null +++ b/framework/core/ember/app/styles/flarum/composer.less @@ -0,0 +1,61 @@ +// ------------------------------------ +// Composer + +.composer-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: @zindex-navbar-fixed; + pointer-events: none; +} +.composer { + pointer-events: auto; + margin-left: 200px; + .box-shadow(0 2px 6px rgba(0, 0, 0, 0.25)); + border-radius: 4px 4px 0 0; + background: rgba(255, 255, 255, 0.98); + transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray +} +.composer-content { + padding: 0 20px 15px; +} +.composer-handle { + height: 20px; + cursor: row-resize; +} +.composer-controls { + float: right; + margin: -5px 15px 0 0; +} +.composer-avatar { + float: left; + width: 64px; + height: 64px; +} +.composer-body { + margin-left: 84px; + + & h3 { + margin: 5px 0 10px; + color: @fl-body-muted-color; + font-size: 16px; + font-weight: normal; + } +} +.composer-editor textarea { + background: none; + border-radius: 0; + padding: 0; + margin-bottom: 10px; + height: 200px; + border: 0; + resize: none; + color: @fl-body-color; + font-size: 15px; + line-height: 1.6; + + &:focus { + background: none; + } +} \ No newline at end of file diff --git a/framework/core/ember/app/styles/flarum/discussion.less b/framework/core/ember/app/styles/flarum/discussion.less new file mode 100644 index 000000000..fd9af5dee --- /dev/null +++ b/framework/core/ember/app/styles/flarum/discussion.less @@ -0,0 +1,232 @@ +// ------------------------------------ +// Sidebar + +.discussion-nav { + float: right; + + &, & > ul { + width: 150px; + } + & > ul { + position: fixed; + margin: 30px 0 0; + padding: 0; + list-style-type: none; + + & > li { + margin-bottom: 10px; + } + } + & .btn-group, & .btn { + width: 100%; + } + & .btn-group:not(.item-count-1) { + & .btn { + width: 80%; + } + & .dropdown-toggle { + width: 19%; + } + } +} + +// ------------------------------------ +// Stream + +.discussion-posts { + margin-top: 40px; + margin-right: 225px; + & .item { + margin-bottom: 40px; + } +} +.gap { + padding: 30px 0; + text-align: center; + color: #aaa; + cursor: pointer; + border: 2px dashed @fl-body-bg; + background: #f2f2f2; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + overflow: hidden; + position: relative; + .transition(padding 0.2s); + + &:hover, &.loading, &.active { + padding: 50px 0; + &.up:before, &.down:after { + opacity: 1; + } + } + &.loading { + .transition(none); + } + &:before, &:after { + font-family: 'FontAwesome'; + display: block; + opacity: 0; + transition: opacity 0.2s; + height: 15px; + color: #aaa; + } + &.up:before { + content: '\f077'; + margin-top: -25px; + margin-bottom: 10px; + } + &.down:after { + content: '\f078'; + margin-bottom: -25px; + margin-top: 10px; + } + &:only-child { + background: none; + border: 0; + color: @fl-primary-color; + &:before, &:after { + display: none; + } + } +} + +// ------------------------------------ +// Posts + +.post { + padding-left: 90px; + transition: 0.2s box-shadow; + + & .contextual-controls { + float: right; + margin: -2px 0 0 10px; + visibility: hidden; + } + &:hover .contextual-controls, & .contextual-controls.open { + visibility: visible; + } +} +.item.highlight .post { + border: 10px solid rgba(255, 255, 0, 0.2); + border-radius: 10px; + padding: 15px 15px 0 105px; + margin: -25px -25px -10px -25px; +} + +.post-header { + margin-bottom: 10px; + color: @fl-body-muted-color; + + & .user { + margin: 0; + display: inline; + font-weight: bold; + font-size: 18px; + &, & a { + color: @fl-body-heading-color; + } + } + & .avatar { + margin-left: -90px; + float: left; + .avatar-size(64px); + } + & .time { + margin-left: 10px; + &, & a { + color: @fl-body-muted-color; + } + } +} +.post-body { + font-size: 15px; + line-height: 1.6; + padding-bottom: 1px; +} +.post-edited { + margin-left: 10px; + font-size: 14px; +} + +// ------------------------------------ +// Scrubber + +@media (min-width: @screen-md-min) { + .stream-scrubber { + margin: 30px 0 0 0; + } + .scrubber { + & a { + margin-left: -5px; + color: @fl-body-muted-color; + & .fa { + font-size: 14px; + margin-right: 2px; + } + &:hover, &:focus { + text-decoration: none; + color: @link-hover-color; + } + } + } + .scrubber-scrollbar { + margin: 8px 0 8px 3px; + height: 300px; + min-height: 50px; // JavaScript sets a max-height + position: relative; + } + .scrubber-before, .scrubber-after { + border-left: 1px solid darken(@fl-secondary-color, 2%); + } + .scrubber-slider { + position: relative; + width: 100%; + padding: 5px 0; + } + .scrubber-handle { + height: 100%; + width: 5px; + background: @fl-primary-color; + border-radius: 4px; + float: left; + margin-left: -2px; + transition: background 0.2s; + .disabled & { + background: @fl-secondary-color; + } + } + .scrubber-info { + height: (2em * @line-height-base); + margin-top: (-1em * @line-height-base); + position: absolute; + top: 50%; + width: 100%; + left: 15px; + & strong { + display: block; + } + & .description { + color: @fl-body-muted-color; + } + } + .scrubber-highlights { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + list-style-type: none; + pointer-events: none; + } + .scrubber-highlights li { + position: absolute; + right: -6px; + background: #fc0; + height: 8px; + width: 13px; + border-radius: 4px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15), inset 0 0 0 1px rgba(255, 255, 255, 0.5); + opacity: 0.99; + } +} \ No newline at end of file diff --git a/framework/core/ember/app/styles/flarum/index.less b/framework/core/ember/app/styles/flarum/index.less new file mode 100644 index 000000000..dd4467969 --- /dev/null +++ b/framework/core/ember/app/styles/flarum/index.less @@ -0,0 +1,253 @@ +// ------------------------------------ +// Sidebar + +.index-nav { + float: left; + + &, & > ul { + width: 175px; + } + & > ul { + margin: 30px 0 0; + padding: 0; + list-style-type: none; + + &.affix { + top: 56px; + } + & > li { + margin-bottom: 10px; + } + } + & .new-discussion { + display: block; + margin-bottom: 20px; + } + + // Expand the dropdown-select component into a normal nav list + // @todo Extract this into a mixin as we'll need to do it elsewhere. + & .dropdown-select { + display: block; + + & .dropdown-toggle { + display: none; + } + & .dropdown-menu { + display: block; + border: 0; + width: auto; + margin: 0; + padding: 0; + min-width: 0; + float: none; + position: static; + background: none; + .box-shadow(none); + & > li > a { + padding: 8px 0; + color: @fl-body-muted-color; + & .fa { + margin-right: 8px; + font-size: 15px; + } + &:hover { + background: none; + color: @link-hover-color; + } + } + & > li.active > a { + background: none; + color: @fl-primary-color; + font-weight: bold; + } + } + } +} + +// ------------------------------------ +// Results + +.index-results { + margin-top: 30px; + margin-left: 225px; + & .loading-indicator { + height: 40px; + } +} +.index-toolbar { + margin-bottom: 15px; +} +.index-toolbar-view { + display: inline-block; + & .control-show { + margin-right: 10px; + } +} +.index-toolbar-action { + float: right; +} + +// ------------------------------------ +// Discussions Pane + +@index-pane-width: 440px; + +.index-area { + left: -@index-pane-width; + width: 100%; + + &.paned { + position: fixed; + z-index: @zindex-navbar-fixed + 1; + overflow: auto; + top: 56px; + bottom: 0; + width: @index-pane-width; + background: #fff; + padding-bottom: 200px; + .box-shadow(2px 2px 6px -2px rgba(0, 0, 0, 0.25)); + .transition(left 0.2s); + + &.showing, .with-pane & { + left: 0; + } + & .container { + width: auto; + margin: 0; + padding: 0 !important; + } + & .index-results { + margin: 0; + } + & .hero, & .index-nav, & .index-toolbar { + display: none; + } + & .discussions-list > li { + margin: 0; + padding-left: 65px + 15px; + padding-right: 65px + 25px; + &.active { + background: @fl-secondary-color; + } + } + & .discussion-summary { + & .title { + font-size: 15px; + } + & .count strong { + font-size: 18px; + } + } + } +} + +// When the pane is pinned, move the other page content inwards +.global-main, .global-footer { + .transition(margin-left 0.2s); + + .with-pane & { + margin-left: @index-pane-width; + & .container { + max-width: 100%; + padding: 0 30px; + } + } +} +.global-header .container { + .transition(width 0.2s); + + .with-pane & { + width: 100%; + } +} + +// ------------------------------------ +// Discussions List + +.discussions-list { + margin: 0; + padding: 0; + list-style-type: none; + position: relative; + + & > li { + margin-right: -25px; + padding-right: 65px + 25px; + & .contextual-controls { + position: absolute; + right: 0; + top: 18px; + visibility: hidden; + } + &:hover .contextual-controls, & .contextual-controls.open { + visibility: visible; + } + } +} +.discussion-summary { + padding-left: 65px; + padding-right: 65px; + position: relative; + + & .author { + float: left; + margin-left: -65px; + margin-top: 18px; + } + & .info { + display: inline-block; + width: 100%; + margin-right: -65px; + color: @fl-body-muted-color; + padding: 20px 0; + + &:hover, &:active, &.active, &:focus { + text-decoration: none; + & .title { + text-decoration: underline; + } + } + &.active .title { + text-decoration: none; + } + } + & .title { + margin: 0 0 5px; + font-size: 16px; + line-height: 1.3; + &, & a { + font-weight: normal; + color: @fl-body-muted-color; + } + .unread&, .unread& a { + font-weight: bold; + color: @fl-body-heading-color; + } + } + & .count { + float: right; + margin-top: 18px; + margin-right: -65px; + width: 60px; + text-align: center; + text-transform: uppercase; + color: @fl-body-muted-color; + font-size: 11px; + + & strong { + font-size: 20px; + display: block; + font-weight: 300; + } + .unread&, .unread& strong { + color: @fl-body-heading-color; + font-weight: bold; + cursor: pointer; + } + } +} + +.load-more { + text-align: center; + margin-top: 10px; +} \ No newline at end of file diff --git a/framework/core/ember/app/styles/flarum/layout.less b/framework/core/ember/app/styles/flarum/layout.less new file mode 100644 index 000000000..911722059 --- /dev/null +++ b/framework/core/ember/app/styles/flarum/layout.less @@ -0,0 +1,158 @@ +body { + background: @fl-body-bg; + color: @fl-body-color; + padding-top: 56px; +} +.container-narrow { + max-width: 500px; + margin: 0 auto; +} + +// ------------------------------------ +// Header + +.global-header { + background: fade(@fl-hdr-bg, 98%); + transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray + padding: 10px; + height: 56px; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: @zindex-navbar-fixed; + .clearfix(); + .transition(box-shadow 0.2s); + + .scrolled & { + .box-shadow(0 2px 6px rgba(0, 0, 0, 0.15)); + } +} +.header-controls { + margin: 0; + padding: 0; + list-style-type: none; + &, & > li { + display: inline-block; + vertical-align: top; + } +} +.header-primary { + float: left; + & h1 { + display: inline-block; + vertical-align: top; + } +} +.header-title { + font-size: 18px; + font-weight: normal; + margin: 0; + line-height: 36px; + &, & a { + color: @fl-hdr-color; + } +} +.header-secondary { + float: right; +} + +// Back button +// @todo Lots of !importants in here, could we be more specific? +.back-button { + float: left; + margin-right: 25px; + + & .back { + z-index: 3; // z-index of an active .btn-group .btn is 2 + border-radius: @border-radius-base !important; + .transition(border-radius 0.2s); + } + & .pin { + opacity: 0; + margin-left: -36px !important; + .transition(~"opacity 0.2s, margin-left 0.2s"); + + &:not(.active) .fa { + .rotate(45deg); + } + } + &.active { + & .back { + border-radius: @border-radius-base 0 0 @border-radius-base !important; + } + & .pin { + opacity: 1; + margin-left: 1px !important; + } + } +} + +// ------------------------------------ +// Main + +.global-main, .paned { + border-top: 1px solid @fl-secondary-color; +} + +// Hero +.hero { + background: @fl-body-hero-bg; + color: @fl-body-hero-color; + margin-top: -1px; + text-align: center; + padding: 30px 0; +} +.hero .close { + float: right; + margin-top: -10px; + color: #fff; + opacity: 0.5; +} +.hero h2 { + margin: 0; + font-size: 22px; + font-weight: normal; +} +.hero p { + margin: 10px 0 0; +} + +// ------------------------------------ +// Footer + +.global-footer { + margin: 100px 0 20px; + color: @fl-body-muted-more-color; + .clearfix(); +} +.footer-primary, .footer-secondary { + margin: 0; + padding: 0; + list-style-type: none; + + & > li { + display: inline-block; + vertical-align: middle; + } + & a { + color: @fl-body-muted-more-color; + &:hover, + &:focus { + text-decoration: none; + color: @link-hover-color; + } + } +} +.footer-primary { + display: inline-block; + & > li { + margin-right: 15px; + } +} +.footer-secondary { + float: right; + & > li { + margin-left: 15px; + } +} \ No newline at end of file diff --git a/framework/core/ember/app/templates/application.hbs b/framework/core/ember/app/templates/application.hbs index 483f43f9c..a4bc0f819 100644 --- a/framework/core/ember/app/templates/application.hbs +++ b/framework/core/ember/app/templates/application.hbs @@ -1,20 +1,14 @@ -
+
-
{{outlet "modal"}} -{{render "composer"}} - -
+
{{#each alert in alerts}} {{alert-message message=alert}} {{/each}} diff --git a/framework/core/ember/app/templates/components/back-button.hbs b/framework/core/ember/app/templates/components/back-button.hbs new file mode 100644 index 000000000..5580f43bf --- /dev/null +++ b/framework/core/ember/app/templates/components/back-button.hbs @@ -0,0 +1,8 @@ +{{#if target}} +
+ + {{#if target.paned}} + + {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/framework/core/ember/app/templates/components/discussions/discussion-listing.hbs b/framework/core/ember/app/templates/components/discussions/discussion-listing.hbs index 967931c0c..01f3da42c 100644 --- a/framework/core/ember/app/templates/components/discussions/discussion-listing.hbs +++ b/framework/core/ember/app/templates/components/discussions/discussion-listing.hbs @@ -1,47 +1,46 @@ -{{ui/controls/dropdown-button items=controls class="contextual-controls"}} +{{ui/controls/dropdown-button + items=controls + class="contextual-controls" + buttonClass="btn btn-default btn-icon btn-sm btn-blend" + buttonClick="populateControls" + menuClass="pull-right"}} -
+{{#link-to "user" discussion.startUser class="author"}}{{user-avatar discussion.startUser}}{{/link-to}} - {{#link-to "user" discussion.startUser}}{{user-avatar discussion.startUser}}{{/link-to}} +
+ {{#each badge in badges}} + {{fl-badge badge}} + {{/each}} +
-
- {{#each badge in badges}} - {{fl-badge badge}} +{{#link-to "discussion" discussion.content (query-params searchQuery=searchQuery start=start) current-when="discussion" class="info"}} +

{{highlight-words discussion.title searchQuery}}

+ + {{#if displayLastPost}} + {{discussion.lastUser.username}} replied + {{human-time discussion.lastTime}} + {{else}} + {{discussion.startUser.username}} started + {{human-time discussion.startTime}} + {{/if}} + +{{/link-to}} + +
+ {{#if displayUnread}} + {{abbreviate-number discussion.unreadCount}} unread + {{else}} + {{abbreviate-number discussion.repliesCount}} replies + {{/if}} +
+ +{{#if relevantPosts}} +
+ {{#each post in relevantPosts}} + {{#link-to "discussion" discussion.content (query-params start=post.number) class="post item"}} + {{user-avatar post.user class="avatar-thumb"}} + {{highlight-words post.relevantContent searchQuery}} + {{/link-to}} {{/each}}
- - {{#link-to "discussion" discussion.content (query-params searchQuery=searchQuery) class="info"}} - -

{{highlight-words discussion.title searchQuery}}

-
- - {{#if displayStartPosts}} - {{#link-to "user" discussion.startUser}}{{discussion.startUser.username}}{{/link-to}} posted - {{#link-to "discussion" discussion.content}}{{abbreviate-time discussion.startTime}}{{/link-to}} - {{else}} - {{#link-to "user" discussion.lastUser}}{{discussion.lastUser.username}}{{/link-to}} posted - {{#link-to "discussion" discussion.content (query-params start="last")}}{{abbreviate-time discussion.lastTime}}{{/link-to}} - {{/if}} - - {{/link-to}} - -
- {{#if discussion.isUnread}} - {{abbreviate-number discussion.unreadCount}} unread - {{else}} - {{abbreviate-number discussion.repliesCount}} replies - {{/if}} -
- - {{#if relevantPosts}} -
- {{#each post in relevantPosts}} - {{#link-to "discussion" discussion.content (query-params start=post.number) class="post item"}} - {{user-avatar post.user class="avatar-thumb"}} - {{highlight-words post.relevantContent searchQuery}} - {{/link-to}} - {{/each}} -
- {{/if}} - -
+{{/if}} diff --git a/framework/core/ember/app/templates/components/discussions/post-content-comment.hbs b/framework/core/ember/app/templates/components/discussions/post-content-comment.hbs index cbfef68a6..5d9f5f9a4 100644 --- a/framework/core/ember/app/templates/components/discussions/post-content-comment.hbs +++ b/framework/core/ember/app/templates/components/discussions/post-content-comment.hbs @@ -2,12 +2,12 @@ {{fa-icon "trash-o" class="post-icon"}} {{/if}} -
+

{{#link-to "user" post.user}}{{user-avatar post.user}} {{post.user.username}}{{/link-to}}

{{#link-to "discussion" post.discussion (query-params start=post.number) class="time"}} - {{abbreviate-time post.time}} + {{human-time post.time}} {{/link-to}} {{#if post.editTime}} {{fa-icon "pencil"}} diff --git a/framework/core/ember/app/templates/components/discussions/post-full.hbs b/framework/core/ember/app/templates/components/discussions/post-full.hbs deleted file mode 100644 index fc7df099f..000000000 --- a/framework/core/ember/app/templates/components/discussions/post-full.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{ui/controls/dropdown-button items=controls}} - -{{dynamic-component type=contentComponent post=post}} diff --git a/framework/core/ember/app/templates/components/discussions/post-wrapper.hbs b/framework/core/ember/app/templates/components/discussions/post-wrapper.hbs new file mode 100644 index 000000000..098535f8a --- /dev/null +++ b/framework/core/ember/app/templates/components/discussions/post-wrapper.hbs @@ -0,0 +1,8 @@ +{{ui/controls/dropdown-button + items=controls + class="contextual-controls" + buttonClass="btn btn-default btn-icon btn-sm btn-blend" + buttonClick="populateControls" + menuClass="pull-right"}} + +{{dynamic-component type=contentComponent post=post}} diff --git a/framework/core/ember/app/templates/components/discussions/scrollbar.hbs b/framework/core/ember/app/templates/components/discussions/scrollbar.hbs deleted file mode 100644 index 8585e3aa1..000000000 --- a/framework/core/ember/app/templates/components/discussions/scrollbar.hbs +++ /dev/null @@ -1,20 +0,0 @@ -Original Post -
-
-
-
-
- 0 of {{postStream.count}} posts - -
-
-
- {{#if relevantPostRanges}} -
    - {{#each range in relevantPostRanges}} -
  • - {{/each}} -
- {{/if}} -
-Now diff --git a/framework/core/ember/app/templates/components/discussions/stream-content.hbs b/framework/core/ember/app/templates/components/discussions/stream-content.hbs new file mode 100644 index 000000000..65e9cebbf --- /dev/null +++ b/framework/core/ember/app/templates/components/discussions/stream-content.hbs @@ -0,0 +1,7 @@ +{{#each item in stream}} + {{#discussions/stream-item item=item stream=stream loadRange="loadRange"}} + {{#if item.content}} + {{dynamic-component type=component content=item.content}} + {{/if}} + {{/discussions/stream-item}} +{{/each}} \ No newline at end of file diff --git a/framework/core/ember/app/templates/components/discussions/stream-scrubber.hbs b/framework/core/ember/app/templates/components/discussions/stream-scrubber.hbs new file mode 100644 index 000000000..09dbcd34b --- /dev/null +++ b/framework/core/ember/app/templates/components/discussions/stream-scrubber.hbs @@ -0,0 +1,20 @@ +{{fa-icon "angle-double-up"}} Original Post +
+
+
+
+ {{#if loaded}} +
+ {{visibleIndex}} of {{count}} posts + {{description}} +
+ {{/if}} +
+
+
    + {{#each index in relevantPostIndexes}} +
  • + {{/each}} +
+
+{{fa-icon "angle-double-down"}} Now diff --git a/framework/core/ember/app/templates/components/ui/controls/dropdown-button.hbs b/framework/core/ember/app/templates/components/ui/controls/dropdown-button.hbs index d48fb2bc2..015acda4f 100644 --- a/framework/core/ember/app/templates/components/ui/controls/dropdown-button.hbs +++ b/framework/core/ember/app/templates/components/ui/controls/dropdown-button.hbs @@ -1,8 +1,6 @@ -{{#if items}} - - {{ui/controls/item-list items=items class=dropdownMenuClass}} -{{/if}} \ No newline at end of file + + {{fa-icon icon class="icon-glyph"}} + {{title}} + {{fa-icon "caret-down" class="icon-caret"}} + +{{ui/controls/item-list items=items class=dropdownMenuClass}} diff --git a/framework/core/ember/app/templates/components/ui/controls/dropdown-select.hbs b/framework/core/ember/app/templates/components/ui/controls/dropdown-select.hbs index 9a57f27c9..7885e564b 100644 --- a/framework/core/ember/app/templates/components/ui/controls/dropdown-select.hbs +++ b/framework/core/ember/app/templates/components/ui/controls/dropdown-select.hbs @@ -1,7 +1,7 @@ {{#if items}} - + {{ui/controls/item-list items=items class=dropdownMenuClass viewName="menu"}} {{/if}} \ No newline at end of file diff --git a/framework/core/ember/app/templates/components/ui/controls/search-input.hbs b/framework/core/ember/app/templates/components/ui/controls/search-input.hbs index 8dfa49f4a..7d7ce7782 100644 --- a/framework/core/ember/app/templates/components/ui/controls/search-input.hbs +++ b/framework/core/ember/app/templates/components/ui/controls/search-input.hbs @@ -1,2 +1,2 @@ {{input type="text" placeholder=placeholder class="form-control" value=value action="search"}} - diff --git a/framework/core/ember/app/templates/components/ui/controls/text-editor.hbs b/framework/core/ember/app/templates/components/ui/controls/text-editor.hbs index 40d299326..82950d83e 100644 --- a/framework/core/ember/app/templates/components/ui/controls/text-editor.hbs +++ b/framework/core/ember/app/templates/components/ui/controls/text-editor.hbs @@ -1,11 +1,9 @@
-
- -
- - -
+ +
+ +
diff --git a/framework/core/ember/app/templates/components/welcome-hero.hbs b/framework/core/ember/app/templates/components/welcome-hero.hbs new file mode 100644 index 000000000..a16b99660 --- /dev/null +++ b/framework/core/ember/app/templates/components/welcome-hero.hbs @@ -0,0 +1,7 @@ +
+ +
+

Welcome to Flarum Prototype Forum

+

Thanks for stopping by! Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Maecenas sed diam eget risus varius blandit sit amet non magna.

+
+
\ No newline at end of file diff --git a/framework/core/ember/app/templates/composer.hbs b/framework/core/ember/app/templates/composer.hbs index d76e2a12a..d39ba2a36 100644 --- a/framework/core/ember/app/templates/composer.hbs +++ b/framework/core/ember/app/templates/composer.hbs @@ -1,19 +1,29 @@
-
- {{fa-icon "fa-expand"}} - {{fa-icon "fa-caret-square-o-down"}} - {{fa-icon "fa-times"}} + -{{user-avatar user}} +
-
+ {{user-avatar user class="composer-avatar"}} -

{{{title}}}

+
+ +

{{{title}}}

+ +
+ {{ui/controls/text-editor placeholder=""}} +
-
- {{ui/controls/text-editor placeholder=""}}
-
+
\ No newline at end of file diff --git a/framework/core/ember/app/templates/discussion.hbs b/framework/core/ember/app/templates/discussion.hbs index 7dc434675..42e8ee17d 100644 --- a/framework/core/ember/app/templates/discussion.hbs +++ b/framework/core/ember/app/templates/discussion.hbs @@ -1,15 +1,18 @@ -