leifparker leifparker - 6 months ago 61
Javascript Question

Responsive jQuery UI with resizable / draggable elements

I am attempting to build a responsive interface using jQuery UI, that allows for movable and resizable elements, which will stay in proportion even if the screen is resized.

Example: http://jsfiddle.net/NSLmQ/3/

I am using the UI callbacks for

dragStop
and
resizeStop
to convert the pixel position values into percentage values, after the user has completed their resize or drag.

function resizeStop(event, ui){
convert_to_percentage($(this));
}

function dragStop(event, ui){
convert_to_percentage($(this));
}

function convert_to_percentage(el){
var parent = el.parent();
el.css({
left:parseInt(el.css('left'))/parent.width()*100+"%",
top: parseInt(el.css('top'))/parent.height()*100+"%",
width: el.width()/parent.width()*100+"%",
height: el.height()/parent.height()*100+"%"
});
}


This works perfectly fine if the parent container has an explicit height and width — jQuery UI converts the percentages back to pixels before subsequent resize or drags, and my callbacks return them to percentage after the finished resize or drag. All hunky-dory.

The problem occurs during resize when the resizable element's parent is set to height:auto (in the linked Fiddle, I am using a child image to give the containing parent a height)

When I attempt to resize the UI element, jQuery UI will improperly convert the percentage of the top and left into pixels, and the element will jump in position.

This problem seems to be Chrome-specific. Firefox exhibits occasional mini-jumps in position, but they seem to be a matter of slight rounding differences. In Chrome, the position shifting is dramatic.

While setting an explicit height on the parent container solves the UI problem, it doesn't allow for a responsive solution.

Any ideas?

===========================================

Late Update



@peteykun's answer below solved my fundamental problem, but there was one offshoot problem that I thought it might be worth addressing.

Chrome uses sub-pixels when calculating 'auto' sizing, so that getting the height with jQuery through
.height()
returns an inexact, rounded answer (and causes a visual jiggle during the conversion between pixels and percentage, and vice-versa).

Therefore, using the Vanilla JS
.getBoundingClientRect().height
instead of
.height()
returns a sub-pixel result for a jiggleless solution.

function setContainerSize(el) {
var parent = $(el.target).parent().parent();
parent.css('height', parent[0].getBoundingClientRect().height+'px');
}


Thanks again for your help, @peteykun

Answer

How about setting the container's height explicitly before the resize takes place?

Ideally, this should have been doable with the start option for .resizable{}, but I tried it out myself, and it did not help. Instead, by binding a .mouseover() to the handle, the desired effect seems to be achieved:

$('.box')
.resizable({containment: 'parent', handles: "se", create: setContainerResizer, stop:resizeStop})

function setContainerResizer(event, ui) {
    console.log($(this)[0]);
    $($(this)[0]).children('.ui-resizable-handle').mouseover(setContainerSize);
}

function setContainerSize(el) {
    var parent = $(el.target).parent().parent();
    parent.css('height', parent.height() + "px");
}

JSFiddle: http://jsfiddle.net/w2Jje/4/


Update: In order to retain responsiveness, you can make the height auto once again after the resize is complete. Check the update below.

function convert_to_percentage(el){
    var parent = el.parent();

    el.css({
        left:parseInt(el.css('left'))/parent.width()*100+"%",
        top: parseInt(el.css('top'))/parent.height()*100+"%",
        width: el.width()/parent.width()*100+"%",
        height: el.height()/parent.height()*100+"%"
    });

    parent.css('height', 'auto'); // Set it back to auto
}

JSFiddle: http://jsfiddle.net/w2Jje/5/


Update #2: To get around the case where the mouse never leaves the handle before a second resize, let's reset the height on mouseout instead:

function setContainerResizer(event, ui) {
    console.log($(this)[0]);
    $($(this)[0]).children('.ui-resizable-handle').mouseover(setContainerSize);
    $($(this)[0]).children('.ui-resizable-handle').mouseout(resetContainerSize);
}

function resetContainerSize(el) {
    parent.css('height', 'auto');
}

JSFiddle: http://jsfiddle.net/w2Jje/6/