Dienstag, April 25, 2006

Moving HTML objects using drag and drop around, CSS and JavaScript

If you want to enable a custom page layout for the user or a drag & drop functionality in your solution you need a mechanism that enables moving html objects around. Here is a cross browser compatible solution that enables moving of HTML objects.

To make it easy to attach a moving functionality to a object I use the lightweight JavaScript Behaviour mechanism that is described in the AJAX eBook found here: http://www.mathertel.de/AJAX/AJAXeBook.aspx. Especially the cross browser event handling mechanism is used here.

Attaching the mouse events

Three events must be captured to drag objects around:

onmousedown

This event starts the moving scenario and you need to attach a method to this event.

It is sometimes necessary not to move the object that got this event but to identify a specific parent object that also contains other content elements. In this demo sample the title area of a web part is used to drag the whole part around. In the method that is attached to this event the parentNode references are searched until an html object is found that is marked with the className "VEPart".

Then current mouse offset to the left upper corner of the moving object is calculated. This is because you will not start dragging by using a exactly known single point of the object.

Now that we know that a specific object should be dragged around the 2 other events must be monitored and 2 other methods are attached to them.

onmousemove

This event will be thrown multiple times and we will move the object arround by using the new mouse coordinates given each time the event gets fired.

onmouseup

This event will be thrown at the end when the user wants to place the object at the new position by releasing the mouse button.

The first event can be caught on specific objects that enable the moving. I use the CSS class "VEMover" to mark these elements and attach a move cursor. The object that is moves is marked by using the CSS class "VEPart" that contains the VEMover.

The 2 other events should be caught on the document level because they will not be thrown always on the object that initiates the moving especially when the mouse pointer is moved very fast.

Because we need a reference to the object that is moved around the onmousedown event also saves a reference to the object by using properties of the MoverBehaviour element like a global variable so we can find the object again when onmousemove and onmouseup events are caught.

A simple moveable object

<div class="VEPart" style="width:180px;height:90px">
  <div class="VEMover">::: move me</div>
  I can be moved.
</div>

The JavaScript implementation

To make the implementation easier I use my JavaScript Control Library that enables writing compatible behaviours for HTML objects. All we need here is to include the 9 kByte jcl.js file and attach the behavior to the VEMover object.

The MoverBehaviour implements the 3 event handlers:

var MoverBehaviour = {
  mo: null, // reference to the movable obj,
  x: 0, y: 0,
  
  // ----- Events -----
  onmousedown: function (evt) {
    evt = evt || window.event;
    var src = jcl.FindBehaviourElement(evt.srcElement, MoverBehaviour);
    src.MoveStart(evt);
  }, // onmousedown


  // track mouse moves. This handler will be attached to the document level !
  _onmousemove: function (evt) {
    evt = evt || window.event;
    MoverBehaviour.MoveIt(evt);
  }, // onmousemove


  // track mouse button up. This handler will be attached to the document level !
  _onmouseup: function (evt) {
    evt = evt || window.event;
    MoverBehaviour.MoveEnd(evt);
  }, // onmouseup


  // ----- Methods -----
  MoveStart: function (evt) {
    // find the moving part (position:absolute or class="VEPart")
    var mo = this;
    while ((mo != null) && (mo.className != "VEPart"))
      mo = mo.parentNode;

    if (mo == null)
      return; // don't move
    MoverBehaviour.mo = mo;
      
    // calculate mousepointer-object distance
    mo.x = mo.y = 0;
    obj = mo;
    while (obj != null) {
      mo.x += obj.offsetLeft;
      mo.y += obj.offsetTop;
      obj = obj.offsetParent;
    } // while
    mo.x = evt.clientX - mo.x;
    mo.y = evt.clientY - mo.y;

    // make the moving object globally evailable when mouse is leaving this object.
    jcl.AttachEvent(document, "onmousemove", this._onmousemove);
    jcl.AttachEvent(document, "onmouseup", this._onmouseup);
  }, // MoveStart
  

  MoveIt: function (evt) {
    var mo = MoverBehaviour.mo;
    if (mo != null) {
      var p = (evt.clientX - mo.x) + "px";
      if (p != mo.style.left) mo.style.left = p;
      p = (evt.clientY - mo.y) + "px";
      if (p != mo.style.top) mo.style.top = p;
    } // if
    // cancel selecting anything
    evt.cancelBubble = true;
    evt.returnValue = false;
  }, // MoveIt
  

  MoveEnd: function () {
    var mo = MoverBehaviour.mo;
    if (mo != null) {
      MoverBehaviour.mo = null;
      jcl.DetachEvent(document, "onmousemove", this._onmousemove);
      jcl.DetachEvent(document, "onmouseup", this._onmouseup);
    } // if
  } // MoveEnd
    
} // MoverBehaviour

jcl.LoadBehaviour("moveme", MoverBehaviour);

The sample web page is available at http://www.mathertel.de/AJAXEngine/S04_VisualEffects/MoverDemo.aspx and a brief description of the JavaScript control library is available on web site in the eBook about my AJAX engine in the chapter "Building AJAX Controls" at: http://www.mathertel.de/AJAX/AJAXeBook.aspx.

The MoverBehaviour is also available as a separate include file if you want to follow my advice of reusing behaviours by separating them into a JavaScript include file. I will have a typical WebPart sample soon and will use the mover functionality there again.

1 Kommentar:

Jason Kolb hat gesagt…

Just wanted to say that this is some fantastic stuff you're posting, please keep it coming!! It's hard to find good original code that is really innovative, but you have it :)

Also, do you have any plans to extend your AJAX/SOAP framework to support WS-Trust and/or WS-Security?