Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

A small jQuery Resizable Plug-in


:P
On this page:

A few days ago I was working on one of my old applications and needed to add support for a resizable panel based layout in HTML. Specifically this is for HTML Help Builder which is an application that generates HTML based documentation from class libraries, databases and that also lets you create help topics manually for full documentation purposes. The generated output for documentation typically has a two panel layout and I needed to integrate resizing functionality from the old school frames interface that had been in use before.

Surprisingly there aren't a lot of resizing libraries out there and the ones that are available tend to be rather large as they are either part of larger libraries or are trying to manage the UI specifically for a scenario such as panel layout components. I couldn't find anything that was lean and can just rely on basic CSS layout to handle the UI part of resizing. So as is often the case, I ended up creating my own small jquery-resizable plug-in as this isn't the first time I've looked into this.

The jquery-resizable Plug-in

jquery-resizable is a small jquery plug-in that handles nothing but the actual resizing of a DOM element. It has no direct UI characteristics other than physically resizing the element. It supports mouse and touch events for resizing and otherwise relies on CSS and HTML to handle the visual aspects of the resizing operations. Despite being minimalistic, I find it really easy to hook up resize operations for things like resizable windows/panels or for things like split panels which is the use case I set out to solve.

If you're impatient and just want to get to it, you can jump straight to the code on GitHub or check out some of the basic examples:

Creating a jQuery-resizable Plug-in

jQuery-resizable is a small jQuery plug-in that – as the name implies – resizes DOM elements when you drag them in or out. The component handles only the actual resizing operation process and doesn't deal with any UI functionality such as managing containers or sizing grips – this is all left up to HTML and CSS, which as it turns out is pretty easy and very flexible. The plug-in itself simply manages the drag operation events for both mouse and touch operation and resizing the specified container(s) that is being resized. The end result is a pretty small component that's easily reusable.

You can use this component to make any DOM element resizable by using a jQuery selector to specify the resizable element as well as specifying a drag handle element. A drag handle is the element that has to be selected initially to start dragging which in a splitter panel would be the splitter bar, or in a resizable dialog would be the sizing handle on the lower left of a window.

The syntax for the component is very simple:

$(".box").resizable({
    handleSelector: "size-grip",
    resizeHeight: false,
    resizeWidth: true
});

Note that you can and should select a handle selector which is a separate DOM element that is used to start the resize operation. Typically this is a sizing grip or splitter bar. If you don't provide a handleSelector the base element resizes on any drag operation, which generally is not desirable, but may work in some situations.

The options object also has a few event hooks – onDragStart, onDrag, onDragEnd - that let you intercept the actual drag events that occur such as when the element is resized. For full information on the parameters available you can check the documentation or the GitHub page.

A Basic Example: Resizing a Box

Here's a simple example on CodePen that demonstrates how to make a simple box or window resizable:

ResizeBox[8]

In order to resize the window you grab the size-grip and resize the window as you would expect.

The code to enable this functionality involves adding the jQuery and jquery-resizable scripts to the page and attaching the resizable plug-in to the DOM element to resize:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js" type="text/javascript"></script>
<script src="scripts/jquery-resizable.js"></script>
<script>
    $(".box").resizable({ handleSelector: ".win-size-grip" });        
</script>

The key usage requirement is to select the DOM element(s), using a jQuery selector to specify the element(s) to resize. You can provide a number of options with the most important one being the .handleSelector which specifies the element that acts as the resizing initiator – when clicked the resizing operation starts and the as you move the mouse the base element is resized to that width/height.

As mentioned, jquery-resizable doesn't do any visual formatting or fix-up, but rather just handles the actual sizing operations. All the visual behavior is managed via plain HTML and CSS which allows for maximum flexibility and simplicity.

The HTML page above is based on this HTML markup:

<div class="box">
    <div class="boxheader">Header</div>
    <div class="boxbody">
        Resize me
    </div>
    <div class="win-size-grip"></div>
</div>

All of the UI based aspects – displaying the sizing handles (if any) and managing the min and max sizes etc. can be easily handled via CSS:

