Making Elements Draggable - Building Our UI - Developing Web Components (2015)

Developing Web Components (2015)

Part II. Building Our UI

Chapter 7. Making Elements Draggable

Frequently in web applications you will see elements that are draggable—slider handles, dialogs, column resizing handles, etc. If you have ever wondered exactly how this is accomplished, or needed to implement this behavior, then this is the chapter for you! The first step in the process is learning about mouse events.

NOTE

This and subsequent chapters will utilize jQuery events, which, for the most part, are wrappers to native JavaScript events that are normalized for cross-browser consistency.

Mouse Events

jQuery supports eleven different mouse events. However, making an element draggable only requires understanding three of these events: $.mousemove, $.mousedown, and $.mouseup.

$.mousemove

Tracking mouse movements is fundamental to making an element draggable because the mouse position will be used to coordinate the movement of the element being dragged. The native JavaScript mousemove event provides the mouse position data, such as the values of clientX andclientY, which are properties of the native event object. However, support for these properties varies between browsers. Fortunately, jQuery normalizes these properties as part of the jQuery event object. We bind the $.mousemove event listener as follows:

$('body').on('mousemove', function (e) {

// log the normalized mouse coordinates

console.log('pageX: ' + e.pageX + ' ' + 'pageY: ' + e.pageY);

});

TIP

mousemove is useful for tracking user mouse movements, which can be used to gather analytics. These analytics can be used to improve the functionality of a UI by helping users to get to information and complete frequent actions faster.

$.mousedown

Determining when a user presses the mouse button (mousedown event) is required for dragging elements because this is the event that marks the moment when the mousemove event translates into moving the element marked to be dragged. We bind the $.mousedown event listener as follows:

$('body').on('mousedown', function (e) {

console.log('Yo. You just pressed the mouse down.');

});

$.mouseup

Knowing when to stop moving an element is the final step during a drag sequence. As soon as the mouseup event is fired, the last mouse position is used to determine the final location of the element being dragged. We bind this event listener as follows:

$('body').on('mouseup', function (e) {

console.log('Dude. You just let the mouse go up.');

});

Mouse Events Best Practices

In addition to knowing the required mouse events for making an element draggable, it is equally important to know how to optimize the usage of these events. I do not advocate premature optimization, but these are not strictly optimizations: they are common patterns and best practices that have been vetted by UI widget libraries such as jQuery UI. jQuery advocates their usage as well.

1. Bind $.mousemove on $.mousedown

mousemove is triggered every time the mouse is moved, even if it is only by a pixel. This means hundreds of events can easily be triggered in a matter of seconds if a user is interacting with a UI that requires frequent mouse movements. To ensure that mousemove events are only processed when necessary, it is best to bind the $.mousemove handler on $.mousedown:

$('.some-element').on('mousedown', function (e) {

$('.some-element').on('mousemove', function (e) {

// this is where the dragging magic will happen

});

});

2. Unbind $.mousemove on $.mouseup

Just as you should only bind the $.mousemove handler on $.mousedown, you should unbind the $.mousemove handler on $.mouseup—for every action. This will prevent any unnecessary mousemove events from being processed after the dragging of the element has been completed:

$('body').on('mouseup', function (e) {

// stop processing mousemove events

$('.some-element').off('mousemove');

});

3. Bind $.mouseup to the <body>

You may have noticed in the previous code block that the mouseup event was bound to the <body>. This is because, according to the API documentation, in some cases “the mouseup event might be sent to a different HTML element than the mousemove event was.” For this reason alone, it is best to always bind the $.mouseup handler to the <body> tag.

4. Namespace All Event Bindings

It is always a good practice to namespace all event bindings. This ensures that events that are bound by a widget do not conflict with other event handlers, which makes cleanup easier as you don’t have to worry about destroying event handlers bound by processes outside of the widget. Since the $.mouseup event handler is bound to the <body>, the namespace is also used to filter out other mouse events that have bubbled up to the <body>:

// namespace all events

