Wednesday, May 6, 2015

Both Draggable and Resizable Images with jQuery UI

Not a WYSI wig.
Probably the most important pieces of my WYSIWYG comic editor are the ability to drag the frogs around and pull on the corners to resize them. These were the first things I added to the editor, and of course where I encountered the first weird problems.

My idea was to just display the comic exactly like it is displayed to people viewing it, but make the images draggable and resizable using the features in jQuery UI. However, there's always a catch. It turns out that when you apply resizable to an image element directly, jQuery wraps it in a div. Then if you apply draggable to it, the container it drags inside is that wrapper div and you get strange behavior.

Try it yourself. Here's some code similar to my starting point. You will be able to resize the image but not drag it.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Draggable and Resizable</title>
  <link rel="stylesheet" href="jquery-ui-1.10.4.min.css">
</head>

<body>

  <img id="frogImg" style="width: 276; height: 281;" src="frog.svg">

</body>

<script src="jquery-1.11.2.min.js"></script>
<script src="jquery-ui-1.10.4.min.js"></script>
<script>

$(function() {

    $("#frogImg").draggable();
    $("#frogImg").resizable({aspectRatio: true});

});

</script>

</html>

The trick was to wrap the images in a <div> and set that wrapper div to be draggable instead of the image directly. The image still has resizable applied to it, but only after draggable is applied to the wrapper. Then it will work perfectly. That little snag cost me many hours, but it doesn't have to cost you! Look at the better example code below:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Draggable and Resizable</title>
  <link rel="stylesheet" href="jquery-ui-1.10.4.min.css">
</head>

<body>

  <div id="wrapper">
    <img id="frogImg" style="width: 276; height: 281;" src="frog.svg">
  </div>

</body>

<script src="jquery-1.11.2.min.js"></script>
<script src="jquery-ui-1.10.4.min.js"></script>
<script>

$(function() {

    $("#wrapper").draggable();
    $("#frogImg").resizable({aspectRatio: true});

});

</script>

</html>

The flip-side of that was that I couldn't just grab the size and position of the images when it was time to save the data. I had to examine my wrapper divs and understand the structure that jQuery adds to them in order to figure out the coordinates used to re-create the comic later. I need to get the top and left CSS attributes as well as the width of the resized image. After closely examining the DOM after jQuery did its stuff, I came up with the following solution.

// Sometimes, the style for 'top' and 'left' comes back as 'auto'...
// In these cases, we should treat these values as 0.
function getCssPosition(elem) {

    var pos = {};

    pos.top = isNaN(elem.css('top').replace('px', '')) ? 0 : Number(elem.css('top').replace('px', ''));
    pos.left = isNaN(elem.css('left').replace('px', '')) ? 0 : Number(elem.css('left').replace('px', ''));

    return pos;

}

function getData() {

    $("div.divimg").each(function(idx, elem) {

        var iEl = $(elem);

        // get the position of my draggable wrapper div
        var draggablePos = getCssPosition($(elem));
        var draggableTop  = draggablePos.top;
        var draggableLeft = draggablePos.left;

        // get the position of jQuery's resizable wrapper div
        var wrapper = iEl.find("div.ui-wrapper");
        var wrapperPos = getCssPosition(wrapper);
        var wrapperTop = wrapperPos.top;
        var wrapperLeft = wrapperPos.left;

        var t = draggableTop + wrapperTop;
        var l = draggableLeft + wrapperLeft;

        console.log("top = " + t);
        console.log("left = " + l);

        var img = wrapper.find('img');
        var w = Number(img.css('width').replace('px', ''));

        console.log("width = " + w);

    });

}

The first function normalizes the top and left values. Depending on if the image was specifically positioned before being drug around (in my case, I almost always do this), the values for CSS top and left might be of the format "NNNpx" or just the word "auto". If it comes back as auto, that essentially means 0 because these are offset values from the parent element.

The getData function can use that to find the top and left values for both my draggable wrapper and the ui-wrapper that jQuery puts around the resizable image. By adding them together, I arrive at the top and left values to use directly on the image when the comic is displayed in static form on the main page. It is necessary to add the values together like this to compensate for how jQuery sets absolute and relative positioning on the two divs in the process of making them draggable and resizable.

Additionally, by grabbing the "width" attribute of the image itself from inside the ui-wrapper div, I'll know how wide the image should be as well.

This example has been simplified somewhat, but that's basically how I get the position and size data for every image that makes up a comic. The editor assembles them all into a JSON string and POSTs this data to the Node backend which stores it in a database. When the comic is viewed, that JSON is applied to a Jade template and it comes back out just like it looked in the editor!

Amphibian.com comic for 6 May 2015