Apr 12

pseudocode + diagrams = ui interactions on a silver platter

I mentioned in a previous post how I’d been working on the drag and drop module of thud for about 2 weeks. It was definitely challenging, though now that I’ve done it, I think I could do it again in about 2 or 3 days.

Making something on a page draggable without using the new HTML5 attributes and events is the easy part. What are the tricky parts? Setting the coordinates of the drag image correctly when the drag element is in a positioned element, scrolling correctly and getting the snap to grid correct. It was a little frustrating, but I got there in the end. Thanks to pseudocode. This post is mostly to do with implementing drag functionality. However, it was pseudocode and drawing diagrams that saved me, hence the title.

I first “learnt” (about) pseudocode in high school — I use the term loosely, because I didn’t really learn much in high school, except about how to be a yes man and how everyone in the class was going to fail. I failed at both failing and being a yes man — computer studies class. I didn’t really get it then and thought it was a waste of time, never looking at it again until recently.

However, after failing to figure out some of the major errors I was encountering I needed to get back to basics and so I turned to pen and paper. I didn’t follow the exact pseudocode syntax, I don’t think you really need to. As long as you understand what is going on and can use what you write in your program. Mine is my own special recipe of pseudocode and JavaScript.

I thought I would add post about it, in case anyone else finds it useful.

Finding and setting the drag image’s current left and top positions

The logic for finding and setting the drag image’s current left and top position also includes the logic to snap the drag image to a grid, constrain it to specific dimensions, keep it a specific amount of pixels from the mouse pointer (offset) and/ or limit it to been dragged horizontally or vertically.

Glossary

I’ve put this at the top so it’s easier to see while reading the rest. Assuming you’re lucky enough to have a big monitor.

propertydescription
xThe position the mouse cursor is at, relative to the page. i.e. the event’s pageX value.
coordinatesThe bottom, height, left, offsetLeft, offsetTop, right, scrollHeight, scrollLeft, scrollTop, scrollWidth, top & width properties of an html element or the document.
orientationA value, either horizontal, vertical or null, and if set is used to limit the drag_image to only moving in either direction but not both.
drag_elementThe element we want to drag.
drag_imageThe element that will be following the cursor while the drag operation is taking place. This could be the drag_element itself, a clone of the drag_element or a completely bespoke element.
scroll_elementThe element to scroll if scrolling is allowed and the drag_image is at the boundary of the viewable_area.
viewable_areaThe coordinates of the area currently visible in the scroll_element (this will generally be the document).
snapAs in snap to grid. The number of pixels to snap the drag_image to. If this is less than 2 it will be ignored.
gridA predefined array of all the values the drag_image should snap to within the boundaries of the scroll_element. This is worked out as:
( new Array( Math.ceil( scroll_element.scrollWidth / snap ) + 2 ) ).map( function( value, index ) { return index * snap; } );
e.g. If the scrollWidth was 1024px and the snap is 16px, the grid array would be: [16, 32, 48, …, 1024, 1040]
The reason the final grid value is larger than the scroll_element’s boundary is that we want to be able to position the drag_image at the scroll_element’s boundary.
To figure out the vertical grid, we use scrollHeight instead of scrollWidth.
constrain_coordinatesthe bottom, left, right, & top coordinates to limit the drag_image’s movements within.
offsetthe number of pixels to keep the drag_image from the mouse pointer.
scrollXthe amount of pixels the viewable_area was scrolled left or right.
aggregateOffsetLeftThis is worked out as:
var aggregateOffsetLeft = 0, el = drag_element;
do { aggregateOffsetLeft += el.offsetLeft; } while ( el = el.offsetParent );
aggregateOffsetTopThis is worked out as above, except instead of offsetLeft we aggregate offsetTop.
Finding and setting the drag image’s left (x) position
IF orientation != vertical // if we are only allowing vertical movement, don't worry about calculating the horizontal
   IF viewable_area WAS SCROLLED BY AMOUNT scrollX // check to see if the scrolling function has scrolled horizontally
      viewable_area.left += scrollX // if so we need to adjust the viewable_area properties to reflect this
      viewable_area.right += scrollX

   x = x - drag_image.aggregateOffsetLeft // we don't want ot use the -= operator here as it will change the original x value

   IF x >= viewable_area.right // this makes sure the drag_image is always visible to the user dragging it
      IF viewable_area.right // 20 is the standard vertical scroll bar width
      ELSE
         x = scroll_element.scrollWidth - drag_image.width - |offset| // |offset| if you don't know is the absolute value of offset so |8| == |-8| == 8
   ELSE IF x // closestGridPosition returns the number in the grid array that x is between. 
                                                   // e.g. if snap == 16px and x is between 16 and 32 it will return 16, 
                                                   // if x is between 32 and 48 it will return 32, etc.
   IF CONSTRAIN TO COORDINATES
      IF constrain_coordinates.left && x  constrain_coordinates.right
         x = constrain_coordinates.right

   drag_image.left = x + offset

