-
-
Save thomseddon/4703968 to your computer and use it in GitHub Desktop.
/** | |
* The MIT License (MIT) | |
* | |
* Copyright (c) 2013 Thom Seddon | |
* Copyright (c) 2010 Google | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
* THE SOFTWARE. | |
* Adapted from: http://code.google.com/p/gaequery/source/browse/trunk/src/static/scripts/jquery.autogrow-textarea.js | |
* | |
* Works nicely with the following styles: | |
* textarea { | |
* resize: none; | |
* word-wrap: break-word; | |
* transition: 0.05s; | |
* -moz-transition: 0.05s; | |
* -webkit-transition: 0.05s; | |
* -o-transition: 0.05s; | |
* } | |
* | |
* Usage: <textarea auto-grow></textarea> | |
*/ | |
app.directive('autoGrow', function() { | |
return function(scope, element, attr){ | |
var minHeight = element[0].offsetHeight, | |
paddingLeft = element.css('paddingLeft'), | |
paddingRight = element.css('paddingRight'); | |
var $shadow = angular.element('<div></div>').css({ | |
position: 'absolute', | |
top: -10000, | |
left: -10000, | |
width: element[0].offsetWidth - parseInt(paddingLeft || 0) - parseInt(paddingRight || 0), | |
fontSize: element.css('fontSize'), | |
fontFamily: element.css('fontFamily'), | |
lineHeight: element.css('lineHeight'), | |
resize: 'none' | |
}); | |
angular.element(document.body).append($shadow); | |
var update = function() { | |
var times = function(string, number) { | |
for (var i = 0, r = ''; i < number; i++) { | |
r += string; | |
} | |
return r; | |
} | |
var val = element.val().replace(/</g, '<') | |
.replace(/>/g, '>') | |
.replace(/&/g, '&') | |
.replace(/\n$/, '<br/> ') | |
.replace(/\n/g, '<br/>') | |
.replace(/\s{2,}/g, function(space) { return times(' ', space.length - 1) + ' ' }); | |
$shadow.html(val); | |
element.css('height', Math.max($shadow[0].offsetHeight + 10 /* the "threshold" */, minHeight) + 'px'); | |
} | |
element.bind('keyup keydown keypress change', update); | |
update(); | |
} | |
}); |
i would also add 'word-wrap': 'break-word' to the list of properties
I found that whenever the autogrow is attached to an element with an ng-binding, the directive runs before the bound data is put inside of the textarea.
This means that when you navigate to a page with a textarea already containing overflowing text from a binding, the textarea will be too small.
I fixed this by adding a timeout of 0 to the update call.
$timeout(update, 0)
on line 54 will do the trick.
Don't forget to inject $timeout
into the directive
Instead of the $timeout-based version, another alternative would be to simply watch for model changes and resize the textarea accordingly:
if (attr.ngModel) {
// update when the model changes
scope.$watch(attr.ngModel, update);
}
Thanks for all these guys! Some things worth noting:
- If the width of the textarea can/will change (i.e. width: 100%), you must adjust the shadow width accordingly (so either on update() or on window.resize() or similar)
- the Enter key introduces strange behaviour as either watching for ngModel change or keyup will only fire after the newline is added, which results in a brief clipping of the bottom line of text, assuming your threshold is set to 0. I've avoided this by hooking the keydown event and, if enter is pressed, appending a newline+non-breaking space to the textarea's value
- finally, changing
.replace(/\n$/, '<br/> ')
to.replace(/\n ?$/, '<br/> ')
(i.e. adding an optional space) will allow for more accurate accomodating of trailing whitespace
Hi, this really helped me out, after trying other failing solutions.
May I ask why do we need the spaces replacement with /\s{2,}/g
and the times
function and everything?
What does it solve that this doesn't?
.replace(/\s/g, ' ')
Also, why do we bind to keydown
and keypress
if they fire before the content change?
Isn't keyup
enough here?
This directive is super helpful, thanks!
Hi, I read all the comments but i still have a small problem. When i have bind data to the text area it is too small. The update don't run. I tried with the $timeout solution and I checked the solution of @ingorammer with the attr.ngModel. Both don't work. For example when i tried adding a watcher for the attr.ngModel i saw that such an attribute don't exist.
This is what attr has:
$$element: x.fn.x.init[1]
$attr: Object //here is missing the ngModel
autoGrow: "auto-grow"
class: "class"
placeholder: "placeholder"
style: "style"
__proto__: Object
autoGrow: ""
class: "form-control parsley-validated"
placeholder: "Description"
and that's the way how i have it in the template:
<textarea data-auto-grow data-ng-model="task.description" placeholder="Description"></textarea>
Still needs a lot of refinements but it a great start:
http://blog.justonepixel.com/geek/2013/08/14/angularjs-auto-expand/
Better approach than this shadow div thing...
I found https://github.com/monospaced/angular-elastic to be exactly what i was looking for.
It could be simple like this :
appModule.directive('autoGrow', function() {
return function(scope, element, attr){
var update = function(event) {
element.css('height', element[0].scrollHeight + 'px');
}
element.bind('keydown', update);
update();
}
});
@huyttq perfect!
I'd say that keyup flows better than keydown.
Also it may need a shrink method..
myApp.directive("autoGrow", function(){
return function(scope, element, attr){
var update = function(){
element.css("height", "auto");
element.css("height", element[0].scrollHeight + "px");
};
scope.$watch(attr.ngModel, function(){
update();
});
attr.$set("ngTrim", "false");
};
});
@enapupe your directive jumps always to the top of textarea if its height is bigger that window height.
@rafalenden would you fiddle your issue? My approach is at https://gist.github.com/enapupe/2a59589168f33ca405d0 , and has changed a bit.
Inspired by @huyttq & @enapupe, with a fix to control initial height, in CoffeeScript:
autoGrow = ->
(scope, element, attrs) ->
update = ->
if element[0].scrollWidth > element.outerWidth isBreaking = true else isBreaking = false
element.css 'height', 'auto' if isBreaking
height = element[0].scrollHeight
element.css 'height', height + 'px' if height > 0
scope.$watch attrs.ngModel, update
attrs.$set 'ngTrim', 'false'
# Register
App.Directives.directive 'autoGrow', autoGrow
@enapupe & @rafalenden - The jump issue can be fixed by setting the window scroll position after the text area is resized. Not the most elegant or clean solution but it works.
.directive("autoGrow", ['$window', function($window){
return {
link: function (scope, element, attr, $window) {
var update = function () {
var scrollLeft, scrollTop;
scrollTop = window.pageYOffset;
scrollLeft = window.pageXOffset;
element.css("height", "auto");
var height = element[0].scrollHeight;
if (height > 0) {
element.css("height", height + "px");
}
window.scrollTo(scrollLeft, scrollTop);
};
scope.$watch(attr.ngModel, function () {
update();
});
attr.$set("ngTrim", "false");
}
};
}]);
how about visibility
var $shadow = angular.element('<div></div>').css({
- position: 'absolute',
- top: -10000,
- left: -10000,
+ visibility: "hidden",
width: element[0].offsetWidth - parseInt(paddingLeft || 0) - parseInt(paddingRight || 0),
fontSize: element.css('fontSize'),
fontFamily: element.css('fontFamily'),
``
It probably makes sense to avoid an angular watch here and listen to native events of the input/textarea field.
This also makes it possible to avoid messing around with the ngTrim
setting.
function uiAutoGrow($window) {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.on('input propertychange', update);
function update() {
var scrollTop = $window.pageYOffset,
scrollLeft = $window.pageXOffset;
element.css('height', 'auto');
var height = element[0].scrollHeight;
if (height > 0) {
element.css('height', height + 'px');
}
$window.scrollTo(scrollLeft, scrollTop);
}
}
};
}
For anyone else that finds this page, I used LFDM's solution, but I added 'overflow:hidden' css, and I added '$timeout(update)' to apply the update on page load as well (for previously saved form data).
function($window, $timeout) {
return {
restrict: 'A',
link: function(scope, element, attr) {
element.on('input propertychange', update);
function update() {
var scrollTop = $window.pageYOffset,
scrollLeft = $window.pageXOffset;
element.css({
height: 'auto',
overflow: 'hidden'
});
var height = element[0].scrollHeight;
if (height > 0) {
element.css('height', height + 'px');
}
$window.scrollTo(scrollLeft, scrollTop);
}
$timeout(update);
}
};
}
@huyttq good work. thank you.
@kevinleedrum Thanks for your solution. Helped me.
@LFDM Thanks, your solution is quite elegant. Although you may want to add focus
event, so you can expand textarea by entering it, when you loaded a page and have some text inside it already.
Good solution, I would change event handler a bit to handle paste event with the right mouse button, which is often used to copy paste stuff
element.bind('keyup paste', function(){setTimeout(update, 0);});
my solution, for angular and bootstrap with form-control class, using shadow textarea (without fixing input data)
function autoGrow() {
return {
restrict: 'A',
link: function (scope, element, attr, ctrl) {
var minHeight = element[0].offsetHeight;
//create shadow textarea
var $shadowtx = angular.element('<textarea></textarea>').css({
position: 'absolute',
top: -10000,
left: -10000,
resize: 'none',
});
element.css({resize: "none"}).parent().css({ position: "relative" });
$shadowtx.addClass("form-control");
//add to element parent
angular.element(element.parent()[0]).append($shadowtx);
var update = function (addtext) {
addtext = addtext || " aaa"; //when line is ending show new line
$shadowtx.val(element.val() + addtext);
element.css('height', Math.max($shadowtx[0].scrollHeight + 5 /* the "threshold" */, minHeight) + 'px');
}
//on enter catch keydown, but simulate adding enter
element.bind('keydown', function (event) {
var keyc = event.keyCode || event.which;
if (keyc == 13) {
update("\n aaa");
}
}).bind('keyup', function (event) {
var keyc = event.keyCode || event.which;
document.title = keyc;
if ((keyc == 46) || (keyc==8)) { //delete, backspace, not fired by scope.$watch
update();
}
});
//watch model binding
scope.$watch(attr['ngModel'], function (v) {
update();
});
//watch show
scope.$watch(attr['ngShow'], function (v) {
update();
});
update();
}
};
};
Thanks . my adjusted version added below makes sure that there are no extra spaces added under the text
- I used $timeout to make sure the shadow element's sizes are set after the current element is rendered and we have the correct size of the text
- For textarea style i have set a height / min-height:
textarea{resize: none; box-sizing: content-box; height: auto; overflow: hidden;}
textarea.form-control{ min-height: 16px; height: 16px; width: 100%; }
- Sample usage
<textarea class="form-control word-wrap" auto-size placeholder="Set Location…" ng-model="item.address" ui-focus="isInTabFocus(listIndex, 1)" tabindex="{{getTabIndex(listIndex,1)}}"></textarea>
-
.directive('autoSize', ['$timeout', function($timeout) { return function (scope, element, attr, ctrl) { var minHeight; var $shadowtx = angular.element("<textarea class='" + attr['class'] + "'></textarea>"); //add to element parent angular.element(element.parent()[0]).append($shadowtx); element.css({resize: "none"}).parent().css({ position: "relative" }); var update = function (addtext) { $shadowtx.val(element.val() + (addtext ? addtext: '')); element.css('height', Math.max($shadowtx[0].scrollHeight, minHeight) + 'px'); }; element.bind('keydown', function (event) { var keyc = event.keyCode || event.which; if (keyc == 13) { update("\n"); } }).bind('keyup', function (event) { var keyc = event.keyCode || event.which; if ((keyc == 46) || (keyc==8)) { //delete, backspace, not fired by scope.$watch update(); } }); $timeout(function(){ minHeight = element[0].offsetHeight; $shadowtx.css({ position: 'absolute', top: -10000, left: -10000, resize: 'none', width: element.width(), height: element.height() }); update(); }, 0); scope.$watch(attr['ngModel'], function(v){update();}); } }]);
@symonny - works great OOTB. Nice!
It doesn't work if ng-model is set by code (controller). Like I fetch text from server and set it there on success so it doesn't set proper height.
I found that the CSS values for "top" and "left" did not work with the jqlite built into angular js. To use this code change the top and left values from -10000 to "-10000px".
If you use this code with the jquery library loaded, jquery will automatically append the "px" at the end of your value. However jqlite does not.