From aa2bc23039375c52f024e7b833f572dc1f57018a Mon Sep 17 00:00:00 2001
From: Toby Zerner <toby.zerner@gmail.com>
Date: Mon, 18 May 2015 10:40:14 +0930
Subject: [PATCH] Drastically improve how the composer looks and behaves

- New, cleaner, more prominent look
- Make it statically positioned down the bottom on mobile, so you can
still scroll up to look at posts
- Fix some bugs with animation, jumping between views
---
 js/forum/src/components/composer-body.js      |  4 +-
 .../src/components/composer-discussion.js     |  3 +-
 js/forum/src/components/composer-edit.js      |  6 +-
 js/forum/src/components/composer-reply.js     | 16 +++-
 js/forum/src/components/composer.js           | 29 +++---
 js/lib/components/text-editor.js              |  4 +-
 less/forum/composer.less                      | 94 +++++++++----------
 7 files changed, 83 insertions(+), 73 deletions(-)

diff --git a/js/forum/src/components/composer-body.js b/js/forum/src/components/composer-body.js
index e2fbd0ce4..bc46b0203 100644
--- a/js/forum/src/components/composer-body.js
+++ b/js/forum/src/components/composer-body.js
@@ -21,8 +21,8 @@ export default class ComposerBody extends Component {
     });
   }
 
