Browse Source

239 Auto-expand message area when typing

adambar 9 years ago
parent
commit
07ac0ae9cc

+ 1 - 1
background.html

@@ -45,7 +45,7 @@
                   <input type='file' class='file-input'>
               </div>
               <input class="send-btn" type='submit' value='' />
-              <input class='send-message' type='textarea' placeholder="Send a message">
+              <textarea class='send-message' placeholder="Send a message" rows="1"></textarea>
           </form>
       </div>
   </script>

+ 7 - 2
bower.json

@@ -22,7 +22,8 @@
     "blueimp-load-image": "~1.13.0",
     "blueimp-canvas-to-blob": "~2.1.1",
     "twemoji": "~1.2.1",
-    "emojijs": "iamcal/js-emoji"
+    "emojijs": "iamcal/js-emoji",
+    "autosize": "~3.0.6"
   },
   "devDependencies": {
     "mocha": "~2.0.1",
@@ -111,6 +112,9 @@
     ],
     "mock-socket": [
       "dist/mock-socket.js"
+    ],
+    "autosize": [
+      "dist/autosize.js"
     ]
   },
   "concat": {
@@ -131,7 +135,8 @@
       "blueimp-load-image",
       "blueimp-canvas-to-blob",
       "twemoji",
-      "emojijs"
+      "emojijs",
+      "autosize"
     ],
     "libtextsecure": [
       "jquery",

+ 211 - 0
components/autosize/dist/autosize.js

@@ -0,0 +1,211 @@
+/*!
+	Autosize 3.0.5
+	license: MIT
+	http://www.jacklmoore.com/autosize
+*/
+(function (global, factory) {
+	if (typeof define === 'function' && define.amd) {
+		define(['exports', 'module'], factory);
+	} else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
+		factory(exports, module);
+	} else {
+		var mod = {
+			exports: {}
+		};
+		factory(mod.exports, mod);
+		global.autosize = mod.exports;
+	}
+})(this, function (exports, module) {
+	'use strict';
+
+	function assign(ta) {
+		var _ref = arguments[1] === undefined ? {} : arguments[1];
+
+		var _ref$setOverflowX = _ref.setOverflowX;
+		var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
+		var _ref$setOverflowY = _ref.setOverflowY;
+		var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
+
+		if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || ta.hasAttribute('data-autosize-on')) return;
+
+		var heightOffset = null;
+		var overflowY = 'hidden';
+
+		function init() {
+			var style = window.getComputedStyle(ta, null);
+
+			if (style.resize === 'vertical') {
+				ta.style.resize = 'none';
+			} else if (style.resize === 'both') {
+				ta.style.resize = 'horizontal';
+			}
+
+			if (style.boxSizing === 'content-box') {
+				heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
+			} else {
+				heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
+			}
+
+			update();
+		}
+
+		function changeOverflow(value) {
+			{
+				// Chrome/Safari-specific fix:
+				// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+				// made available by removing the scrollbar. The following forces the necessary text reflow.
+				var width = ta.style.width;
+				ta.style.width = '0px';
+				// Force reflow:
+				/* jshint ignore:start */
+				ta.offsetWidth;
+				/* jshint ignore:end */
+				ta.style.width = width;
+			}
+
+			overflowY = value;
+
+			if (setOverflowY) {
+				ta.style.overflowY = value;
+			}
+
+			update();
+		}
+
+		function update() {
+			var startHeight = ta.style.height;
+			var htmlTop = document.documentElement.scrollTop;
+			var bodyTop = document.body.scrollTop;
+			var originalHeight = ta.style.height;
+
+			ta.style.height = 'auto';
+
+			var endHeight = ta.scrollHeight + heightOffset;
+
+			if (ta.scrollHeight === 0) {
+				// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+				ta.style.height = originalHeight;
+				return;
+			}
+
+			ta.style.height = endHeight + 'px';
+
+			// prevents scroll-position jumping
+			document.documentElement.scrollTop = htmlTop;
+			document.body.scrollTop = bodyTop;
+
+			var style = window.getComputedStyle(ta, null);
+
+			if (style.height !== ta.style.height) {
+				if (overflowY !== 'visible') {
+					changeOverflow('visible');
+					return;
+				}
+			} else {
+				if (overflowY !== 'hidden') {
+					changeOverflow('hidden');
+					return;
+				}
+			}
+
+			if (startHeight !== ta.style.height) {
+				var evt = document.createEvent('Event');
+				evt.initEvent('autosize:resized', true, false);
+				ta.dispatchEvent(evt);
+			}
+		}
+
+		var destroy = (function (style) {
+			window.removeEventListener('resize', update);
+			ta.removeEventListener('input', update);
+			ta.removeEventListener('keyup', update);
+			ta.removeAttribute('data-autosize-on');
+			ta.removeEventListener('autosize:destroy', destroy);
+
+			Object.keys(style).forEach(function (key) {
+				ta.style[key] = style[key];
+			});
+		}).bind(ta, {
+			height: ta.style.height,
+			resize: ta.style.resize,
+			overflowY: ta.style.overflowY,
+			overflowX: ta.style.overflowX,
+			wordWrap: ta.style.wordWrap });
+
+		ta.addEventListener('autosize:destroy', destroy);
+
+		// IE9 does not fire onpropertychange or oninput for deletions,
+		// so binding to onkeyup to catch most of those events.
+		// There is no way that I know of to detect something like 'cut' in IE9.
+		if ('onpropertychange' in ta && 'oninput' in ta) {
+			ta.addEventListener('keyup', update);
+		}
+
+		window.addEventListener('resize', update);
+		ta.addEventListener('input', update);
+		ta.addEventListener('autosize:update', update);
+		ta.setAttribute('data-autosize-on', true);
+
+		if (setOverflowY) {
+			ta.style.overflowY = 'hidden';
+		}
+		if (setOverflowX) {
+			ta.style.overflowX = 'hidden';
+			ta.style.wordWrap = 'break-word';
+		}
+
+		init();
+	}
+
+	function destroy(ta) {
+		if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+		var evt = document.createEvent('Event');
+		evt.initEvent('autosize:destroy', true, false);
+		ta.dispatchEvent(evt);
+	}
+
+	function update(ta) {
+		if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+		var evt = document.createEvent('Event');
+		evt.initEvent('autosize:update', true, false);
+		ta.dispatchEvent(evt);
+	}
+
+	var autosize = null;
+
+	// Do nothing in Node.js environment and IE8 (or lower)
+	if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
+		autosize = function (el) {
+			return el;
+		};
+		autosize.destroy = function (el) {
+			return el;
+		};
+		autosize.update = function (el) {
+			return el;
+		};
+	} else {
+		autosize = function (el, options) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], function (x) {
+					return assign(x, options);
+				});
+			}
+			return el;
+		};
+		autosize.destroy = function (el) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], destroy);
+			}
+			return el;
+		};
+		autosize.update = function (el) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], update);
+			}
+			return el;
+		};
+	}
+
+	module.exports = autosize;
+});