$('body').on('mouseup.draggable', function (e) {

});

Defining an API

In order to make our code for making an element draggable more reusable, it is necessary to create an object with a convenient API that can be applied to any element:

// I can move any mountain

var Shamen = (function (global, $) {

'use strict';

// instance default options

var defaults = {

dragHandle: null

};

// create draggable instance

function Shamen(el, options) {

this.options = $.extend({}, defaults, options);

var css = { cursor: (this.options.cursor || 'move') };

// check if element is a child of a document fragment (i.e.,

// a shadow DOM); this will be important later

this.isChildOfDocFragment = isChildOfDocFragment(el);

this.el = el;

this.$el = $(el);

this.$dragHandle = this.options.dragHandle ?

this.$el.find(this.options.dragHandle) : this.$el;

this.bind();

this.originalDragHandleCursor = this.$dragHandle.css('cursor');

// apply cursor css

this.$dragHandle.css(css);

}

// bind mousedown event handler

Shamen.prototype.bind = function () {

};

// clean up to prevent memory leaks

Shamen.prototype.destroy = function () {

};

return Shamen;

})(window, jQuery);

Creating a Drag Handle

In some cases you only want a specific child element of the draggable element to be the drag handle, as opposed to the entire element. For instance, in the case of a dialog it is often the title or header bar that is the drag handle.

NOTE

A drag handle is an element that responds to a drag or resize event when a mousedown event occurs, allowing a user to move or resize an ancestor node of the drag handle. In this book the ancestor element will be the widget’s el or $el property.

We can implement a drag handle as follows:

.drag-handle {

cursor: move;

}

<div class="draggable">

<div class="drag-handle"></div>

</div>

$('.draggable').on('mousedown.draggable', '.drag-handle', function (e) {

});

Making Things Move

Now that the nuts and bolts of making an element draggable have been defined, it is time to use these pieces to make something move!

$.mousedown Handler

The first step is to write the $.mousedown handler, which gets the mouse’s current position:

// bind mousedown event handler

Shamen.prototype.bind = function () {

// filter on drag handle if developer defined one

var selector = this.options.dragHandle || null;

var self = this;

// account for margins if element is position absolutely;

// code reused from Duvet.js

var parentMargins = this.$el.attr('position') === 'absolute' ?

getMargins(this.$el.parent()[0]) : { top: 0, left: 0};;

this.$el.on('mousedown.shamen', selector, function (e) {

// get the initial mouse position

var mousePos = {

x: e.pageX,

y: e.pageY

};

});

};

$.mousemove Handler

This is where the moving of an element occurs! On mousemove the $.mousemove handler uses the element dimension and position values set in the $.mousedown handler to move the draggable element pixel for pixel as mousemove events are triggered:

// bind mousedown event handler

Shamen.prototype.bind = function () {

// filter on drag handle if developer defined one

var selector = this.options.dragHandle || null;

var self = this;

// account for margins if element is position absolutely;

// code reused from Duvet.js

var parentMargins = this.$el.attr('position') === 'absolute' ?

getMargins(this.$el.parent()[0]) : { top: 0, left: 0};

this.$el.on('mousedown.shamen', selector, function (e) {

// get the initial mouse position

var mousePos = {

x: e.pageX,

y: e.pageY

};

// bind mousemove event handler

$(window).on('mousemove.shamen', function (e) {

// get the differences between the mousedown position and the

// positions from the mousemove events

var xDiff = e.pageX - mousePos.x;

var yDiff = e.pageY - mousePos.y;

// get the draggable el's current position relative to the document

var elPos = {

left: el.offsetLeft,

top: el.offsetTop

};

// apply the mouse position differences to the el's position

self.$el.css({

top: elPos.top + yDiff,

left: elPos.left + xDiff

});

// store the current mouse position

// to diff with the next mousemove positions

mousePos = {

x: e.pageX,

y: e.pageY

};

});

});

};

$.mouseup Handler