On line 5, we have the following code x = x - drag_image.aggregateOffsetLeft. This was a tricky part, it took me about 2 days to realise I was using the wrong property here.

As I mentioned before making something draggable is easy. In it’s simplest form you can set the left and top position of the drag_image to the mousemove event’s pageX and pageY values. Consider the following diagram:

diagram: dragging within the viewport

If our drag_element is a child — and not a descendant of a positioned element — of the body tag and you don’t need to scroll the page, then setting the drag_image’s left and top styles to the event’s pageX and pageY values is ok. However, consider the next diagram:

diagram: dragging within a positioned element

Basically, the element we want to drag is a child of positioned_element in the diagram. Its left and top values are relative to this element. So setting the left and top values to the event’s pageX and pageY values, would position the drag_image in the wrong location on the page, as its position is relative to positioned_element, NOT the viewport.

If we want to drag the drag_image out of the positioned_element, then we need a way of figuring out the drag_image’s left and top values, relative to positioned_element.

In the example above, positioned_element is 100px from the left and top of the document, the drag_image is currently 100px from the left and top of the positioned_element and we want to drag the drag_image to 10px from the left and top of the document, the new coordinates for the drag_image would need to be set to -90px from the left and top of positioned_element.

This is worked out as, 10px from the left of the viewport minus the positioned_element’s 100px from the left of the viewport, equals -90px.

Finding and setting the drag image’s top (y) position

I was going to put the pseudocode for this in, but it’s exactly the same as the above, only we replace the left (x) properties, with the corresponding top (y) properties:

left (x) propertytop (y) property
xy
verticalhorizontal
scrollXscrollY
viewable_area.leftviewable_area.top
viewable_area.rightviewable_area.bottom
drag_image.heightdrag_image.width
drag_image.leftdrag_image.top
drag_image.aggregateOffsetLeftdrag_image.aggregateOffsetTop
scroll_element.scrollWidthscroll_element.scrollHeight
constrain_coordinates.leftconstrain_coordinates.top
constrain_coordinates.rightconstrain_coordinates.bottom

Scrolling when the drag image is at a boundary point

The tricky part here is working the scroll sensitivity into the equation. We could adjust the viewable_area boundary of the scroll_element to incorporate the scroll_sensitivity, so that when the drag_image reaches this new boundary we can start scrolling.

However, as the viewable area will then change we will need to readjust the boundary each time we scroll. We need to create a boundary — I like to think of it as a force field — around the drag_image. The easiest way to envision this, I’ve found, is using the following diagram:

diagram: scrolling an element while the drag_image is at its boundary

Using this method, we can create a boundary — force field — around the drag_image the size of the scroll_sensitivity amount. When this drag_boundary’s bottom, left, right, top — or a combination of those — is greater — or less than, depending on the direction of the drag — the boundary of the viewable_area we can initiate scrolling in the specific direction. Figuring out the drag_boundary is defined in the glossary below, so I won’t repeat it here.

Glossary

Apart from what’s already above, we also have these properties:

propertydescription
scroll_sensitivityThe distance from the scroll_element’s boundary points to initiate scrolling.
drag_boundaryThis is an object representing the drag_image dimensions plus the scroll_sensitivity. This is worked out as:
var drag_boundary = {
   bottom : drag_image.height + drag_image.aggregateOffsetTop + scroll_sensitivity,
   left   : drag_image.aggregateOffsetLeft - scroll_sensitivity,
   right  : drag_image.width + drag_image.aggregateOffsetLeft + scroll_sensitivity,
   top    : drag_image.aggregateOffsetTop - scroll_sensitivity
};
scroll_amountThe amount in pixels to scroll by if scrolling is required.
scrollXIf scrolling is required, then this will be set to either positive scroll_amount (to scroll down/ right) or negative scroll_amount (to scroll up/ left).
Scrolling horizontally
IF orientation != vertical
   IF drag_boundary.right > viewable_area.right && drag_boundary.right  scroll_element.scrollLeft + scroll_sensitivity
      scrollX = scroll_amount
   ELSE IF drag_boundary.left = 0
      scrollX = -scroll_amount

   IF scrollX IS A NUMBER
      scroll_element.scrollLeft += scrollX
Scrolling vertically

Again, apart from what is already above, we swap the corresponding properties:

left (x) propertytop (y) property
drag_boundary.leftdrag_boundary.top
drag_boundary.rightdrag_boundary.bottom
scroll_element.scrollLeftscroll_element.scrollTop

Conclusion

Using pseudocode and diagrams, we can clearly visualise user interface interactions and this in turn will help us write clean and efficient code.

Obviously, project time constraints are an issue and a lot of development teams are pushed to get something live rather than something — of quality — that works. This type of careful planning often takes a back seat to showing working code for your next sprint so you can rush through the next chunk of work.

Having the opportunity to take time off, to focus on development with an emphasis on quality is an amazing experience. It’s going to be hard if I have to go back!


Page 1 of 1