tiff / wysihtml5

Open source rich text editor based on HTML5 and the progressive-enhancement approach. Uses a sophisticated security concept and aims to generate fully valid HTML5 markup by preventing unmaintainable tag soups and inline styles.
http://xing.github.com/wysihtml5/
MIT License
6.49k stars 1k forks source link

Autoresize the content area? #18

Open wvl opened 12 years ago

wvl commented 12 years ago

Is there any way to auto resize the editing area based on the size of the content?

tiff commented 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!

wvl commented 12 years ago

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.

tiff commented 12 years ago

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

Neener54 commented 12 years ago

Would it be possible to get a wiki?

tiff commented 12 years ago

Of course. Coming soon!

micho commented 12 years ago

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

micho commented 12 years ago

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();
});
tiff commented 12 years ago

Awesome. Will do a code review and consider implementing it. Thanks @micho!

micho commented 12 years ago

My only worries are using jQuery for measuring and some Underscore.

jwcooper commented 12 years ago

This seems to work quite well. Already had the dependencies, so no issues there. Thanks!

iceton commented 12 years ago

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.

micho commented 12 years ago

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

iceton commented 12 years ago

Saw that, thanks micho! I'm not using Underscore, so I'm hoping for/messing around with a library-independent solution.

micho commented 12 years ago

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

edwardmsmith commented 12 years ago

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();
      });
    };
edwardmsmith commented 12 years ago

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.

ingochao commented 12 years ago

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.

wyrd-code commented 12 years ago

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.

beep commented 12 years ago

@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?

wyrd-code commented 12 years ago

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); } }

pdf commented 12 years ago

@TomoGlavas what does chunk refer to in that last snippet?

wyrd-code commented 12 years ago

Sory bout that, its a proprietary element of my cms. Here its just a html element containing the editor.

pdf commented 12 years ago

@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...

wyrd-code commented 12 years ago

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.

adamyonk commented 12 years ago

@TomoGlavas FTW!

brainztorm commented 11 years ago

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);
    }
nevf commented 11 years ago

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

tim-peterson commented 11 years ago

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).

r043v commented 11 years ago

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);
dhoulb commented 11 years ago

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:

  1. I use 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.
  2. When you set the height of the <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.
  3. When you backspace and remove lines, the scrollheight isn't calculated correctly (as it includes the CSS height property we set on the previous keypress). As someone mentioned above, the best way to do it on textareas is to clone the textarea, set the height to 1, and calculate the height off the clone. I found the best way to do this in wysihtml5 was to clone the contents of the <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)
});
adamyonk commented 11 years ago

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.

dhoulb commented 11 years ago

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.

r043v commented 11 years ago

with overflow hidden on the iframe html we can directly read body height without any clone or visible scrollbar

dhoulb commented 11 years ago

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:

  1. Reduce the height of the <iframe> until the scrollheight of the <body> is higher, then stop.
  2. Clone the contents of the <body> into a <div>, and using that <div>'s scrollheight instead.
  3. Only ever work in a <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.

r043v commented 11 years ago

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)

nevf commented 11 years ago

@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.

r043v commented 11 years ago

what type of content need this addition ?

nevf commented 11 years ago

@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.

gabrielengel commented 11 years ago

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 :)

gabrielengel commented 11 years ago

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/

gabrielengel commented 11 years ago

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!

micho commented 11 years ago

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.

nevf commented 11 years ago

@gabrielengel My earlier post mentioned than moving the contenteditable attribute from the body to a new div container would indeed simplify this.

gabrielengel commented 11 years ago

@micho Seems that once the image was loaded, this technique will work properly:

Screen Shot 2013-01-04 at 10 24 45

@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?

gabrielengel commented 11 years ago

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!

micho commented 11 years ago

@gabrielengel nice job with the jsfiddle. I'm testing in Chrome 23 and found the following issues:

Looks awesome so far!

jezell commented 11 years ago

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.

gabrielengel commented 11 years ago

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:

gabrielengel commented 11 years ago

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

gabrielengel commented 11 years ago

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