When the mouseup event is fired the $.mousemove handler is unbound, and the draggable element is in its final resting place (at least, until dragging is initiated again by another mousedown event). The $.mouseup event handler looks like this:

// bind mousedown event handler

Shamen.prototype.bind = function () {

// unbind mousemove handler on mouseup

$('body').on('mouseup.shamen', function (e) {

$(window).off('mousemove.shamen');

});

};

Now that have seen the $.mouseup handler binding, we need to fill in the details:

// bind mousedown event handler

Shamen.prototype.bind = function () {

// filter on drag handle if developer defined one

var selector = this.options.dragHandle || null;

var self = this;

// account for margins if element is position absolutely;

// code reused from Duvet.js

var parentMargins = this.$el.attr('position') === 'absolute' ?

getMargins(this.$el.parent()[0]) : { top: 0, left: 0};

// unbind mousemove handler on mouseup

$('body').on('mouseup.shamen', function (e) {

$(window).off('mousemove.shamen');

});

this.$el.on('mousedown.shamen', selector, function (e) {

// get the initial mouse position

var mousePos = {

x: e.pageX,

y: e.pageY

};

// bind mousemove event handler

$(window).on('mousemove.shamen', function (e) {

// get the differences between the mousedown position and the

// positions from the mousemove events

var xDiff = e.pageX - mousePos.x;

var yDiff = e.pageY - mousePos.y;

// get the draggable el's current position relative to the document

var elPos = self.$el.offset();

// apply the mouse position differences to the el's position

self.$el.css({

top: elPos.top + yDiff,

left: elPos.left + xDiff,

position: 'absolute'

});

// store the current mouse position

// to diff with the next mousemove positions

mousePos = {

x: e.pageX,

y: e.pageY

};

});

});

};

Destroying a Draggable Instance

Like with the overlay widget and any other widget, it is always good practice to clean up. This helps to prevent memory leaks and reduces the possibility of a destroyed widget having an impact on the application (e.g., through a lingering node property or event handler):

// clean up to prevent memory leaks

Shamen.prototype.destroy = function () {

// unbind mousedown

this.$el.off('mousedown.shamen');

// revert cursor for drag handle

this.$dragHandle.css({ cursor: this.originalDragHandleCursor });

// null out jQuery object, element references

this.el = null;

this.$el = null;

this.$dragHandle = null;

// revert options to defaults

this.options = defaults;

};

Making the Dialog Widget Draggable

In some cases you may want a dialog to be draggable (for example, so that a user can see content that is obfuscated by the dialog). However, this may not always be the case, so making the dialog draggable will be optional.

We can use the following code to make our dialog widget draggable:

function Dialog (options) {

// optionally clone dialog $el

options.$el = options.clone ? $(options.$el).clone() :

$(options.$el);

// append to body

if (options.appendToEl) {

$(options.appendToEl).append(options.$el);

}

Voltron.call(this, options);

// create a draggable instance

if (options.draggable) {

this.shamen = new Shamen(this.$el[0], {

// dialog header is the drag handle

dragHandle: '#title'

});

}

// create overlay instance

this.overlay = new Duvet(this.$el[0], {

fixed: options.draggable ? false : true

});

return this;

}

Dialog.prototype.destroy = function () {

// clean up overlay

this.overlay.destroy();

// clean up draggable

if (this.shamen) {

this.shamen.destroy();

}

// call superclass

Voltron.prototype.destroy.call(this);

};

Summary

In this chapter we demystified how elements are moved along the x- and y-axes by a user via a mouse. This demystification involved understanding the relevant mouse events: mousedown, mousemove, and mouseup. We covered best practices for the different events, then used this knowledge to create a small library that could be used to make any element draggable. We then integrated this library into the dialog widget, so you know how to create a draggable dialog instance when a use case requires that the dialog be movable.

This was just one possible use case and implementation, but it highlighted common patterns that are useful in general when handling different mouse events. This knowledge is useful for other use cases too, such as creating drop-down menus, and we will leverage it in the next chapter to make elements resizable.