+ 212 - 0
js/components.js

@@ -29667,3 +29667,215 @@ function emoji(){}
 }).call(function(){
 	return this || (typeof window !== 'undefined' ? window : global);
 }());
+
+/*!
+	Autosize 3.0.5
+	license: MIT
+	http://www.jacklmoore.com/autosize
+*/
+(function (global, factory) {
+	if (typeof define === 'function' && define.amd) {
+		define(['exports', 'module'], factory);
+	} else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
+		factory(exports, module);
+	} else {
+		var mod = {
+			exports: {}
+		};
+		factory(mod.exports, mod);
+		global.autosize = mod.exports;
+	}
+})(this, function (exports, module) {
+	'use strict';
+
+	function assign(ta) {
+		var _ref = arguments[1] === undefined ? {} : arguments[1];
+
+		var _ref$setOverflowX = _ref.setOverflowX;
+		var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
+		var _ref$setOverflowY = _ref.setOverflowY;
+		var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
+
+		if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || ta.hasAttribute('data-autosize-on')) return;
+
+		var heightOffset = null;
+		var overflowY = 'hidden';
+
+		function init() {
+			var style = window.getComputedStyle(ta, null);
+
+			if (style.resize === 'vertical') {
+				ta.style.resize = 'none';
+			} else if (style.resize === 'both') {
+				ta.style.resize = 'horizontal';
+			}
+
+			if (style.boxSizing === 'content-box') {
+				heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
+			} else {
+				heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
+			}
+
+			update();
+		}
+
+		function changeOverflow(value) {
+			{
+				// Chrome/Safari-specific fix:
+				// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+				// made available by removing the scrollbar. The following forces the necessary text reflow.
+				var width = ta.style.width;
+				ta.style.width = '0px';
+				// Force reflow:
+				/* jshint ignore:start */
+				ta.offsetWidth;
+				/* jshint ignore:end */
+				ta.style.width = width;
+			}
+
+			overflowY = value;
+
+			if (setOverflowY) {
+				ta.style.overflowY = value;
+			}
+
+			update();
+		}
+
+		function update() {
+			var startHeight = ta.style.height;
+			var htmlTop = document.documentElement.scrollTop;
+			var bodyTop = document.body.scrollTop;
+			var originalHeight = ta.style.height;
+
+			ta.style.height = 'auto';
+
+			var endHeight = ta.scrollHeight + heightOffset;
+
+			if (ta.scrollHeight === 0) {
+				// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+				ta.style.height = originalHeight;
+				return;
+			}
+
+			ta.style.height = endHeight + 'px';
+
+			// prevents scroll-position jumping
+			document.documentElement.scrollTop = htmlTop;
+			document.body.scrollTop = bodyTop;
+
+			var style = window.getComputedStyle(ta, null);
+
+			if (style.height !== ta.style.height) {
+				if (overflowY !== 'visible') {
+					changeOverflow('visible');
+					return;
+				}
+			} else {
+				if (overflowY !== 'hidden') {
+					changeOverflow('hidden');
+					return;
+				}
+			}
+
+			if (startHeight !== ta.style.height) {
+				var evt = document.createEvent('Event');
+				evt.initEvent('autosize:resized', true, false);
+				ta.dispatchEvent(evt);
+			}
+		}
+
+		var destroy = (function (style) {
+			window.removeEventListener('resize', update);
+			ta.removeEventListener('input', update);
+			ta.removeEventListener('keyup', update);
+			ta.removeAttribute('data-autosize-on');
+			ta.removeEventListener('autosize:destroy', destroy);
+
+			Object.keys(style).forEach(function (key) {
+				ta.style[key] = style[key];
+			});
+		}).bind(ta, {
+			height: ta.style.height,
+			resize: ta.style.resize,
+			overflowY: ta.style.overflowY,
+			overflowX: ta.style.overflowX,
+			wordWrap: ta.style.wordWrap });
+
+		ta.addEventListener('autosize:destroy', destroy);
+
+		// IE9 does not fire onpropertychange or oninput for deletions,
+		// so binding to onkeyup to catch most of those events.
+		// There is no way that I know of to detect something like 'cut' in IE9.
+		if ('onpropertychange' in ta && 'oninput' in ta) {
+			ta.addEventListener('keyup', update);
+		}
+
+		window.addEventListener('resize', update);
+		ta.addEventListener('input', update);
+		ta.addEventListener('autosize:update', update);
+		ta.setAttribute('data-autosize-on', true);
+
+		if (setOverflowY) {
+			ta.style.overflowY = 'hidden';
+		}
+		if (setOverflowX) {
+			ta.style.overflowX = 'hidden';
+			ta.style.wordWrap = 'break-word';
+		}
+
+		init();
+	}
+
+	function destroy(ta) {
+		if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+		var evt = document.createEvent('Event');
+		evt.initEvent('autosize:destroy', true, false);
+		ta.dispatchEvent(evt);
+	}
+
+	function update(ta) {
+		if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+		var evt = document.createEvent('Event');
+		evt.initEvent('autosize:update', true, false);
+		ta.dispatchEvent(evt);
+	}
+
+	var autosize = null;
+
+	// Do nothing in Node.js environment and IE8 (or lower)
+	if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
+		autosize = function (el) {
+			return el;
+		};
+		autosize.destroy = function (el) {
+			return el;
+		};
+		autosize.update = function (el) {
+			return el;
+		};
+	} else {
+		autosize = function (el, options) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], function (x) {
+					return assign(x, options);
+				});
+			}
+			return el;
+		};
+		autosize.destroy = function (el) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], destroy);
+			}
+			return el;
+		};
+		autosize.update = function (el) {
+			if (el) {
+				Array.prototype.forEach.call(el.length ? el : [el], update);
+			}
+			return el;
+		};
+	}
+
+	module.exports = autosize;
+});

