Build your own WebMockup Tool using jQuery

This article is to show the capability of Javascript and jQuery Framework’s rather than as reference implementation to create a web mockup tool.  Infact this is not the best architecture to build a mockup tool in javascript.

Sample demo is hosted at  http://linkwithweb.appspot.com/DynamicForm.html   ( It only Works in Firefox and Chrome) 🙂

Concepts like  Drag/Drop , Resizable , Context Menu , Container’s, Grouping,

Cornering element view’s etc…. are being explained through this article.

I have used following javascript libraries to create this

  1. jquery
  2. jqueryui
  3. jquery.corner.js
  4. jquery.contextmenu.js

To create any Mockup tool we first to define few things that are required to build it

  1. Creation of Draggable Objects
  2. Creation of Droppable Area where object’s can be dropped.
  3. Layout of objects on droppable area(Dashboard)
  4. Deleting/Changing properties of objects/elements in the dashboard





Because i ‘m going to build a HTML mockup tool , i’ll define few html element’s as my draggable objects which i’m going to play with. Apart from this as i’m planning to make all objects/elements in my mockup tool as draggable one’s i’ll define a Dragholder which is used to hold my draggable objects. I have defined a class with name “drag” that defines all draggable objects

TextBox

<div id="drag1" class="drag">
     <input type="text" value="test" style="width: 80%; height: 80%;"></input>
</div>

Form

<form id="drag2" class="drag containerdiv">

	<div style=""
		class="context-menu context-menu-theme-vista context-menu-example-trigger menu-vista">
		<div class="context-menu-item">
			<div class="context-menu-item-inner">Form Container</div>
		</div>
	</div></form>

Button

<div id="drag3" class="drag">
	<input type="button" value="Drag Me" />
</div>

TextArea

<div id="drag4" class="drag">
	<textarea name="userInput" style="width: 80%; height: 80%;"></textarea>
</div>

SelectBox

<div id="drag4" class="drag">
	<select>
		<option>Small</option>
		<option selected="true">Medium</option>
		<option>Large</option>
		<option>X-Large</option>
	</select>
</div>

All the above elements have been defined with css class “drag” which we use to query latter.

Now lets define a container where all the above draggable elements will be dragged and played around

<div id="frame">
</div>

the above div element with id “frame” is used as container in our mockup tool.

Now as we have defined all elements in our HTML playground , let me explain the logic i’m going to use to create this.  Before going into that lemme explain that elements like form can have other elements so lets also make it droppable once it enters our playground

  1. Get all objects with class “drag”
  2. Add control key listener which add’s class “grouped” to elements if clicked by holding cotrol key ( Kind of selecting a element)
  3. Make all of then resizable.
  4. Add context menu for all elements
  5. Enable drag on them and assign them some id’s
  6. If element contain’s class “grouped” , moved all other elements with class “grouped” same width and height as our draggable element
  7. If elements also have class name “containerdiv” then they droppable apart from draggable, so enable droppable feature on them
Here is how sample menu is created
var menu = [
	{
		'Show an alert' : function() {
			alert('This is an alert!');
		}
	},
	{
		'Turn me red (try it)' : function() {
			this.style.backgroundColor = 'red';
		}
	},
	{
		'<b>HTML</b> <i>can be</i> <u>used in</u> <input type="text" value="test"/>' : function(
				menuItem, cmenu, e) {
			$t = $(e.target);
			return false;
		}
	},
	$.contextMenu.separator,
	{
		'Just above me was a separator!' : function() {
		}
	},
	{
		'Option with icon (for themes that support it)' : {
			onclick : function() {
			},
			icon : 'i_save.gif'
		}
	},
	{
		'Disabled option' : {
			onclick : function() {
			},
			disabled : true
		}
	},
	{
		'<div><div style="float:left;">Choose a color (don\'t close):</div><div class="swatch" style="background-color:red"></div><div class="swatch" style="background-color:green"></div><div class="swatch" style="background-color:blue"></div><div class="swatch" style="background-color:yellow"></div><div class="swatch" style="background-color:black"></div></div><br>' : function(
				menuItem, cmenu, e) {
			$t = $(e.target);
			if ($t.is('.swatch')) {
				this.style.backgroundColor = $t.css('backgroundColor');
				$t.parent().find('.swatch').removeClass('swatch-selected');
				$t.addClass('swatch-selected');
			}
			return false;
		}
	},
	{
		'Use fadeOut for hide transition' : function(menuItem, cmenu) {
			cmenu.hideTransition = 'fadeOut';
			cmenu.hideSpeed = 'fast';
		}
	},
	{
		'Use slideUp for hide transition (quirky in IE)' : function(
				menuItem, cmenu) {
			cmenu.hideTransition = 'slideUp';
		}
	},
	{
		'Use normal hide transition' : function(menuItem, cmenu) {
			cmenu.hideTransition = 'hide';
		}
	},
	{
		'Increments each time menu is displayed using beforeShow hook: #<span class="option-count">1</span>' : function() {
		}
	}, {
		'Custom menu item class and hover class for this item only' : {
			onclick : function() {
			},
			className : 'context-menu-item-custom',
			hoverClassName : 'context-menu-item-custom-hover'
		}
	} ];

