Open wvl opened 12 years ago
Something like this could work (untested):
editor.observe("load", function() { editor.composer.element.addEventListener("keyup", function() { editor.composer.iframe.style.height = editor.composer.element.scrollHeight + "px"; }); });
I'm considering it to implement it for wysihtml5 0.4. Please don't use the issues for questions or support requests :) thanks!
I would appreciate this feature. That code causes the height to grow, but not shrink.
With textareas, the accepted means of doing this seems to be to clone the textarea, set the clone's height to 0, then track the height of the original textarea's height to the scrollTop of the clone. One example is: https://github.com/jackmoore/autosize/blob/master/jquery.autosize.js
However, assuming this approach works, it would require cloning the contenteditable in the iframe. I'm not sure that's too feasible from outside wysihtml5.
Sorry for opening an issue for a question -- if there's a better place for such questions (aside from reading the source and figuring it out myself), maybe mention it in the README? There's lots of good stuff hidden, undocumented in the source, I thought I might've missed it.
Thanks.
I hope I can implement something for v0.4.
@wvl Well apparently you are right: opening an issue is probably the best way right now to get an answer and share knowledge :) Thanks
Would it be possible to get a wiki?
Of course. Coming soon!
I :+1: on issues for feature requests. This way you can group everybody asking about the same thing. Thanks to this ticket I was able to come up to speed, if I get it working I will share a pull request
Here's a working snippet that allows a textarea to resize: https://gist.github.com/2243439
It grows and shrinks. It calculates the target size by creating a test container to measure dimensions.
this.editor = new wysihtml5.Editor("textarea");
this.editor.observe("load", function () {
$(this.composer.iframe).autoResize();
});
Awesome. Will do a code review and consider implementing it. Thanks @micho!
My only worries are using jQuery for measuring and some Underscore.
This seems to work quite well. Already had the dependencies, so no issues there. Thanks!
A dynamically-sized editor (that behaves like a contenteditable element directly on the page) is the number one thing I need to figure out before I can use this. Glad to hear it's got your attention.
Iceton, check out the snippet I posted. It's fully functional, but it's hard to merge it since it depends on jquery and underscore.
Sent from my phone
On 11/04/2012, at 06:35, iceton reply@reply.github.com wrote:
A dynamically-sized editor (that behaves like a contenteditable element directly on the page) is the #1 thing I need to figure out before I can use this. Glad to hear it's got your attention.
Reply to this email directly or view it on GitHub: https://github.com/xing/wysihtml5/issues/18#issuecomment-5062042
Saw that, thanks micho! I'm not using Underscore, so I'm hoping for/messing around with a library-independent solution.
The underscore part is easily replaceable! You can just pull out the function from the source.
Sent from my phone
On 11/04/2012, at 20:50, iceton reply@reply.github.com wrote:
Saw that, thanks micho! I'm not using Underscore, so I'm hoping for/messing around with a library-independent solution.
Reply to this email directly or view it on GitHub: https://github.com/xing/wysihtml5/issues/18#issuecomment-5076004
I've found that the code @micho posted has an issue when the editor's contents contain an image without explicit height attributes.
This is because the image is not loaded when the height of the test container is measured.
While this adds another dependency, I'm using this plugin: https://github.com/desandro/imagesloaded (which sets up an imagesLoaded event, even for dynamically added images), and have modified the adjustHeight function to:
a.prototype.adjustHeight = function () {
var a, b, c, d, e, _this;
_this = this;
this.$testContainer.width(this.$source.width());
e = this.$testContainer.html("X").height();
this.$testContainer.html(this.sourceContents());
this.$testContainer.imagesLoaded(function(){
d = parseInt(_this.$el.data("rows") || _this.$el.attr("rows")) || false;
c = d === 1 ? 1 : _this.resizeBy * e;
a = d ? e * d + 1 : _this.originalHeight;
b = _this.$testContainer.height() + c;
if (_this.heightLimit && b > _this.heightLimit) {
b = _this.heightLimit;
}
if (b < a) {
b = a;
}
b = Math.round(b);
return _this.$el.css("min-height", b);
});
};
Edit: As I experiment with this change, it seems to have some issues... Ok, there can be an issue, I think, where we're getting overlapping "imagesLoaded" events. By increasing the _.throttle window from 5ms to 100ms seems to have suppressed the problem, at least on this computer (needs more testing), but is still quick enough that it feels fine.
a.prototype.watchForChanges = function () {
var a = this;
this.$source.bind("keyup keydown paste change focus", _.throttle(function (evt) {
return a.adjustHeight(evt);
}, $.support.touch ? 300 : 100));
this.$el.closest("form").bind("reset", function () {
return a.resetHeight();
});
};
I've run across other difficulties with this resize process that I'm going to have to, for expediency, go back to @tiff's first solution and forgo shrinking.
In our case, we have a lot of images and oembed-previewed objects.
Every time the contents gets copied to the test node for sizing, all these assets reload again. Some of the images are cached, but some others are not, and the oembeds are not cached either, and this is causing a LOT of network traffic.
Just wanted to bring this up for anybody else that might have a content load similar to ours.
Shrink-to-fit: The snippet by @tiff above works for me in Chrome more or less by adding the following:
remove "height", "padding-top", "padding-bottom" from BOX_FORMATTING -because this is causing the fixed height of the iframe add "min-height" to BOX_FORMATTING (so we can see a composer line)
add textarea{min-height: 2em;} in the CSS (height of this line)
Would be great if someone could try this. I think that the composer is already shrink-to-fit (because of inline-block), and its height can be copied to its parent iframe if it is allowed to.
I got this working properly by observing keyup, focus and blur. Shrinking works only on blur, but thats acceptable for me.
var resizeIframe = function() {
editor.composer.iframe.style.height = editor.composer.element.scrollHeight + "px";
}
editor.on("load", function() {
editor.composer.element.addEventListener("keyup", resizeIframe, false)
editor.composer.element.addEventListener("blur", resizeIframe, false)
editor.composer.element.addEventListener("focus", resizeIframe, false)
})
Didnt mess with BOX_FORMATTING.
@TomoGlavas: Great fix. Having a bit of a focus issue, though: clicking at a lower point in the document causes the page to jump to the top. Related?
Yea, I had some strange effects happening too. Temporarily, I use a different check for height, as iframe and composer height values seem to not allways reflect reality. No strange effects with this code:
var resizeIframe = function() { if($(chunk).find(".wysihtml5-sandbox").height() != editor.composer.element.offsetHeight) { $(chunk).find(".wysihtml5-sandbox").height(editor.composer.element.offsetHeight); } }
@TomoGlavas what does chunk
refer to in that last snippet?
Sory bout that, its a proprietary element of my cms. Here its just a html element containing the editor.
@TomoGlavas if I use the code from your last comment I get a weird effect where the box increases in size minutely for every character typed...
It is tricky, yes. Try playing the css of the editor (css that is applied to the body inside the iframe), specifically - padding. It affects the height calculation and can cause such effects.
@TomoGlavas FTW!
Hi, love to have this feature too
I see that you plan to add auto-resize for V0.4 … great feature !! :) any release date ?
@tiff … i'm not good at javscript but i have this code to emulate this feature : (btw, i use multiple wysihtml5 whom can be added dynamicaly … if you need some beta tester, don't hesitate ! it would be a pleasure to help you :) )
When a new word is added:
//resize iframe
function onNewWord() {
var editorHeight = editor.composer.commands.doc.body.clientHeight;
editor.composer.iframe.style.height = editorHeight+20+"px";
};
and on load :
function onLoad() {
resizeTextFrame();
}
function resizeTextFrame(){
var editorHeight = editor.composer.commands.doc.body.clientHeight;
var iframeClassName = ".wysihtml5-sandbox."+ textareaid;
$(iframeClassName).height(editorHeight+20);
}
I have been working on code to resize the wsyihtml5 iframe for a while now. The aforementioned autoresize.js does not working correctly with certain content. Also it's use of a temporary off-screen copy of the content is not ideal, performance wise. And the code is hard to read!
The recommend way to calculate actual content height is to use scrollHeight
(autoresize.js doesn't use this).
However with certain content this doesn't give the correct height when the content is inside the <body>
element. And different browsers behave differently. For example in Chrome, if the first node is a textnode
, scrollHeight is incorrect.
To resolve this issue we need to wrap the content in a <div>
. I am doing this outside of wysihtml5, however it is all a bit messy and I have seen a case where the div wrapper was able to be deleted by the user.
So the best way to handle this is for wysihtml5 to remove the contenteditable attribute from the <body>
, add a <div>
child node to it and make this the contenteditable element. This paves the way for correct auto-resizing and prevents the user from ever deleting the div.
I've had a look through the code and the <body>
element is used all over the place. The editor (composer) itself uses this.element
as the editable element, but whether changing this to the new div will work properly I have no idea.
So I'd really like to see this change incorporated, as we can then finally solve the resizing issue that many of us want and need.
-Neville
FWIW I would definitely also be interested in whatever comes up in v0.4 for resizing the WYSIHTML5 editor.
I'd like to suggest a small snippet of logic to add to the resizeIframe()
proposed by @tiff and @TomoGlavas. This snippet checks to make sure the editor has a certain scrollHeight, e.g., 200px, before resizing the iframe. Without it, the iframe will shrink down to less than 1 line tall until more than 1 line of text exists which is terrible from a UI/UX perspective.
var resizeIframe = function() {
//check to make sure the scrollHeight is some reasonable height, e.g, 200px, before resizing the <iframe>
if(editor.composer.element.scrollHeight>200) editor.composer.iframe.style.height = editor.composer.element.scrollHeight + "px";
};
//editor.composer.iframe.style.height='1000';
editor.on("load", function() {
editor.composer.element.addEventListener("keyup", resizeIframe, false)
editor.composer.element.addEventListener("blur", resizeIframe, false)
editor.composer.element.addEventListener("focus", resizeIframe, false)
});
The only other alternative/suggestion I can add to this discussion is to consider using JqueryUI's resizeable(). However, this is a seriously suboptimal solution since 1) its manual resizing; 2) you really can't get JqueryUI component by component (don't want to tack on 100kb just for this).
how i done that, using jquery
html :
<div id="editor"><textarea>:)</textarea></div>
css :
#editor > * { width:100%; height:100%; padding:0; margin:0; }
javascript :
var $editor = $("#editor");
var editor = ... init wysihtml5 ...
var ifrm = $(editor.composer.iframe).css({border:0});
var ifrmContent = $(ifrm[0].contentWindow.document);
ifrmContent = ifrmContent.find("html").css({width:"100%",height:"100%",margin:0,padding:0,overflow:"hidden"}).find("body").css({height:"auto",width:"100%",margin:0,padding:0});
function resize(){
var h = ifrmContent.height();
$editor.stop().animate({height:h});
}
editor.composer.element.addEventListener("keyup", resize);
editor.on("aftercommand:composer", resize);
window.setTimeout(resize,10);
Love this thread so far! Autoresizing is the major missing feature in wysihtml5, as far as I can see.
The examples above pretty much nailed it for me. I found one or two minor things:
box-sizing: border-box
on textareas, which gets imported by wysihtml5, so when setting the <iframe>
height I needed to factor in border width and padding to get the height right.<iframe>
to the exact scrollHeight of the <body>
, you get a weird effect on newlines where all the text in the <body>
shunts upwards (on the keydown) before the height increases (on the keyup). The simplest way around this (I've found) is to add a buffer (of 50px) to the <iframe>
height, so a newline doesn't cause the <body>
to shunt down.<iframe>
body into a new <div>
, get the height of that <div>
, remove it again, and set the height of the <iframe>
based on the <div>
.The code I ended up using, that covers all these, is below. Note that it doesn't do detection for the box-sizing mode - it just assumes you'd always use border-box, because why wouldn't you! Also note that it's a weird mix of jQuery and native JS, that could probably be cleaned up a bit
var editor = new wysihtml5.Editor('textarea', {
useLineBreaks: false
});
editor.on('load', function()
{
var minheight = 150;
var buffer = 50;
var padding = parseFloat(editor.composer.iframe.style.paddingTop) + parseFloat(editor.composer.iframe.style.paddingBottom) + parseFloat(editor.composer.iframe.style.borderTopWidth) + parseFloat(editor.composer.iframe.style.borderBottomWidth);
editor.composer.iframe.style.height = (minheight + padding) + 'px';
var resize = function() {
var $div = $('<div>').append($(editor.composer.element).clone().contents()).appendTo(editor.composer.element);
var scrollheight = $div.get(0).scrollHeight;
$div.remove();
if (scrollheight > (minheight - buffer)) editor.composer.iframe.style.height = (scrollheight + buffer + padding) + 'px';
else editor.composer.iframe.style.height = (minheight + padding) + 'px';
}
editor.composer.element.addEventListener('keyup', resize, false)
editor.composer.element.addEventListener('blur', resize, false)
editor.composer.element.addEventListener('focus', resize, false)
});
It seems kind of expensive to do a clone() on every keyup, have you seen any performance issues with it? Other than that, I like this approach - I've been having trouble with the backspacing too.
I agree. I was a bit worried, but I went with that anyway because I couldn't think of anything more efficient that'd still be accurate. I suppose you could check the keycode and only calculate the height the expensive way if backspace or delete were pressed (or if there was a range selected before the keypress). Seems like more trouble than it's worth though.
I'm seeing no real performance issues on my main machine using 500 paragraphs of ipsum text. I've not tested on mobiles or slightly crapper machines yet, but I can't imagine it's too awful.
There's a slowness generally with wysihtml5 when you remove a newline before an extremely long line of text (> 5KB ish), but the resize code above doesn't make any noticeable difference to that.
with overflow hidden on the iframe html we can directly read body height without any clone or visible scrollbar
That works when increasing the height, but doesn't work when reducing the height. The problem is that the scrollheight of a <body>
tag is always 100% of its frame (in this case, the <iframe>
).
The only ways to ascertain the correct scrollheight when shrinking the <iframe>
are:
<iframe>
until the scrollheight of the <body>
is higher, then stop.<body>
into a <div>
, and using that <div>
's scrollheight instead.<div>
inside the <body>
, which would require a complete reworking of wysihtml5.I went for #2. I imagine #3 is what the team will settle on in the long run when this feature gets baked in, but it's a bit of a ballache! And might mess up some people's stylesheets.
i was put height:auto on the iframe body
and it work well, check this demo page http://r043v.github.com/jQuery.scribe/
the only problem i get is the position not stay at top when animate bigger (i need try your 50px more solution)
@r043v I've tested your code and it works quite well. I've enhanced it to take into account my findings using scrollheight on a div wrapper. I've tested my updates on the latest Chrome and Firefox and on IE9.
I had all sorts of problems when using animate() and have removed that. There is also a slight content jump when Enter etc. is pressed.
/** Auto-resize the iframe by resizing it's parent wrapper.
* ref: r043v code at https://github.com/xing/wysihtml5/issues/18#issuecomment-11041675
* @param editor is wysihtml5 editor instance.
*/
autoResize: function( editor ){
var iframe = $(editor.composer.iframe).css({border:0});
var iframeDocument = $(iframe[0].contentWindow.document);
var iframeBody = iframeDocument.find("html").css({width:"100%",height:"100%",margin:0,padding:0,overflow:"hidden"})
.find("body").css({height:"auto",width:"100%",margin:0,padding:0});
function resize(){
// console.log( '1) iframeBody:', iframeBody, 'iframeBody.height', iframeBody.height(), 'iframeBody.scrollHeight', iframeBody.get(0).scrollHeight );
// Do an initial height change so we get the correct scrollHeight
var $editor_wrapper = iframe.parent();
$editor_wrapper.css( { height: iframeBody.height() } );
// For Firefox, scrollHeight doesn't include offsetTop for the first child node.
// Where the firstchild has offsetTop > 0 we need to add it. ex. <body><h3>...
var $bodyChildren = iframeBody.children();
var offsetTop = ( $bodyChildren.length && $bodyChildren.get(0).nodeType == 1 ) ? $bodyChildren.get(0).offsetTop : 0;
// scrollHeight now gives the correct document height, so use it.
$editor_wrapper.css( { height: iframeBody.get(0).scrollHeight + offsetTop } );
// For Firefox if the iFrame has no content it's height will be 0, so set it to body.line-height.
if ( iframeBody.height() < parseInt( iframeBody.css('line-height') ) )
$editor_wrapper.css( { height: parseInt( iframeBody.css('line-height') ) } );
// console.log( '2) iframeBody:', iframeBody, 'iframeBody.height', iframeBody.height(), 'iframeBody.scrollHeight', iframeBody.get(0).scrollHeight, 'offsetTop', offsetTop, 'iframeBody.line-height', iframeBody.css('line-height') );
}
editor.composer.element.addEventListener( "keyup", resize );
editor.on( "aftercommand:composer", resize );
resize(); // kick things off
},
and in editor creation:
editor.on( "load", function(){
// Init iframe auto height resizing. rem: Only call this after content is loaded.
self.autoResize( editor );
});
I'm still of the opinion that wysihtml5's should use a <div contenteditable=true>
instead of <body>
and then the height calc is even simpler. See my earlier post.
what type of content need this addition ?
@r043v Please read the comments in my code, they should explain the issues it addresses. In addition if the first node is a text node there was a problem.
I'm trying o implement @nevf sollution - rewritten in pure JS - with a few changes.
Commit: https://github.com/gabrielengel/wysihtml5/commit/eca6331cbd3205e7869b240818dc90a5ea9b99ca
Important changes:
1) It's possible not to use the autoResize: https://github.com/gabrielengel/wysihtml5/commit/eca6331cbd3205e7869b240818dc90a5ea9b99ca#L0R45
2) Avoid default WysiHtml5's blur & focus reset height: https://github.com/gabrielengel/wysihtml5/commit/eca6331cbd3205e7869b240818dc90a5ea9b99ca#L1R165
Pending: 1) Firefox compatibility - I had some issues with childNodes method. In Chrome it returns all elements, even if plain text, Ff doesn't. 2) Test in IE
Any feedback will be appreciated :)
Still had no luck in fixing it to work with Firefox.
I am having a strange dispairity between the iframe's body size while running a test, will take some more time on that.
Released a minified working version for Chrome here: https://gist.github.com/4345332
Fiddle example: http://jsfiddle.net/gabito/jnSPf/
Hey, just to update. I'm working on a new approach that seems tons of times easier than what we were doing. I'm encapsulating all the editor's content in a container and letting the browser tell us what is it's final height...
Sounds pretty obvious, no visible conflicts and will allow us to have "auto-resize-down".
ASAP will send a sample!
One of the problems I had when I attempted to write it was adding images, that would resize the container after they loaded and made things harder.
@gabrielengel My earlier post mentioned than moving the contenteditable
attribute from the body to a new div
container would indeed simplify this.
@micho Seems that once the image was loaded, this technique will work properly:
@nevf not sure how/where to change this, we should check with xing people. I see your point and could be handy indeed.
I am struggling to observe the composer element for keydown/keypress events. Did someone work with that before?
Good news :)
Seems that this logic works! The resize itself is working very well under Firefox and Chrome on a Mac. Now I'm still trying to attach events correctly.
Please, have a look: http://jsfiddle.net/gabito/jnSPf/2/
What works and doesnt:
Chrome:
Firefox:
Both:
This is due to the assignment of events, which today is like this: https://gist.github.com/4452796
I'm installing IE VMs to test here, but if anyone has the chance to open that jsfiddle on other browsers, I would appreciate :)
Current version (unminified): https://gist.github.com/raw/4452703/f7512ab0b0c097f8d08a63dcfc8d8e3f24274dae/wysihtml5_autoresize.js
Cheers!
@gabrielengel nice job with the jsfiddle. I'm testing in Chrome 23 and found the following issues:
Looks awesome so far!
First day trying out the editor, but immediately ran into this problem. Put together something with these ideas that works without cloning and with some other behavior quirks addressed:
https://gist.github.com/4498389
The trick is to edit a div inside the body of the iframe instead of making the body the area being edited. Unlike the body, the div height will be correctly recalculated without needing to clone the contents into another div. Also needed to disable some of the auto copy of styles when auto size is turned on.
Pass autoSize: true in config to enable.
Buffer height is being fixed at 35 right now because that's what my content uses. Should probably be configurable or calculated.
Hi @jezell! How did you implement the div wrapper? Or you have to declare it every time you instantiate?
Looks like a pretty clean code :+1:
Bingo! Found the missing links: editor.on("newword:composer", resize); editor.on("paste", resize);
@jezell, I've stolen a bit of your code too, let's get to a conclusion, merge, test and try a pull request :)
Will send a fiddle again in a few minutes
Fiddle: http://jsfiddle.net/gabito/jnSPf/5/ Unminified source: https://gist.github.com/raw/4452703/c60d871bf4b9c253dcfca0cefea4a6411b94aa0c/wysihtml5_autoresize.js Observing: https://gist.github.com/4452796
Is there any way to auto resize the editing area based on the size of the content?