+ 26 - 1
js/views/conversation_view.js

@@ -59,6 +59,8 @@
 
         events: {
             'submit .send': 'sendMessage',
+            'input .send-message': 'updateMessageFieldSize',
+            'keydown .send-message': 'updateMessageFieldSize',
             'close': 'remove',
             'click .destroy': 'destroyMessages',
             'click .end-session': 'endSession',
@@ -161,7 +163,7 @@
 
         sendMessage: function(e) {
             e.preventDefault();
-            var input = this.$('.send input.send-message');
+            var input = this.$('.send .send-message');
             var message = this.replace_colons(input.val());
             var convo = this.model;
 
@@ -170,9 +172,11 @@
                     convo.sendMessage(message, attachments);
                 });
                 input.val("");
+                window.autosize(input);
                 this.fileInput.deleteFiles();
             }
         },
+
         replace_colons: function(str) {
             return str.replace(emoji.rx_colons, function(m){
                 var idx = m.substr(1, m.length-2);
@@ -187,6 +191,27 @@
 
         updateTitle: function() {
             this.$('.conversation-title').text(this.model.getTitle());
+        },
+
+        updateMessageFieldSize: function (event) {
+            var keyCode = event.which || event.keyCode;
+
+            if (keyCode === 13) {
+                // enter pressed - submit the form now
+                return this.$('.bottom-bar form').submit();
+            }
+
+            var $messageField = this.$('.send-message'),
+                $discussionContainer = this.$('.discussion-container'),
+                $discussionContainerPrevHeight = $discussionContainer.outerHeight(),
+                $bottomBar = this.$('.bottom-bar'),
+                $bottomBarPrevHeight = $bottomBar.outerHeight();
+
+            window.autosize($messageField);
+            $bottomBar.outerHeight($messageField.outerHeight() + 1);
+
+            var $bottomBarNewHeight = $bottomBar.outerHeight();
+            $discussionContainer.outerHeight($discussionContainerPrevHeight - ($bottomBarNewHeight - $bottomBarPrevHeight));
         }
     });
 })();