Below is sample code on how we implement above functionality using jquery

/**
 * Called once document is loaded
 */
$(document).ready(function() {
	$('a[rel*=facebox]').facebox({
		div : '#frame',
		loading_image : 'loading.gif',
		close_image : 'closelabel.gif'
	});

	// Counter
	counter = 0;

	// Make element draggable
	// $(".dragged").resizable().draggable();
	enableDrag("frame", ".drag", "false");

	// $(".drag").corner();

	// Make element droppable
	$("#frame").droppable({
		drop : function(ev, ui) {
			if (ui.helper.attr('id').search(/drag[0-9]/) != -1) {
				counter++;
				var element = $(ui.draggable).clone();

				// alert(element);
				element.addClass("tempclass");

				$(this).append(element);

				$(".tempclass").attr("id", "clonediv" + counter);
				$("#clonediv" + counter).removeClass("tempclass");

				// Get the dynamically item id
				draggedNumber = ui.helper.attr('id').search(/drag([0-9])/)
				// itemDragged = "dragged" + RegExp.$1
				// //console.log(itemDragged)
				itemDragged = "dragged";

				$("#clonediv" + counter).addClass(itemDragged);

				enableDrag("parent", "#clonediv" + counter, "true");

			}
		}
	});
});

Code for function enableDrag which is main funciton in our project

/**
 * This function enables drag on a object and contains its drag in the passed
 * container
 *
 * @param dragContainment
 * @param draggableClass
 * @param resizable
 */