.box { margin: 80px; position: relative; width: 500px; height: 400px;
min-height: 100px; min-width: 200px; max-width: 999px; max-height: 800px; } .boxheader { background: #535353; color: white; padding: 5px; } .boxbody { font-size: 24pt; padding: 20px; } .win-size-grip { position: absolute; width: 16px; height: 16px; bottom: 0; right: 0; cursor: nwse-resize; background: url(images/wingrip.png) no-repeat; }

So to make the UI work – in this case the sizing grip in the bottom right corner – pure CSS is used.  The Box is set using position:relative and the grip is rendered to the bottom left corner with position:absolute which allows the grip to be attached to the lower right corner using a background image. You can also control sizing limitations using max/min/width/height in CSS.  to constrain the sizing to appropriate limits.

You can check out and play around with this simple example in CodePen or in the sample grabbed from GitHub.

A two panel Splitter with jquery-resizable

I mentioned that I was looking for a light-weight way to implement a two panel display that allows for resizing. There are a number of components available that provide this sort of container management. These are overkill for what I needed and it turns out that it's really easy to create a resizable two panel layout using jquery-resizable.

You can take a look at the Resizable Splitter Panels sample on CodePen to see how this works in a simple example.

SplitterPanelExample

Let's take a look and see how this works. Let's start with the top panel that horizontally splits the two panels. This example uses FlexBox to create two panes that span the whole width of the screen, with the left side being a fixed width element, while the right side is a variable width auto-stretching container.

Here's the HTML:

<div class="panel-container">

    <div class="panel-left">
        left panel
    </div>

    <div class="splitter">
    </div>

    <div class="panel-right">
        right panel
    </div>
</div>

Pretty simple – the three panels are contained in top level container that in this case provides the FlexBox container.  Here's the CSS:

/* horizontal panel*/
.panel-container {
    display: flex;
    flex-direction: row;   
    border: 1px solid silver;         
    overflow: hidden;
}
.panel-left {
    flex: 0 0 auto;  /* only manually resize */
    padding: 10px;
    width: 300px;
    min-height: 200px;
    min-width: 150px;
    white-space: nowrap;
            
    background: #838383;
    color: white;
}
.splitter {
    flex: 0 0 auto;
    width: 18px;
    background: url(images/vsizegrip.png) center center no-repeat #535353;
    min-height: 200px;
    cursor: col-resize;
}
.panel-right {
    flex: 1 1 auto; /* resizable */
    padding: 10px;
    width: 100%;
    min-height: 200px;
    min-width: 200px;
    background: #eee;
}

FlexBox makes this sort of horizontal layout really simple by providing relatively clean syntax to specify how the full width of the container should be filled. The top level container is marked as display:flex and flex-direction: row which sets up the horizontal flow. The panels then specify whether they are fixed in width with flex: 0 0 auto or stretching/shrinking using flex: 1 1 auto. What this means is that right panel is auto-flowing while the right panel and the splitter are fixed in size – they can only be changed by physically changing the width of the element.

And this is where jquery-resizable comes in: We specify that we want the left panel to be resizable and use the splitter in the middle as the sizing handle. To do this with jquery-resizable we can use this simple code:

$(".panel-left").resizable({
   handleSelector: ".splitter",
   resizeHeight: false
});

And that's really all there's to it. You now have a resizable two panel layout. As the left panel is resized and the width is updated by the plug-in, the panel on the right automatically stretches to fill the remaining space which provides the appearance of the splitter resizing the list.

The vertical splitter works exactly the same except that the flex-direction is column. The layout for the verticals:

<div class="panel-container-vertical">

    <div class="panel-top">
        top panel
    </div>

    <div class="splitter-horizontal">
    </div>

    <div class="panel-bottom">
        bottom panel
    </div>
</div>

The HTML is identical to the horizontal except for the names. That's part of the beauty of flexbox layout which makes it easy to change the flow direction of content.

/* vertical panel */
.panel-container-vertical {
    display: flex;
    flex-direction: column;   
    height: 500px;
    border: 1px solid silver;         
    overflow: hidden;
}
.panel-top {
    flex: 0 0 auto;  /* only manually resize */
    padding: 10px;
    height: 150px;
    width: 100%;                        
    background: #838383;
    color: white;
}
.splitter-horizontal {
    flex: 0 0 auto;
    height: 18px;
    background: url(images/hsizegrip.png) center center no-repeat #535353;            
    cursor: row-resize;
}
.panel-bottom {
    flex: 1 1 auto; /* resizable */
    padding: 10px;            
    min-height: 200px;            
    background: #eee;
}

and finally the JavaScript:

$(".panel-top").resizable({
    handleSelector: ".splitter-horizontal",
    resizeWidth: false
});

It's pretty nice to see how little code is required to make this sort of layout. You can of course mix displays like this together to do both vertical and horizontal resizing which gets a little more complicated, but the logic remains the same – you just have to configure your containers properly.

The thing I like about this approach is that that JavaScript code is minimal and most of the logic actually resides in the HTML/CSS layout.

This is pretty close to the implementation I ended up with using for my Html Help Builder implementation of the final help layout, which ended up looking like this:

HelpBuilderSplitPanel

Sweet!

Implementation

The code for the jquery-resizable is pretty straight forward. The code essentially waits for mouseDown or touchStart events on the sizing handle which indicates the start of the resizing operation. When the resize starts additional mouse and touch events are hooked up for mouseMove and touchMove and mouseUp and touchEnd events. When the move events fire the code captures the current mouse position and resizes the selected element's width or height to that location. Note that the sizing handle itself is not explcitly moved – it should move on its own as part of the layout, so that when the container resizes the handle is moved with it automatically adjusting to the location.

For reference here's the relatively short code for the plug-in (or you can also check out the latest code on GitHub):

/// <reference path="jquery.js" />
/*
jquery-watcher 
Version 0.13 - 12/22/2015
© 2015 Rick Strahl, West Wind Technologies 
www.west-wind.com
Licensed under MIT License
*/
(function($, undefined) {    
    if ($.fn.resizable)
        return;

    $.fn.resizable = function fnResizable(options) {
        var opt = {
            // selector for handle that starts dragging
            handleSelector: null,
            // resize the width
            resizeWidth: true,
            // resize the height
            resizeHeight: true,
            // hook into start drag operation (event passed)
            onDragStart: null,
            // hook into stop drag operation (event passed)
            onDragEnd: null,
            // hook into each drag operation (event passed)
            onDrag: null,
            // disable touch-action on $handle
            // prevents browser level actions like forward back gestures
            touchActionNone: true
        };
        if (typeof options == "object") opt = $.extend(opt, options);
        
        return this.each(function () {            
            var startPos, startTransition;
            
            var $el = $(this);
            var $handle = opt.handleSelector ? $(opt.handleSelector) : $el;

            if (opt.touchActionNone)
                $handle.css("touch-action", "none");

            $el.addClass("resizable");
            $handle.bind('mousedown.rsz touchstart.rsz', startDragging);

            function noop(e) {
                e.stopPropagation();
                e.preventDefault();
            };
            
            function startDragging(e) {
                startPos = getMousePos(e);
                startPos.width = parseInt($el.width(), 10);
                startPos.height = parseInt($el.height(), 10);

                startTransition = $el.css("transition");
                $el.css("transition", "none");
                
                if (opt.onDragStart) {
                    if (opt.onDragStart(e, $el, opt) === false)
                        return;
                }
                opt.dragFunc = doDrag;

                $(document).bind('mousemove.rsz', opt.dragFunc);
                $(document).bind('mouseup.rsz', stopDragging);
                if (window.Touch || navigator.maxTouchPoints) {
                    $(document).bind('touchmove.rsz', opt.dragFunc);
                    $(document).bind('touchend.rsz', stopDragging);                    
                }
                $(document).bind('selectstart.rsz', noop); // disable selection
            }

            function doDrag(e) {                
                var pos = getMousePos(e);
                
                if (opt.resizeWidth) {
                    var newWidth = startPos.width + pos.x - startPos.x;                    
                    $el.width(newWidth);
                }

                if (opt.resizeHeight) {
                    var newHeight = startPos.height + pos.y - startPos.y;                    
                    $el.height(newHeight);
                }

                if (opt.onDrag)
                    opt.onDrag(e, $el, opt);

                //console.log('dragging', e, pos, newWidth, newHeight);
            }

            function stopDragging(e) {                
                e.stopPropagation();
                e.preventDefault();                

                $(document).unbind('mousemove.rsz', opt.dragFunc);
                $(document).unbind('mouseup.rsz', stopDragging);

                if (window.Touch || navigator.maxTouchPoints) {
                    $(document).unbind('touchmove.rsz', opt.dragFunc);
                    $(document).unbind('touchend.rsz', stopDragging);
                }
                $(document).unbind('selectstart.rsz', noop);

                // reset changed values
                $el.css("transition", startTransition);

                if (opt.onDragEnd)
                    opt.onDragEnd(e, $el, opt);
                
                return false;
            }

            function getMousePos(e) {
                var pos = { x: 0, y: 0, width: 0, height: 0 };                
                if (typeof e.clientX === "number") {
                    pos.x = e.clientX;
                    pos.y = e.clientY;
                } else if (e.originalEvent.touches) {
                    pos.x = e.originalEvent.touches[0].clientX;
                    pos.y = e.originalEvent.touches[0].clientY;
                } else
                    return null;

                return pos;
            }            
        });
    };
})(jQuery,undefined);

There are a few small interesting things to point out in this code.

Turning off Transitions

The first is a small thing I ran into which was that I needed to turn off transitions for resizing. I had my left panel setup with a width transition so when the collapse/expand button triggers the panel opens with a nice eas-in animation. When resizing this becomes a problem, so the code explicitly disables animations on the resized component.

Hooking into Drag Events

If you run into other things that might interfere with resizing you can hook into the three drag event hooks – onDragStart, onDrag, onDragEnd – that are fired as you resize the container. For example the following code explicitly sets the drag cursor on the container that doesn't use an explicit drag handle when the resize is started and stopped:

$(".box").resizable({
    onDragStart: function (e, $el, opt) {
        $el.css("cursor", "nwse-resize");
    },
    onDragStop: function (e, $el, opt) {
        $el.css("cursor", "");
    }
});        

You can return false from onDragStart to indicate you don't want to start dragging.

Touch Support

The resizing implementation was surprisingly simple to implement, but getting the touch support to work took a bit of sleuthing. The tricky part is that touch events and mouse events overlap so it's important to separate where each is coming from. In the plug-in the important part is getting the mouse/finger position reliably which requires looking both at the default jQuery normalized mouse properties as well as at the underlying touch events on the base DOM event:

function getMousePos(e) {
    var pos = { x: 0, y: 0, width: 0, height: 0 };
    if (typeof e.clientX === "number") {
        pos.x = e.clientX;
        pos.y = e.clientY;
    } else if (e.originalEvent.touches) {
        pos.x = e.originalEvent.touches[0].clientX;
        pos.y = e.originalEvent.touches[0].clientY;
    } else
        return null;

    return pos;
}

It sure would be nice if jQuery could normalize this automatically so properties things like clientX/Y and pageX/Y on jQuery's wrapper event could return the right values or either touch or mouse properties, but for now we still have to normalize manually.

Checking for Mouse and or Touch Support

On the same note the code has to explicitly check for touch support and if available bind the various touch events like touchStart, touchMove and touchEnd which adds a bit of noise to the otherwise simple code. For example, here's the code that decides whether the touchmove and touchend events need to be hooked:

if (window.Touch || navigator.maxTouchPoints) {                    
    $(document).bind('touchmove.rsz', opt.dragFunc);
    $(document).bind('touchend.rsz', stopDragging);                    
}

There are a couple of spots like this in the code that make the code less than clean, but… the end result is nice and you can use either mouse or touch to resize the elements.

Arrrggggh! Internet Explorer and Touch

It wouldn't be any fun if there wasn't some freaking problem with IE or Edge, right?

Turns out IE and Edge on Windows weren't working with my original code. I didn't have a decent touch setup on Windows until I finally managed to get my external touch monitor to work in a 3 monitor setup. At least now I can test under this setup. Yay!

Anyway. There are two issues with IE – it doesn't have window.Touch object, and so checking for touch was simply failing to hook up the other touch events that the plug-in is listening for. Instead you have to look for an IE specific navigator.maxTouchPoints property. That was problem #1.

Problem #2 is that IE and Edge have browser level gestures that override element level touch events. Other browsers like Chrome ave those too but they are a bit more lenient in their interference with the document. By default I couldn't get the touchStart event to fire because the browser level events override the behavior.

The workaround for this is the touch-action: none property that basically disables the browser from monitoring for document swipes for previous. This CSS tag can be applied to the document, or any container or as I was happy to see the actual drag handle. Ideally applying it to the drag handle doesn't have any other side effects on the document and prohibit scrolling so the code now optionally forces touch-action: none onto the drag handle via a flagged operation:

if (opt.touchActionNone)
    $handle.css("touch-action", "none");

You can try it out here with Edge, IE, Chrome on a touch screen. I don't have a Windows Phone to try with – curious whether that would work.

http://codepen.io/rstrahl/pen/eJZQej

Remember: If you support Touch…

As always if you plan on supporting touch make very sure that you make your drag handles big enough to support my fat fingers. It's no fun to try and grab a 3 point wide drag handle 10 times before you actually get it…

Slide on out

All of this isn't rocket science obviously, but I thought I'd post it since I didn't find an immediate solution to a simple way to implement resizing and this fits the bill nicely. It's only been a week since I created this little plug-in and I've retrofitted a number of applications with sliders and resizable window options where it makes sense which is a big win for me. Hopefully some of you might find this useful as well.

Resources

Posted in HTML5  jQuery  JavaScript  ASP.NET  

The Voices of Reason


 

Ian Yates
December 22, 2015

# re: A small jQuery Resizable Plug-in

Hi Rick,

Thanks for sharing :)