+ 42 - 25
stylesheets/_conversation.scss

@@ -1,6 +1,4 @@
 .conversation {
-  padding: $header-height 0;
-
   .file-input .close {
     top: -10px;
   }
@@ -8,10 +6,18 @@
   .conversation-title {
     line-height: $header-height;
   }
+
+  #header {
+    position: inherit;
+  }
+
+  .discussion-container {
+    height: calc(100% - 2 * #{$header-height});
+  }
 }
 
 .conversation + .new-group-update-form,
-.conversation, .discussion-container, .message-list, .message-detail, .key-verification {
+.conversation, .message-list, .message-detail, .key-verification {
   height: 100%;
 }
 
@@ -29,6 +35,7 @@
     padding: 0 1em;
   }
 }
+
 .message-detail {
   padding: $header-height 0 0;
   background: $grey_l;
@@ -100,6 +107,7 @@
 .outgoing .sender {
   display: none;
 }
+
 .sender {
   font-size: smaller;
   opacity: 0.8;
@@ -110,10 +118,9 @@
 }
 
 .entry.delivered .checkmark {
-    display: inline;
+  display: inline;
 }
 
-
 .message-list {
   margin: 0;
   padding: 1em 0;
@@ -237,7 +244,9 @@
         font-style: italic;
       }
 
-      &::before, &::after { display: none; }
+      &::before, &::after {
+        display: none;
+      }
     }
   }
 
@@ -308,6 +317,8 @@
 }
 
 .bottom-bar {
+  $button-width: 36px;
+
   position: fixed;
   bottom: 1; // offset 1 for window frame.
   height: 36px;
@@ -315,7 +326,7 @@
   border-top: 1px solid $grey_l;
   background: white;
 
-  button, input {
+  button, input, textarea {
     color: $grey_d;
   }
 
@@ -323,7 +334,7 @@
     position: absolute;
     top: 0;
     height: 100%;
-    width: 36px;
+    width: $button-width;
     padding: 0;
     border: 0;
     outline: 0;
@@ -352,16 +363,19 @@
     }
   }
 
