diff --git a/ember/Brocfile.js b/ember/Brocfile.js index 9f03d0dd8..79dc7ace5 100644 --- a/ember/Brocfile.js +++ b/ember/Brocfile.js @@ -8,6 +8,7 @@ app.import('bower_components/bootstrap/dist/js/bootstrap.js'); app.import('bower_components/spin.js/spin.js'); app.import('bower_components/spin.js/jquery.spin.js'); app.import('bower_components/moment/moment.js'); +app.import('bower_components/jquery.hotkeys/jquery.hotkeys.js'); app.import('bower_components/font-awesome/fonts/fontawesome-webfont.eot'); app.import('bower_components/font-awesome/fonts/fontawesome-webfont.svg'); diff --git a/ember/app/components/discussions/composer-reply.js b/ember/app/components/discussions/composer-reply.js new file mode 100644 index 000000000..1978271c5 --- /dev/null +++ b/ember/app/components/discussions/composer-reply.js @@ -0,0 +1,43 @@ +import Ember from 'ember'; + +import TaggedArray from '../../utils/tagged-array'; + +var precompileTemplate = Ember.Handlebars.compile; + +export default Ember.Component.extend(Ember.Evented, { + layoutName: 'components/discussions/composer-body', + + placeholder: 'Write your reply...', + submitLabel: 'Post Reply', + value: '', + + didInsertElement: function() { + var headerItems = TaggedArray.create(); + this.trigger('populateHeader', headerItems); + this.set('headerItems', headerItems); + }, + + populateHeader: function(header) { + var title = Ember.Component.create({ + tagName: 'h3', + layout: precompileTemplate('Replying to {{component.discussion.title}}'), + component: this + }); + header.pushObjectWithTag(title, 'title'); + }, + + actions: { + submit: function(value) { + this.get('submit').call(this, value); + }, + willExit: function(abort) { + if (this.get('value') && ! confirm('You have not posted your reply. Do you wish to discard it?')) { + abort(); + } + }, + reset: function() { + this.set('loading', false); + this.set('value', ''); + }, + } +}); diff --git a/ember/app/components/ui/controls/text-editor.js b/ember/app/components/ui/controls/text-editor.js index aab281c8b..8457a2e84 100644 --- a/ember/app/components/ui/controls/text-editor.js +++ b/ember/app/components/ui/controls/text-editor.js @@ -1,9 +1,37 @@ import Ember from 'ember'; +import TaggedArray from '../../../utils/tagged-array'; +import ActionButton from './action-button'; + export default Ember.Component.extend({ - actions: { - save: function() { - this.sendAction('save', this.get('value')); - } - } + classNames: ['text-editor'], + + didInsertElement: function() { + var controlItems = TaggedArray.create(); + this.trigger('populateControls', controlItems); + this.set('controlItems', controlItems); + + var component = this; + this.$('textarea').bind('keydown', 'meta+return', function() { + component.send('submit'); + }); + }, + + populateControls: function(controls) { + var component = this; + var submit = ActionButton.create({ + label: this.get('submitLabel'), + className: 'btn btn-primary', + action: function() { + component.send('submit'); + } + }); + controls.pushObjectWithTag(submit, 'submit'); + }, + + actions: { + submit: function() { + this.sendAction('submit', this.get('value')); + } + } }); \ No newline at end of file diff --git a/ember/app/controllers/composer.js b/ember/app/controllers/composer.js index 08615abe3..f24ace0c1 100644 --- a/ember/app/controllers/composer.js +++ b/ember/app/controllers/composer.js @@ -1,41 +1,72 @@ import Ember from 'ember'; -export default Ember.Controller.extend({ +export default Ember.Controller.extend(Ember.Evented, { needs: ['index', 'application'], - user: Ember.Object.create({avatarNumber: 1}), + content: null, - discussion: null, - - showing: true, + showing: false, minimized: false, + fullScreen: false, - title: 'Replying to Some Discussion Title', + switchContent: function(newContent) { + var composer = this; + this.confirmExit().then(function() { + composer.set('content', null); + Ember.run.next(function() { + composer.set('content', newContent); + }); + }); + }, + + confirmExit: function() { + var composer = this; + var promise = new Ember.RSVP.Promise(function(resolve, reject) { + var content = composer.get('content'); + if (content) { + content.send('willExit', reject); + } + resolve(); + }); + return promise; + }, actions: { close: function() { + var composer = this; + this.confirmExit().then(function() { + composer.send('hide'); + }); + }, + hide: function() { this.set('showing', false); + this.set('fullScreen', false); + var content = this.get('content'); + if (content) { + content.send('reset'); + } }, minimize: function() { - this.set('minimized', true); + this.set('minimized', true); + this.set('fullScreen', false); }, show: function() { - this.set('minimized', false); + var composer = this; + Ember.run.next(function() { + composer.set('showing', true); + composer.set('minimized', false); + composer.trigger('focus'); + }); }, - save: function(value) { - var store = this.store; - var discussion = this.get('discussion'); - var controller = this; - - var post = store.createRecord('post', { - content: value, - discussion: discussion - }); - post.save().then(function(post) { - discussion.set('posts', discussion.get('posts')+','+post.get('id')); - controller.get('delegate').send('replyAdded', post); - }); + fullScreen: function() { + this.set('fullScreen', true); + this.set('minimized', false); + this.trigger('focus'); + }, + exitFullScreen: function() { + this.set('fullScreen', false); + this.trigger('focus'); } } diff --git a/ember/app/controllers/discussion.js b/ember/app/controllers/discussion.js index cd6e3cabe..ee35f0939 100644 --- a/ember/app/controllers/discussion.js +++ b/ember/app/controllers/discussion.js @@ -1,6 +1,8 @@ import Ember from 'ember'; import PostStream from '../models/post-stream'; +import ComposerReply from '../components/discussions/composer-reply'; +import ActionButton from '../components/ui/controls/action-button'; export default Ember.ObjectController.extend(Ember.Evented, { @@ -38,28 +40,52 @@ export default Ember.ObjectController.extend(Ember.Evented, { }); }, + saveReply: function(discussion, content) { + var controller = this; + var composer = this.get('controllers.composer'); + var stream = this.get('stream'); + + composer.set('content.loading', true); + + var post = this.store.createRecord('post', { + content: content, + discussion: discussion + }); + + var promise = post.save().then(function(post) { + if (discussion == controller.get('model')) { + discussion.set('posts', discussion.get('posts')+','+post.get('id')); + stream.set('ids', controller.get('model.postIds')); + stream.addPostToEnd(post); + } + composer.send('hide'); + }, function(reason) { + var error = reason.errors[0].detail; + alert(error); + }); + + promise.finally(function() { + composer.set('content.loading', false); + }); + + return promise; + }, + actions: { reply: function() { + var controller = this; + var discussion = this.get('model'); var composer = this.get('controllers.composer'); - // composer.beginPropertyChanges(); - composer.set('minimized', false); - composer.set('showing', true); - composer.set('title', 'Replying to '+this.get('model.title')+''); - composer.set('delegate', this); - composer.set('discussion', this.get('model')); - // composer.endPropertyChanges(); - }, - - replyAdded: function(post) { - var stream = this.get('stream'); - stream.set('ids', this.get('model.postIds')); - var index = stream.get('count') - 1; - stream.get('content').pushObject(Ember.Object.create({ - indexStart: index, - indexEnd: index, - content: post - })); - this.get('controllers.composer').set('showing', false); + if (composer.get('content.discussion') != discussion) { + composer.switchContent(ComposerReply.create({ + user: controller.get('session.user'), + discussion: discussion, + submit: function(value) { + controller.saveReply(this.get('discussion'), value); + } + })); + } + composer.send('show'); }, // This action is called when the start position of the discussion diff --git a/ember/app/routes/discussion.js b/ember/app/routes/discussion.js index 6b4373834..a818d4934 100644 --- a/ember/app/routes/discussion.js +++ b/ember/app/routes/discussion.js @@ -50,12 +50,15 @@ export default Ember.Route.extend({ }, willTransition: function() { - // 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. + // When we transition into a new 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); - } + }, + + didTransition: function() { + this.controllerFor('composer').send('minimize'); + } } }); diff --git a/ember/app/routes/index/index.js b/ember/app/routes/index/index.js index 15c977f4b..2e236f42c 100644 --- a/ember/app/routes/index/index.js +++ b/ember/app/routes/index/index.js @@ -10,6 +10,12 @@ export default Ember.Route.extend(AddCssClassToBodyMixin, { this.controllerFor('index').set('paned', false); this.controllerFor('index').set('paneShowing', false); this._super(controller, model); - } + }, + actions: { + didTransition: function() { + // @todo only if it's not a new discussion + this.controllerFor('composer').send('minimize'); + } + } }); diff --git a/ember/app/styles/flarum/composer.less b/ember/app/styles/flarum/composer.less index 8b00791b8..fc88b6fd8 100644 --- a/ember/app/styles/flarum/composer.less +++ b/ember/app/styles/flarum/composer.less @@ -19,23 +19,33 @@ margin-left: -20px; margin-right: 180px; .box-shadow(0 2px 6px rgba(0, 0, 0, 0.25)); - border-radius: 4px 4px 0 0; + border-radius: @border-radius-base @border-radius-base 0 0; background: rgba(255, 255, 255, 0.9); transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray position: relative; - .transition(~"margin-left 0.2s, margin-right 0.2s, background 0.2s"); + height: 300px; + .transition(~"background 0.2s"); .index-index & { margin-left: 205px; margin-right: -20px; } - &.active { + &.active, &.fullscreen { background: #fff; } &.minimized { height: 50px; cursor: pointer; } + &.fullscreen { + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + margin: 0; + height: auto; + } } .composer-content { padding: 20px 20px 15px; @@ -43,14 +53,18 @@ .minimized & { padding: 10px 20px; } + .fullscreen & { + max-width: 900px; + margin: 0 auto; + padding: 30px; + } } .composer-handle { height: 20px; margin-bottom: -20px; - cursor: row-resize; position: relative; - .minimized & { + .minimized &, .fullscreen & { display: none; } } @@ -58,6 +72,16 @@ position: absolute; right: 10px; top: 10px; + list-style-type: none; + padding: 0; + margin: 0; + + & li { + display: inline-block; + } +} +.btn-minimize .fa { + vertical-align: -5px; } .composer-avatar { float: left; @@ -82,12 +106,15 @@ } } .composer-editor { + .minimized & { + visibility: hidden; + } +} +.text-editor { & textarea { background: none; border-radius: 0; padding: 0; - margin-bottom: 10px; - height: 200px; border: 0; resize: none; color: @fl-body-color; @@ -98,8 +125,30 @@ background: none; } } +} +.text-editor-controls { + margin: 10px 0 0; + padding: 0; + list-style-type: none; + & li { + display: inline-block; + } +} - .minimized & { - visibility: hidden; +.composer-loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + opacity: 0; + pointer-events: none; + border-radius: @border-radius-base @border-radius-base 0 0; + .transition(opacity 0.2s); + + &.active { + opacity: 1; + pointer-events: auto; } } \ No newline at end of file diff --git a/ember/app/templates/components/discussions/composer-body.hbs b/ember/app/templates/components/discussions/composer-body.hbs new file mode 100644 index 000000000..0c4a8f1dc --- /dev/null +++ b/ember/app/templates/components/discussions/composer-body.hbs @@ -0,0 +1,11 @@ +{{user-avatar user class="composer-avatar"}} + +