I found that touch in IE 11, at least in the first codepen sample, was broken (I haven't had a chance to try Edge). Altering the CSS to include touch-events: none, made it work just fine. That change still allowed the plugin to work with touch, on my Windows 8 laptop, in Chrome with touch too.

Rick Strahl
December 22, 2015

# re: A small jQuery Resizable Plug-in

@Ian - thanks for checking. You mean touch-action, right? 

I had to dig out my Windows touch screen to check this out as I'd been testing on iOS and Android devices where it works. I've updated the CodePens by adding touch-action: none and also added some base logic into the resizable that disables touch-action while resizing then resets it on the body element. This makes it work in IE even without the explicit CSS albeit with slight delay for the initial drag. But at least it works. Adding the touch-action: none in CSS makes the drag operation immediate.

I've also added some additional logic to check for maxTouchPoints which initially was not checked as IE doesn't supported window.Touches. It looks like it's working both in Chrome and IE/Edge now.

If not filing an issue on GitHub would be appreciated.

Ian Yates
December 22, 2015

# re: A small jQuery Resizable Plug-in

Hi Rick,

Yes touch-action - serves me right for typing from memory :)

Thanks for your reply and input. I'll visit GitHub and see if there's a place or two I might be able to make use of this.

Cheers!

Francis Kim
December 23, 2015

