Created
March 25, 2014 12:10
-
-
Save bennadel/9760574 to your computer and use it in GitHub Desktop.
Treating User Interface (UI) Widgets Like Finite State Machines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Treating UI Widgets Like State Machines</title> | |
<style type="text/css"> | |
/* DEFAULT state for MENU. */ | |
div.menu { | |
height: 30px ; | |
position: relative ; | |
width: 200px ; | |
} | |
div.menu > a.header { | |
background-color: #F0F0F0 ; | |
border: 1px solid #999999 ; | |
border-radius: 5px 5px 5px 5px ; | |
color: #333333 ; | |
display: block ; | |
height: 30px ; | |
line-height: 30px ; | |
padding: 0px 10px 0px 10px ; | |
width: 180px ; | |
} | |
div.menu > ol.items { | |
border: 1px solid #999999 ; | |
border-width: 0px 1px 1px 1px ; | |
border-radius: 0px 0px 5px 5px ; | |
display: none ; | |
left: 0px ; | |
list-style-type: none ; | |
margin: 0px 0px 0px 0px ; | |
padding: 0px 0px 0px 0px ; | |
position: absolute ; | |
top: 30px ; | |
width: 200px ; | |
} | |
div.menu li.item { | |
border-top: 1px solid #999999 ; | |
cursor: pointer ; | |
height: 30px ; | |
line-height: 30px ; | |
margin: 0px 0px 0px 0px ; | |
padding: 0px 10px 0px 10px ; | |
} | |
/* HOVER state for MENU. */ | |
div.menuInHover {} | |
div.menuInHover > a.header { | |
background-color: #EAEAEA ; | |
border-color: #333333 ; | |
} | |
/* ACTIVE state for MENU. */ | |
div.menuInActive {} | |
div.menuInActive > a.header { | |
border-bottom-width: 0px ; | |
border-radius: 5px 5px 0px 0px ; | |
} | |
div.menuInActive > ol.items { | |
display: block ; | |
} | |
div.menuInActive li.item:hover { | |
background-color: #F0F0F0 ; | |
} | |
</style> | |
</head> | |
<body> | |
<h1> | |
Treating UI Widgets Like State Machines | |
</h1> | |
<!-- BEGIN: Menu Widget. --> | |
<div class="menu"> | |
<a href="#" class="header"> | |
My Awesome Friends | |
</a> | |
<ol class="items"> | |
<li class="item"> | |
Anna Banana | |
</li> | |
<li class="item"> | |
Joanna | |
</li> | |
<li class="item"> | |
Tricia | |
</li> | |
</ol> | |
</div> | |
<!-- END: Menu Widget. --> | |
<!-- Include JavaScript library. --> | |
<script type="text/javascript" src="./jquery-1.6.1.js"></script> | |
<script type="text/javascript"> | |
// Create a sandbox for our menu widget controller. | |
(function( $, menu ){ | |
// Cache DOM references for later use. | |
var dom = {}; | |
dom.stage = $( document ); | |
dom.menu = menu; | |
dom.header = dom.menu.find( "> a.header" ); | |
dom.items = dom.menu.find( "> ol.items" ); | |
// This is the current state of the widget. Once the | |
// states are defined, this will be further set. | |
var currentState = null; | |
// I fascilitate the transition from the current to | |
// the target state. | |
var gotoState = function( newState ){ | |
// Check to see if the current state is available | |
// and has a teardown method: | |
if ( | |
currentState && | |
currentState.teardown | |
){ | |
// Teardown the old state. | |
currentState.teardown(); | |
} | |
// Check to see if the new state has a setup method. | |
if (newState.setup){ | |
// Setup the new state. | |
newState.setup() | |
} | |
// Store the new state. | |
currentState = newState; | |
}; | |
// Define the states for this widget. Each state is going | |
// to have a setup and teardown state. | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
var inDefault = { | |
// I am the description of the state (mostly for | |
// debugging and documentation). | |
description: "I am the state in which only the menu header appears.", | |
// I am a short-hand GOTO function. This method can | |
// be passed off to event handlers without scoping | |
// problems. | |
gotoState: function(){ | |
// Put widget into this state. | |
gotoState( inDefault ); | |
}, | |
// I setup the current state. | |
setup: function(){ | |
// Add a mouse-enter event for the menu. When the | |
// user mouses over the header, we need to put | |
// the menu into the Hover state. | |
dom.menu.mouseenter( inHover.gotoState ); | |
}, | |
// I teardown the current state. | |
teardown: function(){ | |
// Remove the mouse-enter event. | |
dom.menu.unbind( "mouseenter" ); | |
} | |
}; | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
var inHover = { | |
// I am the description of the state (mostly for | |
// debugging and documentation). | |
description: "I am the state in which the user has moused-over the header of the menu, but the menu has now shown yet.", | |
// I am a short-hand GOTO function. This method can | |
// be passed off to event handlers without scoping | |
// problems. | |
gotoState: function(){ | |
// Put widget into this state. | |
gotoState( inHover ); | |
}, | |
// I setup the current state. | |
setup: function(){ | |
// Change the menu class. | |
dom.menu.addClass( "menuInHover" ); | |
// Add a mouse-leave event for the menu. When | |
// the user mouses out of the menu, we need to | |
// put it back into the Default state. | |
dom.menu.mouseleave( inDefault.gotoState ); | |
// Add a click handler to show the menu items. | |
dom.header.click( | |
function( event ){ | |
// Kill the default behavior - this isn't | |
// a "real" link. | |
event.preventDefault(); | |
// Goto to the active tate. | |
gotoState( inActive ); | |
} | |
); | |
}, | |
// I teardown the current state. | |
teardown: function(){ | |
// Change the menu class. | |
dom.menu.removeClass( "menuInHover" ); | |
// Remove the mouse-leave event. | |
dom.menu.unbind( "mouseleave" ); | |
// Remove the click event. | |
dom.header.unbind( "click" ); | |
} | |
}; | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
var inActive = { | |
// I am the description of the state (mostly for | |
// debugging and documentation). | |
description: "I am the state in which the user has clicked on the menu and the menu items have been shown. At this point, menu items can be clicked for fun and profit.", | |
// I am a short-hand GOTO function. This method can | |
// be passed off to event handlers without scoping | |
// problems. | |
gotoState: function(){ | |
// Put widget into this state. | |
gotoState( inActive ); | |
}, | |
// I setup the current state. | |
setup: function(){ | |
// Change the menu class. | |
dom.menu.addClass( "menuInActive" ); | |
// Now that the menu is shown, let's use a click | |
// event on the state to trigger the exiting of | |
// this state (rather than a mouseleave). | |
dom.stage.mousedown( | |
function( event ){ | |
// Get a reference to the target item. | |
var target = $( event.target ); | |
// Check to make sure that the click did | |
// not occur INSIDE the menu. If it did, | |
// then we don't want to do anything. | |
if (!target.closest( "div.menu" ).length){ | |
// Click was OUTSIDE the menu. Go | |
// back to the default state. | |
gotoState( inDefault ); | |
} | |
} | |
); | |
// Add a click handler to hide the menu items. | |
dom.header.click( | |
function( event ){ | |
// Kill the default behavior - this isn't | |
// a "real" link. | |
event.preventDefault(); | |
// Go back to the hover tate. | |
gotoState( inHover ); | |
} | |
); | |
// Delegate the click items on the menu to listen | |
// for clicks to individual items. | |
dom.items.delegate( | |
"li.item", | |
"click", | |
function( event ){ | |
// Log for proof. | |
console.log( | |
"Clicked:", | |
$.trim( $( this ).text() ) | |
); | |
} | |
); | |
}, | |
// I teardown the current state. | |
teardown: function(){ | |
// Change the menu class. | |
dom.menu.removeClass( "menuInActive" ); | |
// Remove the mouse listener from the stage. | |
dom.stage.unbind( "mousedown", inDefault.gotoState ); | |
// Remove the click handler on the header. | |
dom.header.unbind( "click" ); | |
// Undelegate the click event for menu items. | |
dom.items.undelegate( "li.item", "click" ); | |
} | |
}; | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
// ---------------------------------------------- // | |
// To start with, put the menu into the default state. | |
gotoState( inDefault ); | |
})( jQuery, jQuery( "div.menu" ) ); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment