Lightboxes are overused. They take way to long to load and consume far too much screen real estate. A far better user experience can be achieved by teaming up JavaScript with ActionScript. The end result is less than 6KB of code!

**Update: The feedback widget replaced the draggable widget. Please visit this link for a demo of the draggable one.**

Before going any further, you’ve probably noticed the dark red rectangle docked to the left side of this web page. Click on it. Notice how the widget expands out smoothly. Now for the cool part, click and drag on the blue bar once the widget has expanded. Drag the widget around the screen and notice the alpha transparency of the Flash object. Imagine using this for simple forms, help support chat windows, video conferencing and more.

Cool! Let’s see some code.

JavaScript

This code works great but I’m not a JS expert so it could probably be optimized. The expand and contract code is very simple JS. A div is re-sized on contract complete and expand begin. This allows the user to select text behind the div while it is contracted. The animation is done in Flash for two reasons: it looks smoother and we don’t need any CSS trickery to get the drop shadow to work in IE. I would love to see a full JS implementation of this for comparison but it is out of my skill set. Any JS guru’s willing to give it a try?

Alternatively, the div movement was offloaded completely to JavaScript for two reasons. First, the slight lag between communication becomes quickly apparent when attempting to communicate from Flash 30 times per second. Second, changing the size of the div to full screen giving Flash complete control caused a flicker at start and end. After moving all of the movement code to JS everything performed great!

Draggable Code:

var currentLeft = 0;
var currentTop = 0;
var dragObj = new Object();
var dragging = false;

// Browser detection code removed for simplicity. 
// See full source at the bottom of this post.

function expandWidget() {
    e = document.getElementById("expandableDiv");
    e.style.width = 525 + 'px';
}

function contractWidget() {
    e = document.getElementById("expandableDiv");
    e.style.width = 50 + 'px';
}

function onMouseWheel(event) {
    if (!event) event = window.event;

    if (dragging) {
        return cancelEvent(e);
    }
    return false;
}

function startDrag(clientX, clientY) {
    var x, y;

    dragging = true;

    e = document.getElementById("expandableDiv");
    dragObj.elNode = e;

    // Get cursor position with respect to the page.
    if (browser.isIE) {
        x = clientX + currentLeft + document.documentElement.scrollLeft + document.body.scrollLeft;
        y = clientY + currentTop + document.documentElement.scrollTop + document.body.scrollTop;
    }
    if (browser.isNS) {
        x = clientX + currentLeft + window.scrollX;
        y = clientY + currentTop + window.scrollY;
    }

    // Save starting positions of cursor and element.
    dragObj.cursorStartX = x;
    dragObj.cursorStartY = y;
    dragObj.elStartLeft = parseInt(dragObj.elNode.style.left, 10);
    dragObj.elStartTop = parseInt(dragObj.elNode.style.top, 10);

    if (isNaN(dragObj.elStartLeft)) dragObj.elStartLeft = 0;
    if (isNaN(dragObj.elStartTop)) dragObj.elStartTop = 0;

    // Capture mousemove events on the page.
    if (browser.isIE) {
        document.attachEvent("onmousemove", dragGo);
        window.event.cancelBubble = true;
        window.event.returnValue = false;
    }
    if (browser.isNS) {
        document.addEventListener("mousemove", dragGo, true);
        event.preventDefault();
    }
}

function stopDrag() {
    dragging = false;
    e = document.getElementById("expandableDiv");
    if (browser.isIE) {
        document.detachEvent("onmousemove", dragGo);
    }
    if (browser.isNS) {
        document.removeEventListener("mousemove", dragGo, true);
    }
}

function dragGo(event) {
    var x, y;

    // Get cursor position with respect to the page.
    if (browser.isIE) {
        x = window.event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
        y = window.event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
    }
    if (browser.isNS) {
        x = event.clientX + window.scrollX;
        y = event.clientY + window.scrollY;
    }

    // Move drag element by the same amount the cursor has moved.
    currentLeft = (dragObj.elStartLeft + x - dragObj.cursorStartX);
    currentTop = (dragObj.elStartTop + y - dragObj.cursorStartY);
    dragObj.elNode.style.left = (dragObj.elStartLeft + x - dragObj.cursorStartX) + "px";
    dragObj.elNode.style.top = (dragObj.elStartTop + y - dragObj.cursorStartY) + "px";

    if (browser.isIE) {
        window.event.cancelBubble = true;
        window.event.returnValue = false;
    }
    if (browser.isNS) event.preventDefault();
}

ActionScript

For the ActionScript, ExternalInterface was used to communicate with JS and TweenNano was used for the animation. One key thing to point out is the offset for where the user clicked when starting a drag. The trickiest part of this example was ensuring that JS knew exactly where the window should be as the mouse moved.

private function downHandler(e:Event):void
{
	_background.alpha = 0.5;
	if (ExternalInterface.available) {
		ExternalInterface.call("startDrag", this.mouseX + 10, this.mouseY + 10);
	}						
}

private function upHandler(e:Event):void
{
	_background.alpha = 1;
	if (ExternalInterface.available) {
		ExternalInterface.call("stopDrag");
	}						
}

private function JSExpand():void
{
	if (ExternalInterface.available) {
		ExternalInterface.call("expandWidget");
	}
}

private function JSContract():void
{
	if (ExternalInterface.available) {
		ExternalInterface.call("contractWidget");
	}			
}

Live demo can be found here.

Full source code can be found here.

Next Steps
Make sure the user can’t shrink the widget off the page and allow for re-docking or snapping back to the original position. Add alternate content that would navigate to an HTML page on click if the user has JS and AS disabled. Allow positioning to be configurable via Flash vars. This widget has many potential uses. I will be populating my widget with some content soon. How could this widget benefit your site?

Resources
http://www.brainjar.com/dhtml/drag/
http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/external/ExternalInterface.html

5 thoughts on “Expanding Draggable Widget

  1. Very cool indeed. An issue I quickly ran into was… If you click to expand and then drag and drop somewhere it works fine. But if you drag the box back to the left side of the screen (so that part of the box is not visible) and then click to contract back to its original form, the box is now off screen.

    Just though I’d point this out, I’m sure there is a hook you could code for the box to default to its original placement on the side of the page if its x position is less than a given amount.

  2. Nice Ken! Thanks for putting that together. I really should look more into the JQuery library. I write all my JS from scratch which takes forever. I can see some pros and cons to both approaches but the JS approach is definitely looking good.

Comments are closed.