# re: A small jQuery Resizable Plug-in

Nice & very lightweight script! Thanks for the post up.

Tuvia
December 27, 2015

# re: A small jQuery Resizable Plug-in

I was unable to get the examples to work in FF, although they were fine in IE.

Rick Strahl
December 27, 2015

# re: A small jQuery Resizable Plug-in

@Tuvia - works for me. What version of FF are you using? Latest auto-updated version works fine.

It should work for versions IE 9 and later, although the slitter sample uses FlexBox which will work only in IE 10 or newer (and evergreen browsers).

Johan Bennink
January 08, 2016

# re: A small jQuery Resizable Plug-in

Works on Windows 10 Mobile Edge browser :-)

Rick Strahl
January 08, 2016

# re: A small jQuery Resizable Plug-in

@Johann - thanks for confirming. I figured it would since the desktop edge and phone browsers seem to be pretty similar in behavior.

robbie
March 03, 2016

# re: A small jQuery Resizable Plug-in

Hi mr. Strahl ,I am trying to catch the element under mouse when dragging resizable(standard ui.js) i think there's two problems to acquire the target attributes.
#1 i see that any element being concurred by resizable element, is moving away so app-on release mouse button, it's not there...
#2 the area is either document (body), or the helper / ghost object.

so i ask my self is it really the tool to do the job? i'm actually building similar idea, which will let me generate a layout of divs visually,
so maybe with all the code all ready done to enforce the logic to get to the data of the "neighbor" being concurred ,
which is the very first step in the idea of many other stages needed (this one is tricky!!)
i might as well do it all from scratch no ui-resizable();
did you ever encounter the need and issues as i described ?
what's your thoughts about it, i will be happy to collaborate on this small project.

David McGlynn
September 22, 2016

# re: A small jQuery Resizable Plug-in

In in your "Hooking into Drag Events" section, you erroneously list the hook as "onDragStop", but when you list the code in the "Implementation" section, you show that it's actually "onDragEnd".

Just wanted to point out a typo and maybe save someone 5-10 minutes.

David

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024