-  form, input {
+  form, input, textarea {
     height: 100%;
   }
 
-  input[type=textarea] {
-      display: block;
-      height: 100%;
-      border: 0;
-      outline: 0;
-      z-index: 5;
+  .send-message {
+    display: block;
+    width: calc(100% - 2 * #{$button-width} - 20px);
+    min-height: $header-height - 1px;
+    max-height: 100px;
+    padding: 10px;
+    border: 0;
+    outline: 0;
+    z-index: 5;
   }
 }
 
@@ -376,17 +390,20 @@
   border-radius: 20px;
   font-size: small;
 }
+
 .confirmation-dialog {
-    position: absolute;
-    top: $header-height;
-    padding: 1em;
-    background: white;
-    border: solid 2px $blue;
+  position: absolute;
+  top: $header-height;
+  padding: 1em;
+  background: white;
+  border: solid 2px $blue;
 
-    .message { text-align: center; }
+  .message {
+    text-align: center;
+  }
 
-    button {
-      float: right;
-      margin-left: 10px;
-    }
+  button {
+    float: right;
+    margin-left: 10px;
+  }
 }

+ 3 - 7
stylesheets/_global.scss

@@ -130,16 +130,12 @@ button.back {
   margin-right: 10px;
   cursor: pointer;
 
-  .thumbnail {
-    width: 36px;
-    height: 36px;
-  }
   .paperclip {
     width: 36px;
-    height: 36px;
-    background: url('/images/paperclip.png') no-repeat;
+    height: 100%;
+    background: url('/images/paperclip.png') no-repeat center center;
     background-size: 90%;
-    background-position: center 6px;
+    margin-top: 4px;
   }
 
   input[type=file] {

+ 19 - 17
stylesheets/manifest.css

@@ -116,15 +116,12 @@ button.back {
   position: relative;
   margin-right: 10px;
   cursor: pointer; }
-  .file-input .thumbnail {
-    width: 36px;
-    height: 36px; }
   .file-input .paperclip {
     width: 36px;
-    height: 36px;
-    background: url("/images/paperclip.png") no-repeat;
+    height: 100%;
+    background: url("/images/paperclip.png") no-repeat center center;
     background-size: 90%;
-    background-position: center 6px; }
+    margin-top: 4px; }
   .file-input input[type=file] {
     display: none;
     position: absolute;
@@ -427,15 +424,17 @@ input.search {
 .conversations .unread .contact-details .last-timestamp {
   font-weight: bold; }
 
-.conversation {
-  padding: 36px 0; }
-  .conversation .file-input .close {
-    top: -10px; }
-  .conversation .conversation-title {
-    line-height: 36px; }
+.conversation .file-input .close {
+  top: -10px; }
+.conversation .conversation-title {
+  line-height: 36px; }
+.conversation #header {
+  position: inherit; }
+.conversation .discussion-container {
+  height: calc(100% - 2 * 36px); }
 
 .conversation + .new-group-update-form,
-.conversation, .discussion-container, .message-list, .message-detail, .key-verification {
+.conversation, .message-list, .message-detail, .key-verification {
   height: 100%; }
 
 .key-verification {
@@ -666,7 +665,7 @@ input.search {
   width: calc(100% - 2px);
   border-top: 1px solid #f3f3f3;
   background: white; }
-  .bottom-bar button, .bottom-bar input {
+  .bottom-bar button, .bottom-bar input, .bottom-bar textarea {
     color: #454545; }
   .bottom-bar button {
     position: absolute;
@@ -693,11 +692,14 @@ input.search {
     cursor: pointer; }
     .bottom-bar .send-btn::before {
       content: '+'; }
-  .bottom-bar form, .bottom-bar input {
+  .bottom-bar form, .bottom-bar input, .bottom-bar textarea {
     height: 100%; }
-  .bottom-bar input[type=textarea] {
+  .bottom-bar .send-message {
     display: block;
-    height: 100%;
+    width: calc(100% - 2 * 36px - 20px);
+    min-height: 35px;
+    max-height: 100px;
+    padding: 10px;
     border: 0;
     outline: 0;
     z-index: 5; }

File diff suppressed because it is too large
+ 0 - 0
stylesheets/manifest.css.map


Some files were not shown because too many files changed in this diff