-  view() {
-    return m('div', {config: this.element}, [
+  view(className) {
+    return m('div', {className, config: this.element}, [
       avatar(this.props.user, {className: 'composer-avatar'}),
       m('div.composer-body', [
         m('ul.composer-header', listItems(this.headerItems().toArray())),
diff --git a/js/forum/src/components/composer-discussion.js b/js/forum/src/components/composer-discussion.js
index e78b4c896..93d5ee8b6 100644
--- a/js/forum/src/components/composer-discussion.js
+++ b/js/forum/src/components/composer-discussion.js
@@ -10,6 +10,7 @@ import ActionButton from 'flarum/components/action-button';
  */
 export default class ComposerDiscussion extends ComposerBody {
   constructor(props) {
+    props.placeholder = props.placeholder || 'Write a post...';
     props.submitLabel = props.submitLabel || 'Post Discussion';
     props.confirmExit = props.confirmExit || 'You have not posted your discussion. Do you wish to discard it?';
     props.titlePlaceholder = props.titlePlaceholder || 'Discussion Title';
@@ -26,7 +27,7 @@ export default class ComposerDiscussion extends ComposerBody {
     items.add('title', m('h3', m('input', {
       className: 'form-control',
       value: this.title(),
-      onchange: m.withAttr('value', this.title),
+      oninput: m.withAttr('value', this.title),
       placeholder: this.props.titlePlaceholder,
       disabled: !!this.props.disabled,
       config: function(element, isInitialized) {
diff --git a/js/forum/src/components/composer-edit.js b/js/forum/src/components/composer-edit.js
index ab06f128c..3f1695fba 100644
--- a/js/forum/src/components/composer-edit.js
+++ b/js/forum/src/components/composer-edit.js
@@ -22,7 +22,11 @@ export default class ComposerEdit extends ComposerBody {
     var items = new ItemList();
     var post = this.props.post;
 
-    items.add('title', m('h3', ['Editing Post #'+post.number()+' in ', m('em', post.discussion().title())]));
+    items.add('title', m('h3', [
+      'Editing ',
+      m('a', {href: app.route.discussion(post.discussion(), post.number()), config: m.route}, 'Post #'+post.number()),
+      ' in ', post.discussion().title()
+    ]));
 
     return items;
   }
diff --git a/js/forum/src/components/composer-reply.js b/js/forum/src/components/composer-reply.js
index 6f33c7d68..632be4ce9 100644
--- a/js/forum/src/components/composer-reply.js
+++ b/js/forum/src/components/composer-reply.js
@@ -2,19 +2,33 @@ import ItemList from 'flarum/utils/item-list';
 import ComposerBody from 'flarum/components/composer-body';
 import Alert from 'flarum/components/alert';
 import ActionButton from 'flarum/components/action-button';
+import Composer from 'flarum/components/composer';
 
 export default class ComposerReply extends ComposerBody {
   constructor(props) {
+    props.placeholder = props.placeholder || 'Write your reply...';
     props.submitLabel = props.submitLabel || 'Post Reply';
     props.confirmExit = props.confirmExit || 'You have not posted your reply. Do you wish to discard it?';
 
     super(props);
   }
 
+  view() {
+    return super.view('composer-reply');
+  }
+
   headerItems() {
     var items = new ItemList();
 
-    items.add('title', m('h3', ['Replying to ', m('em', this.props.discussion.title())]));
+    if (app.composer.position() === Composer.PositionEnum.MINIMIZED ||
+      // https://github.com/babel/babel/issues/1150
+      !app.current.discussion ||
+      app.current.discussion() !== this.props.discussion) {
+      items.add('title', m('h3', [
+        'Replying to ',
+        m('a', {href: app.route.discussion(this.props.discussion), config: m.route}, this.props.discussion.title())
+      ]));
+    }
 
     return items;
   }
diff --git a/js/forum/src/components/composer.js b/js/forum/src/components/composer.js
index 4a6dc64ff..6f1cf8fe1 100644
--- a/js/forum/src/components/composer.js
+++ b/js/forum/src/components/composer.js
@@ -133,19 +133,16 @@ class Composer extends Component {
   }
 
   render(anchorToBottom) {
-    // @todo this function's logic could probably use some reworking. The
-    //     following line is bad because it prevents focusing on the composer
-    //     input when the composer is shown when it's already being shown
     if (this.position() === this.oldPosition) { return; }
 
-    var $composer = this.$();
+    var $composer = this.$().stop(true);
     var oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0;
 
     if (this.position() !== Composer.PositionEnum.HIDDEN) {
       m.redraw(true);
     }
 
-    this.updateHeight();
+    this.$().height(this.computedHeight());
     var newHeight = $composer.outerHeight();
 
     switch (this.position()) {
@@ -178,7 +175,10 @@ class Composer extends Component {
     }
     $('body').toggleClass('composer-open', this.position() !== Composer.PositionEnum.HIDDEN);
     this.oldPosition = this.position();
-    this.setContentHeight(this.computedHeight());
+
+    if (this.position() !== Composer.PositionEnum.HIDDEN) {
+      this.setContentHeight(this.computedHeight());
+    }
   }
 
   // Update the amount of padding-bottom on the body so that the page's
@@ -203,12 +203,12 @@ class Composer extends Component {
   // to fill up the height of the composer, minus the space taken up by the
   // composer's header/footer/etc.
   setContentHeight(height) {
-    var content = this.$('.composer-content');
-    this.$('.flexible-height').height(height -
-      parseInt(content.css('padding-top')) -
-      parseInt(content.css('padding-bottom')) -
-      this.$('.composer-header').outerHeight(true) -
-      this.$('.text-editor-controls').outerHeight(true));
+    var flexible = this.$('.flexible-height');
+    if (flexible.length) {
+      flexible.height(height -
+        (flexible.offset().top - this.$().offset().top) -
+        this.$('.text-editor-controls').outerHeight(true));
+    }
   }
 
   load(component) {
@@ -225,8 +225,7 @@ class Composer extends Component {
     if ([Composer.PositionEnum.MINIMIZED, Composer.PositionEnum.HIDDEN].indexOf(this.position()) !== -1) {
       this.position(Composer.PositionEnum.NORMAL);
     }
-    // work around https://github.com/lhorie/mithril.js/issues/603
-    setTimeout(() => this.render(anchorToBottom));
+    this.render(anchorToBottom);
   }
 
   hide() {
@@ -276,7 +275,7 @@ class Composer extends Component {
         items.add('minimize', this.control({ icon: 'minus minimize', title: 'Minimize', onclick: this.minimize.bind(this) }));
         items.add('fullScreen', this.control({ icon: 'expand', title: 'Full Screen', onclick: this.fullScreen.bind(this) }));
       }
-      items.add('close', this.control({ icon: 'times', title: 'Close', wrapperClass: 'back-control', onclick: this.close.bind(this) }));
+      items.add('close', this.control({ icon: 'times', title: 'Close', onclick: this.close.bind(this) }));
     }
 
     return items;
diff --git a/js/lib/components/text-editor.js b/js/lib/components/text-editor.js
index 56b806b93..b81cca45d 100644
--- a/js/lib/components/text-editor.js
+++ b/js/lib/components/text-editor.js
@@ -25,7 +25,7 @@ export default class TextEditor extends Component {
         disabled: !!this.props.disabled,
         value: this.value()
       }),
-      m('ul.text-editor-controls.fade', listItems(this.controlItems().toArray()))
+      m('ul.text-editor-controls', listItems(this.controlItems().toArray()))
     ]);
   }
 
@@ -43,7 +43,6 @@ export default class TextEditor extends Component {
         label: this.props.submitLabel,
         icon: 'check',
         className: 'btn btn-primary',
-        wrapperClass: 'primary-control',
         onclick: this.onsubmit.bind(this)
       })
     );
@@ -76,7 +75,6 @@ export default class TextEditor extends Component {
   oninput(value) {
     this.value(value);
     this.props.onchange(this.value());
-    this.$('.text-editor-controls').toggleClass('in', !!value);
 
     m.redraw.strategy('none');
   }
diff --git a/less/forum/composer.less b/less/forum/composer.less
index d12700529..7af2580a7 100644
--- a/less/forum/composer.less
+++ b/less/forum/composer.less
@@ -23,10 +23,15 @@
     margin: 0 0 10px;
     line-height: 1.5em;
 
-    &, & input {
+    &, & input, & a {
       color: @fl-body-muted-color;
-      font-size: 16px;
+      font-size: 15px;
       font-weight: normal;
+      transition: color 0.2s;
+
+      .active & {
+        color: @fl-body-primary-color;
+      }
     }
     & input, & input[disabled] {
       background: none;
@@ -36,6 +41,19 @@
     }
   }
 }
+.composer-controls {
+  position: absolute;
+  right: 10px;
+  top: 10px;
+  z-index: 1;
+
+  & li {
+    display: inline-block;
+  }
+  .minimized & {
+    top: 7px;
+  }
+}
 .composer-loading {
   position: absolute;
   top: 0;
@@ -58,35 +76,16 @@
 // screen. The controls are hidden (except for the 'x', which is the back-
 // control), and the avatar hidden.
 @media @phone {
-  .composer-open {
-    overflow: hidden;
-  }
   .composer {
-    position: fixed;
-    top: 0;
-    bottom: 0;
+    position: absolute;
     left: 0;
     right: 0;
-    z-index: @zindex-composer;
-    background: @fl-body-bg;
-    height: 100vh !important;
-    padding-top: @mobile-header-height;
-
-    &:before {
-      content: " ";
-      .toolbar();
-      opacity: 0;
-
-      .visible& {
-        opacity: 1;
-      }
-    }
   }
   .composer-content {
-    padding: 15px;
+    padding: 15px 15px 0;
   }
   .composer-controls {
-    & li:not(.back-control) {
+    & li:not(:last-child) {
       display: none;
     }
   }
@@ -117,11 +116,14 @@
     transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray
     position: relative;
     height: 300px;
-    .transition(~"background 0.2s");
+    .transition(~"background 0.2s, box-shadow 0.2s");
 
     &.active, &.full-screen {
       background: @fl-body-bg;
     }
+    &.active:not(.full-screen) {
+      box-shadow: 0 2px 6px @fl-shadow-color, 0 0 0 2px @fl-body-primary-color;
+    }
     &.minimized {
       height: 50px;
       cursor: pointer;
@@ -142,10 +144,10 @@
     }
   }
   .composer-content {
-    padding: 20px 20px 15px;
+    padding: 20px 20px 0;
 
     .minimized & {
-      padding: 10px 20px;
+      padding: 12px 20px;
     }
     .full-screen & {
       max-width: 900px;
@@ -154,26 +156,14 @@
     }
   }
   .composer-handle {
-    height: 20px;
-    margin-bottom: -20px;
+    height: 15px;
+    margin-bottom: -17px;
     position: relative;
 
     .minimized &, .full-screen & {
       display: none;
     }
   }
-  .composer-controls {
-    position: absolute;
-    right: 10px;
-    top: 10px;
-
-    & li {
-      display: inline-block;
-    }
-    .minimized & {
-      top: 7px;
-    }
-  }
   .fa-minus.minimize {
     vertical-align: -5px;
   }
@@ -204,14 +194,13 @@
     margin-left: -20px;
     margin-right: 180px;
 
-    .index-page & {
+    .index-page &:not(.full-screen) {
       margin-left: 205px;
       margin-right: -20px;
     }
   }
 }
 
-
 // ------------------------------------
 // Text Editor
 
@@ -223,7 +212,7 @@
     resize: none;
     color: @fl-body-color;
     font-size: 14px;
-    line-height: 1.6;
+    line-height: 1.7;
 
     &, &:focus, &[disabled] {
       background: none;
@@ -231,8 +220,8 @@
   }
 }
 .text-editor-controls {
-  margin: 10px 0 0;
-  padding: 0;
+  margin: 0;
+  padding: 15px 0;
   list-style-type: none;
 
   & li {
@@ -240,10 +229,15 @@
   }
 }
 
-// On phones, since one of the text editor controls will probably be the
-// primary-control, we shouldn't hide it completely when it's "disabled".
-@media @phone {
+@media @tablet, @desktop, @desktop-hd {
   .text-editor-controls {
-    opacity: 0.5;
+    margin: 0 -20px 0 -110px;
+    padding: 15px 20px;
+    border-top: 1px solid @fl-body-secondary-color;
+
+    & .btn-primary {
+      padding-left: 20px;
+      padding-right: 20px;
+    }
   }
 }