function enableDrag(dragContainment, draggableClass, resizable) {
	// Make element draggable and resizable
	$(draggableClass).click(function(event) {
		if (event.ctrlKey) {
			$(this).toggleClass('grouped');
		}
	});
	/**
	 * Add Context Menu
	 */
	$(draggableClass).find(".menu-vista").contextMenu(menu, {
		theme : 'vista',
		beforeShow : beforeShowFunc
	});
	$(draggableClass).contextMenu(draggableObjectMenu, {
		theme : 'vista'
	});

	var element;

	/**
	 * Make the element resizable
	 */
	if (resizable == "true") {
		element = $(draggableClass).corner().resizable();
	} else {
		element = $(draggableClass).corner();
	}

	/**
	 * If element is containerdiv then make it droppable
	 */
	if (element.hasClass("containerdiv")) {

		// Make element droppable
		element.droppable({
			greedy : true,
			out : function(event, ui) {

				var element = $(ui.draggable);
				if (!element.hasClass("containerdiv")) {
					$(this).parent().append(element);
				}
			},
			drop : function(ev, ui) {
				if (ui.helper.attr('id').search(/drag[0-9]/) != -1) {
					var element = $(ui.draggable);
					counter++;

					element = $(ui.draggable).clone();
					// alert(element);
					element.addClass("tempclass");

					$(this).append(element);

					$(".tempclass").attr("id", "clonediv" + counter);
					$("#clonediv" + counter).removeClass("tempclass");

					// Get the dynamically item id
					draggedNumber = ui.helper.attr('id').search(/drag([0-9])/)
					// itemDragged = "dragged" + RegExp.$1
					// //console.log(itemDragged)
					itemDragged = "dragged";

					$("#clonediv" + counter).addClass(itemDragged);

					enableDrag("frame", "#clonediv" + counter, "true");

				} else {
					var element = $(ui.draggable);
					$(this).append(element);
				}
			}
		});

	}

	/**
	 * Now make element draggable
	 */
	element.draggable({
		helper : 'clone',
		containment : dragContainment,

		start : function(event, ui) {
			posTopArray = [];
			posLeftArray = [];
			thisElem = $(this);

			if ($(this).hasClass("grouped")) { // Loop through each element and
				// store beginning start and
				// left positions

				$(".grouped").each(function(i) {
					thiscsstop = $(this).css('top');
					if (thiscsstop == 'auto')
						thiscsstop = 0; // For IE

					thiscssleft = $(this).css('left');
					if (thiscssleft == 'auto')
						thiscssleft = 0; // For IE

					posTopArray[i] = parseInt(thiscsstop);
					posLeftArray[i] = parseInt(thiscssleft);
				});
			}

			begintop = $(this).offset().top; // Dragged element top position
			beginleft = $(this).offset().left; // Dragged element left position
		},

		drag : function(event, ui) {
			var topdiff = $(this).offset().top - begintop; // Current distance
			// dragged element
			// has traveled
			// vertically
			var leftdiff = $(this).offset().left - beginleft; // Current
			// distance
			// dragged
			// element has
			// traveled
			// horizontally

			var draggingId = $(this).attr("id");
			if ($(this).hasClass("grouped")) {
				// //console.log("Initial pos "+begintop +"
				// Left----"+beginleft);
				// //console.log("Diff Value is "+topdiff +"
				// Left----"+leftdiff);
				$(".grouped").each(function(i) {
					if (draggingId === $(this).attr("id")) {
						topdiff = $(this).offset().top - begintop; // Current
						leftdiff = $(this).offset().left - beginleft; // Current
					}
				});

				$(".grouped").each(function(i) {
					// //console.log("Pres pos for "+$(this).attr('id')+" is
					// "+posTopArray[i] +" Left----"+posLeftArray[i]);
					if (draggingId !== $(this).attr("id")) {
						$(this).css('top', posTopArray[i] + topdiff); // Move
						$(this).css('left', posLeftArray[i] + leftdiff); // Move
					}
				});

			}
		},

		// When first dragged
		stop : function(ev, ui) {
			var pos = $(ui.helper).offset();
			objName = "#clonediv" + counter
			var draggingId = $(this).attr("id");

			var leftValue = pos.left;
			var topValue = pos.top;

			/**
			 * The below if loop is written to fix the relative and abosiulte
			 * positioning issue. When any draggable object is dropped into a
			 * droppable container which resides in another container then the
			 * droppable with more z-index becomes relative positining for its
			 * sub elements
			 */
			if ($(this).parent().hasClass("containerdiv")) {
				// console.log("Left Pos "+leftValue);
				// console.log("Top Pos "+topValue);
				// console.log("$(this).parent().attr('left')
				// "+$(this).parent().offset().left);
				// console.log("$(this).parent().offset().top
				// "+$(this).parent().offset().top);
				leftValue = pos.left - $(this).parent().offset().left;
				topValue = pos.top - $(this).parent().offset().top;
			}

			if ($(this).hasClass("drag")) {
				$(objName).css({
					"left" : leftValue,
					"top" : topValue
				});
				$(objName).removeClass("drag");

			} else {
				objName = draggingId;
				thisElem.css({
					"left" : leftValue,
					"top" : topValue
				});
			}

			// console.log("Left Pos "+leftValue);
			// console.log("Top Pos "+topValue);

			var topdiff = $(this).offset().top - begintop; // Current distance
			// dragged element
			// has traveled
			// vertically
			var leftdiff = $(this).offset().left - beginleft; // Current
			// distance
			// dragged
			// element has
			// traveled
			// horizontally

			// This logic for Group Elements

			if ($(this).hasClass("grouped")) {
				// //console.log("Initial pos "+begintop +"
				// Left----"+beginleft);
				// //console.log("Diff Value is "+topdiff +"
				// Left----"+leftdiff);
				$(".grouped").each(function(i) {
					if (draggingId === $(this).attr("id")) {
						topdiff = $(this).offset().top - begintop; // Current
						// distance
						// dragged
						// element
						// has
						// traveled
						// vertically
						leftdiff = $(this).offset().left - beginleft; // Current
						// distance
						// dragged
						// element
						// has
						// traveled
						// horizontally
					}
				});

				$(".grouped").each(function(i) {
					// //console.log("Pres pos for "+$(this).attr('id')+" is
					// "+posTopArray[i] +" Left----"+posLeftArray[i]);
					if (draggingId !== $(this).attr("id")) {
						$(this).css('top', posTopArray[i] + topdiff); // Move
						// element veritically - current css top + distance
						// dragged element has travelled vertically
						$(this).css('left', posLeftArray[i] + leftdiff); // Move
						// element horizontally-current css left + distance
						// dragged element has travelled horizontally
					}
				});

			}

		}
	});

}

Below is code which generate HTML Code from the design we made

/**
 * On Spit html button clicked alert the HTML in the frame that gets generated
 */
function alertHTML() {
	var htmlClone = $("#frame").clone();
	alert("<html><head></head><body>" + htmlClone.html() + "</body></html>");
}

Code has been checked in to follow svn url https://linkwithweb.googlecode.com/svn/trunk/WebMockup

Njoy playing with javascript and jQuery

One thought on “Build your own WebMockup Tool using jQuery

Leave a comment