Wrangle jQuery Plugin

A responsive, touch-friendly jQuery plugin to wrangle up your images.

Download View Demo

Wrangle is an experimental plugin that will have you rustling up your herd of photos like a real cowpoke. Our tool gives your app a new way to perform multiple selections — using your mouse ... or even your fingers! So grab your lassos. This tool's your huckleberry.

Click and drag across these pictures to select them. Nifty, huh?

  • Photo 4
  • Photo 3
  • Photo 1
  • Photo 2

How to Use It

First, get the code by cloning the GitHub repository or directly downloading the files. (What—you didn't do that yet? Just click here.)

Include the plugin at the bottom of your page, before the closing body tag and after you've included jQuery.

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="js/jquery.wrangle.min.js"></script>

jQuery 1.8.3 is about as far back as we recommend going for this plugin. It also works just great with jQuery 2!

The HTML

There's a lot to the HTML needed to run Wrangle, but it's easy to understand. First, you need a container to hold the entire selection interface, buttons included:

<div data-wrangle>
</div>

Next is the drawing surface, which includes your list of selectable items and the HTML5 Canvas to draw with.

<div data-wrangle>
  <!-- This container responds to all mouse, touch, and pointer events -->
  <div data-selectarea>
    <!-- Your list should be a ul or ol -->
    <ul data-list>
      <li><img src="http://placekitten.com/400/300" alt="Kittens"></li>
    </ul>
    <!-- The canvas doesn't need a width and height set--we'll handle that -->
    <canvas data-canvas></canvas>
  </div>
</div>

Finally, you can optionally add buttons to your interface to add helpful features like select all or select none.

<div data-wrangle>
  <div data-selectarea>
    <ul data-list>
      <li><img src="http://placekitten.com/400/300" alt="Kittens"></li>
    </ul>
    <canvas data-canvas></canvas>
  </div>

  <!-- Selects all items -->
  <a data-all>Select all</a>
  <!-- Deselects everything -->
  <a data-clear>Select none</a>
  <!-- Toggles drawing on and off (more on that later!) -->
  <a data-edit data-alttext="Cancel">Edit</a>
  <!-- Define a custom action (more on THAT later also!) -->
  <a data-action="delete">Delete</a>
</div>

The CSS

We'll need a dash of CSS to get the functionality working right. None of it is visual, so you're free to style the plugin however you want, with your own classes.

[data-selectarea] {
  position: relative;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  -ms-touch-action: none;
}
[data-canvas] {
  position: absolute;
  top: 0;
  left: 0;
  background: transparent;
  z-index: 10;
}
[data-list] {
  width: 100%;
  background: transparent;
  padding: 1em;
  position: relative;
  z-index: 5;
}
  [data-list] li {
    pointer-events: none;
  }

Note the z-indexes on data-canvas and data-list. If you want the line drawn to appear over the list, keep the code how it is. However, if you'd rather the line disappear under the list items, give the canvas a lower z-index. Just make sure your list container is transparent, or you won't be able to see the line at all!

The JavaScript

Alright, we've finished our setup, and now we can actually start the plugin. We recommend initializing inside the window's load event:

$(window).on('load', function() {
  $(document).wrangle();
});

There are many ways you can customize Wrangle. To learn more about all of the plugin's options, and how to integrate it into your own apps, be sure to read the full documentation.

{
  // Line styles
  lineColor: '#000000',
  lineWidth: 5,
  // CSS classes
  editableClass: 'editable',
  drawingClass: 'drawing',
  selectedClass: 'selected',
  // Ability to toggle selections
  selectToggle: false,
  // Different ways to interact with touch
  touchMode: 'auto',
  multiTouch: false,
  clearOnCancel: true,
}

Score an awesome product engineering or design job

via Job Board from ZURB

Browser Support

Wrangle has been tested in:

  • Desktop
    • Chrome 28
    • Safari 6
    • Firefox 21
  • Mobile
    • Mobile Safari*
    • Chrome for iOS*
    • Android 4.0 Browser*
    • Chrome for Android
    • Kindle Fire browser

* Multitouch supported
Android 2.2 not supported.

GitHub

If you'd like to see the inner workings of Wrangle and the Wrangle demo up-close and personal, check out the code on GitHub:

Wrangle | Wrangle Demo

Thoughts?

Read more about Wrangle and the redesign of the ZURB Playground on our blog, and see what others are saying or leave us your feedback.


How Does it Work?

This was a tricky piece of tech to build. Let's delve into the web standards that made it possible.

Handling input

There are three main ways the user can directly interact with an interface: a mouse, a finger or a stylus. In JavaScript, these are represented as MouseEvent and TouchEvent objects. Most modern touch devices support touch events in JavaScript. Some browsers will still trigger mouse events on top of touch events for the sake of backwards-compatibility, which we have to watch out for.

The vast majority of browsers these days support the basic drawing we're doing with Canvas, but touch support is a bit more spotty (just look at this compatability chart). On desktop browsers, standard touch events are supported in Firefox 6+, Chrome 22+, and Opera 15+. On mobile devices, touch is supported by Android 2.3+ and iOS 3.2+. Android 2.3 only supports single touch events; 3.0 is the first version to support multi-touch events. Windows Phone 7 does not support touch events. Windows Phone 8 and IE10 do, but they use Microsoft's custom Pointer event model (which has since been submitted to the W3C as a potential future standard). This implementation funnels all mouse, pen, and touch input into one MSPointerEvent object.

Wrangle supports mouse, touch and MSPointer events, but your mileage will vary depending on the device and browser. iOS provides the best experience in terms of standards and smoothness.

To keep the codebase simple and streamlined, the coordinates of all input events are funneled into one master of array of coordinates, regardless of how many and what type. This keeps other parts of the plugin from having to worry about where the coordinates came from or how they were delivered.

Drawing

To draw on the interface itself, we just layered an HTML5 Canvas on top of the list of items. A canvas is transparent by default. The drawing area has to account for a few things:

  • Its position relative to the document: mouse and touch events send coordinates relative to the document, not the specific element, so we need to account for that.
  • The size of the list area: When the window is resized, the size of the list area is checked, and the canvas is resized to match it. We can't simply scale the canvas with a percentage width. This causes the coordinate system of the canvas to scale proportionally, which will misalign the line with the user's input.
  • Preventing default input: The default behavior on most all touch-enabled browsers is to scroll the page when a finger is dragged. We can counteract this with a combination of JavaScript and CSS.

To prevent the browser from dragging an image as a file reference when using a mouse, we set pointer-events to none on the list items:

[data-list] > li {
  pointer-events: none;
}

pointer-events: none instructs the browser to ignore any clicking on the element. Interactions will "pass through" this element to any elements directly underneath it.

Next, to make sure the drawing area works as intended, we add a bevy of prefixed properties to the CSS:

[data-selectarea] {
  position: relative;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  -ms-touch-action: none;
}

user-select: none prevents the highlighting effect of selecting text to occur within the drawing area. This is mostly for cosmetic purposes; user-select: none doesn't actually prevent the user from selecting text, it just removes the visual element.

If you're a fan of Compass or Bourbon, both of these libraries include mixins to output the prefixed versions of user-select for you:

// Compass
@import "compass/css3";
@import "compass/css3/user-interface"

[data-selectarea] {
  @include user-select(none);
}

// Bourbon
@import "bourbon";

[data-selectarea] {
  @include user-select(none);
}

-ms-touch-action: none is a property specific to Internet Explorer 10. It instructs the browser to ignore actions like panning, pinch to zoom, or other gestures on the element. This prevents the viewport from scrolling when the user wants to draw.

Making Selections

Now for the big time. Let's break down the selection process from start to finish.

First, we wait.

The plugin listens for the mousedown, touchstart, or MSPointerDown events, depending on the device.

On start → Set up drawing environment

When mouse/finger/pen use is detected, Wrangle springs into action, by calculating the bounding boxes of each selectable item. A bounding box is a set of coordinates and dimmensions that tell us where an item is and how big it is, which lets us determine the space it occupies on the screen. Wrangle grabs each <li> inside ul[data-list] and calculates a bounding box for it. We'll use these to figure out when the user's input overlaps with a selectable item.

Next, Wrangle does an initial collision check to see if the user clicked or tapped an item. This allows the user to select items without drawing.

Lastly, the plugin adds an event listener for the mousemove, touchmove, and MSPointerMove events. When the user moves their input device across the drawing surface, Wrangle will grab the coordinates of those actions and use them to draw and select.

On move → Draw and select

When Wrangle detects movement, it goes through the process of drawing and selecting.

Drawing with HTML5 Canvas happens in JavaScript. The drawing methods are inside a context object, which we can grab from the canvas element itself:

// This code runs when Wrangle is initialized
this.$container  = elem;
this.canvas      = this.$container.find('canvas')[0];

// We're just drawing flat lines, so we ask for a 2D context
this.draw        = this.canvas.getContext('2d');

To create the look of one long line being drawn, Wrangle draws little lines every time the user moves their mouse or finger. The plugin always has the coordinates of the last draw stored so it knows where to start when it has to draw a new line. Here's what that function looks like:

Wrangle.prototype.drawLine = function(from, to, context) {
  // You, the developer, can specify the size and color of the line
  context.strokeStyle = this.settings.lineColor;
  context.lineWidth   = this.settings.lineWidth;

  // These settings give the lines a smooth appearance
  context.lineJoin    = 'round';
  context.lineCap     = 'round';

  // This instructs the canvas to begin a path
  context.beginPath();
  // The line starts at these coordinates...
  context.moveTo(from.x, from.y);
  // ...and it ends at these coordinates
  context.lineTo(to.x, to.y);
  // We're done drawing the path, so now we close it
  context.closePath();
  // However, the line won't actually be visible until we stroke it with the given color and stroke size
  context.stroke();
}

After drawing the line, Wrangle checks the given coordinates to see if they overlap with any unselected items. The function below takes an array of bounding boxes, and an array of pointer coordinates, and iterates through each to try and find an overlapping pair.

Wrangle.prototype.checkCollision = function(coords, prev) {
  // The defenition of "this" changes inside $.each, so let's store a reference to it here
  var self = this;

  // Iterate through each item's bounding box
  $.each(self.itemBoxes, function(index) {
    // If this === window, then we're inside an empty part of the array
    if (this === window) return;

    // "this" refers to the item's bounding box
    var rect = this;

    // Now that we've got a bounding box, iterate through every coordinate we have
    $.each(coords, function(i) {

      // If the two intersect...
      if (rect.intersects(this.x, this.y)) {
        var elem = self.$items.eq(index);
        // ...select it!
        self.select(elem, index);
        // The select method will only add an item that isn't already selected
      }
    });
  });
}

Collision checking requires us to check every coordinate pair against every (unselected) item. So if your interface has ten unselected items and two fingers drawing on the screen, .checkCollision() will run 20 comparisons to try and find a match, every time a finger is moved.

Startup playground

Log in with your ZURB ID

Log in with Google

Don't have a ZURB ID? Sign up

Forgot password?
×

Sign up for a ZURB ID

Sign up with Google

Already have a ZURB ID? Log in

×