Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created March 25, 2014 12:10
Show Gist options
  • Save bennadel/9760574 to your computer and use it in GitHub Desktop.
Save bennadel/9760574 to your computer and use it in GitHub Desktop.
Treating User Interface (UI) Widgets Like Finite State Machines
<!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