Saturday, July 10, 2010

How to click on a drag/sort object

I've seen this question come up a few times on Stackoverflow. How can I make a draggable/sortable/resizable/selectable object clickable as well?

You can use the built in event timestamp to determine the time difference between when the mouse down and mouse up events occur. Here is a basic demo of a sortable list that contains hidden content - clicking on the header "Content #" will open the hidden below. Holding the mouse down on the header will allow you to sort the list.

HTML
This is a basic list which contains a header (h3) and a hidden div.
<ul id="sortable">
<li>
<h3>Content 1</h3>
<div class="hidden">Some Content</div>
</li>
<li>
<h3>Content 2</h3>
<div class="hidden">Some Content</div>
</li>
<li>
<h3>Content 3</h3>
<div class="hidden">Some Content</div>
</li>
<li>
<h3>Content 4</h3>
<div class="hidden">Some Content</div>
</li>
<li>
<h3>Content 5</h3>
<div class="hidden">Some Content</div>
</li>
</ul>
CSS
This css sets the styling of the list and hidden content.
/* set list width & remove bullets */
#sortable {
width: 500px;
list-style: none;
}
/* clickable header */
h3 {
font-size: 20px;
width: 200px;
background: #444;
margin: 5px;
padding-left: 5px;
}
/* header with revealed content */
h3.opened {
background: #0080c0;
}
/* hidden content */
.hidden {
display: none;
min-height: 100px;
background: #444;
color: #ddd;
padding: 5px;
}
Script
I've included a lot of inline comments in the script which I hope makes it clear as to what it does.
Updated code (9/24/2010 - Demo) - added mouse move flag:
$(document).ready(function(){
var last, diff, moved,
clickDelay = 500; // millisecond delay
$('#sortable')
// make the list sortable
.sortable()
// make h3 in the list "clickable"
.find('h3')
.bind('mousedown', function(e){
// set time on mousedown (start of click)
last = e.timeStamp;
// clear moved flag
moved = false;
})
.bind('mouseup mousemove', function(e){
    // flag showing that the mouse had been moved
    if (e.type == 'mousemove') {
     moved = true;
     // no need to continue
     return;
    }
// find time difference on mouse up (end of click)
diff = e.timeStamp - last;
    // check moved variable, to ignore click if the mouse had been moved (dragged)
    // if time difference is less than delay, then it was a click
    // if time difference is greater than the delay, then it was meant for dragging
if ( !moved &&  diff < clickDelay ) {
// toggle the opened class and the hidden content
$(this).toggleClass('opened').next().toggle();
}
});
});
Original code - Demo:
$(document).ready(function(){
var last, diff,
clickDelay = 500; // millisecond delay
$('#sortable')
// make the list sortable
.sortable()
// make h3 in the list "clickable"
.find('h3')
.bind('mousedown', function(e){
// set time on mousedown (start of click)
last = e.timeStamp;
})
.bind('mouseup', function(e){
// find time difference on mouse up (end of click)
diff = e.timeStamp - last;
// if time difference is less than delay, then it was a click
// if time difference is greater than the delay, then it was meant for dragging
if ( diff < clickDelay ) {
// toggle the opened class and the hidden content
$(this).toggleClass('opened').next().toggle();
}
});
});
  • In case you don't understand what's going on, a click is basically composed of a mousedown event and a mouseup event. The time difference between the two events is how you can determine if you have clicked or just pressed the mouse button to select text or drag an object around (like in this demo).
  • So in this script, the "last" variable contains the time stamp of the mouse down event
  • The "diff" is the time difference from the mouseup event and the last variable time.
  • I picked a default of 500 milliseconds for a mouse click delay (reference) for really slow clickers, but you can adjust it to anything you want.
  • If the time difference is less than 500 milliseconds then we can determine that the user clicked and preform the appropriate action (toggle the hidden content).
  • If the time difference is greater, then the user is probably trying to sort the list.
  • You could potentially add another action if the time difference is greater than the delay, but the script wouldn't do anything until the user released the mouse (mouseup event). But then you could bind a "mousemove" event that checks the timer against the "last" variable to find out how long the user has been clicking the mouse.
  • Update (9/24/2010): The newer script added above the original adds a mouse move flag that ignores the timer if the user moved the mouse during the click.

Friday, July 9, 2010

Visual Navigation

There are a few sites that have a navigation menu that can highlight / change when the menu's target scrolls into view (e.g. brizk design & Crush + Lovely) . I tried to generalize the script so it would work with any site and I ended making up with this jQuery plugin.


I set up a demo page here.

Download: Uncompressed | Minified | Zipped Demo | Github

I have tested this plugin in IE8, Firefox and Chrome. Please leave me feedback if you any problems with how it works or if it doesn't work in other browsers.

Setup
  • This plugin requires jQuery in order to function properly.
  • Follow the basic templates below to set this up on your site. For more details, go to the specific section.
Default Setup
  • HTML
    <div id="sidemenu">
    <ul>
    <li><a href="#home">Home</a></li>
    <li><a href="#work">Work</a></li>
    <li><a href="#blog">Blog</a></li>
    <li><a href="#projects">Projects</a></li>
    <li><a href="#about">About</a></li>
    <li><a href="#contact">Contact</a></li>
    </ul>
    </div>
    • Change the side menu id to whatever you want, just make sure you target it with the script.
    • This side menu example will work with the plugin's default settings. See the alternate example of a different layout.
  • CSS
    #sidemenu { position: fixed; top: 50px; left: 20px; background: #444; width: 120px; }
    #sidemenu ul { list-style-type: none; margin: 0; padding: 0; }
    #sidemenu li { margin: 5px; padding: 5px; width: 100px; text-align: center; }
    #sidemenu li.selected { background: #555; }
    #sidemenu a { text-decoration: none; color: #bbbbff; }
    #sidemenu a:hover { color: #fff; }
    • This CSS highly variable. Change the position, size and colors as desired.

  • Script
    $(document).ready(function(){
    $('#sidemenu').visualNav();
    });
Alternate Setup
  • HTML
    <div id="menu">
    <div class="link" title="#Home">Home</div>
    <div class="link" title="#work">Work</div>
    <div class="link" title="#blog">Blog</div>
    <div class="link" title="#projects">Projects</div>
    <div class="link" title=".about">About</div>
    <div class="link" title=".contact">Contact</div>
    </div>
    • This side menu uses divs with a title attribute. The value in this attribute can be used to target an Id or a class (which should be unique).
    • It is important to note, that this menu will not work with javascript disabled, whereas the default one will work.
  • CSS
    #menu { position: fixed; top: 50px; left: 20px; background: #444; width: 120px; }
    #menu div.link { margin: 5px; padding: 5px; width: 100px; text-align: center; }
    #menu div.selected { background: #555; }
  • Script
    $(document).ready(function(){
    $('#menu').visualNav({
    link              : 'div.link',
    targetAttr        : 'title',
    selectedAppliedTo : 'div.link'
    });
    });
Customizing / Options
This plugin has the following default options, so you will only need to include the line below in the script options if you want to change the default:
$('#sidemenu').visualNav({
link              : 'a',        // Add a link class, as necessary
targetAttr        : 'href',     // added in case you have link = "div" and attribute something like
selectedClass     : 'selected', // css class applied to menu
selectedAppliedTo : 'li',       // to only apply to the link, use "a"
topRange          : 100,        // measure from the top of the viewport to X pixels down
topMargin         : 100,        // margin above the top where the target updates the menu
bottomMargin      : 20,         // margin from the end of the page where the last menu item is used
animationTime     : 1200        // time in milliseconds
});
  • The first four options should were hopefully made clear in the examples above.
  • To modify how the menu acts, you will need to adjust the "topRange", "topMargin" and "bottomMargin" values. I made this picture to better understand what these values do for you.
  • topRange: The "topRange" is basically the area where target needs to be inside of in order for the menu to update. For example, the target (found in the link attribute) starts under the topRange. As it moves up and crosses into the topRange. The script then updates the side menu to show that the target area is in view (or inside the view port).
  • topMargin: The top edge margin (topMargin) is the area above the view port. The top edge margin is used while the page is being scrolled up - the target is above the view port and as you scroll up, the target moves down. When the target is inside the edge margin, the menu will update and point to that target.
  • bottomMargin: The bottom edge margin is the area below the view port. It is when the page is scrolled down (the contents are moving up). When the bottom of the page is inside the bottom edge margin, the menu will update with the last targeted id. This was necessary to be include in case the last section is too short and unable to reach the top range area.
  • animationTime: The animation time is the time in milliseconds that the menu will scroll to the selected section.
Known Problems / Bugs / Suggestions
  • The menu will not select (or highlight) the item above the last item if they are both very short. For example, if your browser shows three sections while at the bottom of the page. The third to last may have shown for a brief time just before the bottom of the page reached the bottom edge margin. The menu would then skip directly to the last menu item. This is one reason why the bottom margin value is kept a low number (20 pixels by default).
  • If you click on a menu item, the page contents will automatically scroll to that section and update the browser url with that target. But if you manually scroll the page using the scroll bar or mouse, the web page url will not update with the current position. This was done on purpose, because if the script changes the location, the page will jump to that target automatically. This wouldn't look good if you are quickly scrolling through the page as it would make the movement jittery.
  • To make suggestions or report any bugs please email me at wowmotty at g mail dot com.

Sunday, July 4, 2010

todaysImage v1.0

The plugin will take a group of images which have a set date range, and show an image randomly from the group depending on the current date. It is based on my original Random Image by Date script but has been significantly modified and improved! It's shiny and sparkly, but you won't be able to tell much difference on the surface.

The frame below contains a demo of this script... you can view the page directly by clicking this link so you can more easily understand what it does and test the debuging features.



Download: Uncompressed | Minified | Zipped Demo (2.5Mb - includes all images) | Github

Setup

The set up is fairly simple (I hope). The page requires three things to work:
  1. jQuery loaded (you can use Google's copy as seen in the page source).
  2. An <img> anywhere on your page.
  3. Data source consisting of a date range and image url. You can also include optional comments.
Data Format

To include an image in this script, you will need the following bits of information:
  • Date (required) - Use the format below
  • Image URL (required)
  • Comment (optional)
  1. Date format (do not include the year):

    Dates
    Description
    0Set image as a default (will only display if no date specific images are found).
    1/1 Dispaly image only on Jan 1st.
    1/1 - 1/5Dispaly image from Jan 1st to Jan 5th (spaces are okay).
    7/20 - 8/20Display image from June 20th to August 20th.
    1/3rdMonDisplay image only on the 3rd Monday of January.
    5/lastMonDisplay image only on the last Monday of May.
    11/3rdThur-11/4thThurDisplay image from the 3rd Thursday of Nov to the 4th Thursday of Nov.
    12/20-1/1stSunDisplay image from Dec 20th to the 1st Sunday of January.

    IMPORTANT NOTES:

    • The dates that cross months (7/20-8/20) will display along with date specific images (added to the randomization).
    • Use slashes "/" to separate the month and day and dashes "-" to set a range (e.g., using "1-1 to 1-5" will break the script)
    • Using the last weekday (lastMon) of the month in the date has one restriction at this time... it'll only works for 30 day months for now. I may fix this in future versions.
    • When using the text in the date (e.g. 1st, 2nd, 3rd, last) don't spell these fully out ( first, second, third) as the script is looking for the number and not the text.
    • The weekday in the date must have at least 3 letters - these are okay: (Mon or Monday); these are not okay (M or Mo).

  2. Image URL - Umm, yeah it's required.

  3. Comment (optional)

    • The image comment will be added to the image title (or not, as desired) or into any HTML tag using an ID or class name.

    • The script is set up so that the comment can include HTML elements to add styling, but be careful to not use quotes. The script does replace quotes with the HTML escape code " but this doesn't guarantee it will work once viewed in the comment div.
Data Sources
  • This plugin is set up to accept data from a jQuery selector (for data contained in HTML), an array or a JSON variable that is inline or retrieved remotely.
  • There is an example how to accomplish each method below and there is also a demo of each (included in the zipped file).
  • The examples below expect an image with the ID of "todaysImage"
  • All data should be presented to the script in this order: date, url, comment; you can change this order, but it will require additional option settings, described later.
  • There is no need to maintain the exact chronological order of the data. I tried to keep them this way in the demo to make finding dates easier.
  • All script should be wrapped in a $(document).ready function.
  1. HTML

    • The data can be stored in an HTML list. You point your jQuery selector at the list and tell it which attributes have the data.
    • Here is an example list (hidden using CSS):
      <ul id="myImageList">
      <li rel="0" title="images/main1.jpg">Hello</li>
      <li rel="0" title="images/main2.jpg">Hello</li>
      <li rel="12/31-1/5" title="images/new-year.jpg">Happy New Year!</li>
      <li rel="5/1-8/5" title="images/Summer.jpg">Summer time!</li>
      </ul>
      As you see, the list is stored in an <li> tag with the date in the rel attribute, the url in the title attribute and the comment inside the tag. This is how to set the script to gather the data correctly.
      $('#todaysImage').todaysImage({
      data       : $('#myImageList li'),
      dataObject : ['rel', 'title', 'html'] // these must be in this order: [date, url, comment]
      });
      The 'rel' and 'title' parts of the dataObject are attributes of the tag while the comments needs either a 'text' or 'html' to target its data; use 'html' as in the example, if you have included any styling in your comment.

      NOTE: As stated before the order of the dataObject is important - [ date, url, comment ]

  2. ARRAY

    • An array can be set up, as I did in my original script, to contain all of the data needed for this script.
    • Once again, the order these are placed in the array are important, here is a basic template:

      images.push (["DATES","IMAGE URL", "COMMENT"]);

    • Here is an example:
      var images = [];
      images.push(["0","images/main1.jpg","Hello"]);
      images.push(["0","images/main2.jpg","Hello"]);
      images.push(["12/31-1/5","images/new-year.jpg","Happy New Year!"]);
      images.push(["5/1-8/5","images/Summer.jpg","Summer time!"]);
      the script below will access this data. Notice the dataObject option sets the order of the image array. If you already have an array with similar data, all you need to do is set the dataObject to point to the correct index so that this order is maintained [ date, image URL, comments ] (e.g. images.push([ "comment", "other data", "dates", "url' ]), would required the dataObject to be [2, 3, 0]).
      $('#todaysImage').todaysImage({
      data        : images,
      dataObject  : [0,1,2]
      });
  3. JSON - inline

    • I use this method when I have a short list and want to save myself a server call.
    • Take a standard JSON object and just add "var json =" in front of it.
    • If you already have a JSON set up but it has different names (or even a different language) then just adjust the dataObject as needed
    • Here is a basic template:
      var imageList = {"images": [
      {
      "dates"  : "0",
      "image"  : "images/main1.jpg",
      "comment": "Hello"
      },{
      "dates"  : "0",
      "image"  : "images/main2.jpg",
      "comment": "Hello"
      },{
      "dates"  : "12/31-1/5",
      "image"  : "images/new-year.jpg",
      "comment": "Happy New Year!"
      },{
      "dates"  : "5/1-8/5",
      "image"  : "images/Summer.jpg",
      "comment": "Summer time!"
      }
      ]};
      The script below will access this data. Notice that "imageList" matches the variable and "images" matches the very first name in the JSON object (the key). The dataObject by default is [ 'dates', 'image', 'comment' ] which matches the JSON above; but if you have a JSON object that has different key names like "dateRange", "imageURL" and "imageComment", then you will need to set the dataObject appropriately to [ 'dataRange', 'imageURL', 'imageComment' ].
      $('#todaysImage').todaysImage({
      data: imageList.images
      });
  4. JSON - remote

    • Use this method to load JSON from your server. It won't work cross domain (unless you have it setup to work with JSONP)
    • This method requires your JSON to be valid, or the object won't parse properly (this includes removing all comments)
    • This JSON, as stated before, is essentially the same as the JSON inline.
      {"images": [
      {
      "dates"  : "0",
      "image"  : "images/main1.jpg",
      "comment": "Hello"
      },{
      "dates"  : "0",
      "image"  : "images/main2.jpg",
      "comment": "Hello"
      },{
      "dates"  : "12/31-1/5",
      "image"  : "images/new-year.jpg",
      "comment": "Happy New Year!"
      },{
      "dates"  : "5/1-8/5",
      "image"  : "images/Summer.jpg",
      "comment": "Summer time!"
      }
      ]};
      Access this remote JSON data as follows:
      $.getJSON( 'images.json', function(imageList){
      $('#todaysImage').todaysImage({
      data: imageList.images
      })
      })
      The JSON data in the example is from the file "images.json". Replace this with the URL pointing to your data. I used "imageList" as the JSON data to show the similarities to the JSON inline method. Of course "images" is the first element (key) in the JSON object, and the dataObject doesn't need modification as the names in the JSON object match.
Customizing
This plugin has the following default options, so you will only need to include the line below if you want to change the default:
$('#todaysImage').todaysImage({
data         : '', // dataObject contains the date, image and comment names as seen in the data (names if JSON, numbers [0,1,2] in an array)
dataObject   : ['dates','image','comment'],
comment      : '.imageComment', // class or id where the current image comment will be added; if it doesn't exist, no comment will be shown
noImageTitle : false,           // if true, the script will not add the comment to the image's title attribute

/* language options */
dayEndings : 'st|nd|rd|th',  // 1st, 2nd, 3rd, 4th, etc. (e.g. 1stMon & 3rdThu)
dayLast    : 'last',         // last weekday/weekend of the month
dayWeek    : ['sun','mon','tue','wed','thu','fri','sat'], // days of the week (case insensitive)

/* debugging options */
locked          : false,// prevent debug mode onscreen output if true (debug setting from browser URL only).
debug           : false,// set debug mode.
debugId         : 'imagedebug', // id of the debug output div - all info and show all images will be added here.
debugElement    : 'body',       // Location where debug id is added (where debugId div is appended)
defaultDate     : '1/1/2010',   // date used if setting debug mode but no default date.
inRangeColor    : '#080',       // color highlight (green) for signifying the date is in range.
notInRangeColor : '#f00'// color highlight (red) for dates not in range.
})
OptionDescription
dataPlease refer to the Data Sources section above on how to set this data
dataObjectPlease refer to the Data Sources section above on how to set this data
commentThis option targets the class or Id of an HTML element where you want the current image comment to be displayed, if this target doesn't exist, no comment will be shown
noImageTitlePrevent the script from adding the comment to the image title. Done if you want to apply a tooltip to the image to display the message
dayEndingsAbbreviation suffixes for 1st, 2nd, 3rd, 4th, etc. The script actually only uses these to match the date string, but they are ignored otherwise. So if you mistakenly add 4rd, it will use the 4 and not the "rd"
dayLastString used to match and causes the script to find the last weekday/weekend of the month (e.g. lastMon)
dayWeekList of days of the week. It is used to match the weekday from the data date, so this is language agnostic, but the first day of the array must start with sunday to match the javascript date method. Limit these names to three letters and the letter case doesn't matter.
monthFirstSet to true (default) to use standard US date formatting - mm/dd/yyyy. If set to false, the European date format can be used - dd/mm/yyyy.
lockedLocks out using the debug mode from the browser address bar (adding "#debug" to the url will enable debug mode if the script isn't locked, like my demo)
debugEnable debug mode through the script. This value is set to true if the script is not locked and "#debug" is added to the url. This mode will display image data to show matches found and date ranges. For more detail, see the troubleshooting section below
debugIdThis is the Id of the element to find to output the debug data. If it doesn't exist and debug mode is initiated, a div with this Id will be added to the debugElement (see below) which by default is the document body. If using the showAll() method above, this element will need to me manually added before images will be displayed.
debugElementBy default, the debug element is the document body. This is where the debugId div is appended, if it doesn't exist, when debug mode is initiated.
defaultDateThis date is only used if the debug mode is initiated by the script and no other date is supplied. The default date for the debug mode when adding it to the url would be the current date unless otherwise specified - see the troubleshooting section below for more details
inRangeColorText color applied to the debug text when an image date is in range (green by default)
notInRangeColorText color applied to the debug text when an image date is not in range (red by default)

Methods
You can get, set, find current number or have the script choose another random image as follows:
  • Get current image data

    • Use the "currentImage()" method to get this data.
    • This method will return an array of the currently displayed image's date, url and comment (in that order).
    • The second line was added to show how to target the image url.
    var current = $(image).data('todaysImage').currentImage(); // returns array ['dates','image url','comment']
    alert( 'current image url = ' + current[1] );
  • Set/Get current image

    • Add a number to the "currentImage()" method to set the image. An array of the image is returned (as above).
    • Setting a number that is greater than the number of images in the date range will cause the image to wrap around back to the beginning
    // Sets displayed image to the first (zero based index) image that falls within the date range,
    // then returns array ['dates','image url','comment'] of that image
    var current = $(image).data('todaysImage').currentImage(1);
  • Get current number of images

    • Use the "currentNumber()" method to get this information.
    • This value is the number of images with a date range that covered the date (e.g. if the date is 12/24, the demo script will find 8 images from which to choose)
    // return the number of images that have date ranges that match the date
    var currentNumber = $(image).data('todaysImage').currentNumber(); // returns a number
  • Random Image

    • Use the "randomImage()" method to display another random image.
    • This method will return an array of the currently displayed image's date, url and comment (in that order).
      $(image).data('todaysImage').randomImage(); // displays a random image in the date range & returns array
  • Show All Images

    • This method is used by in edit mode when clicking on the "Show All Images" button. It is useful for checking that all the image urls are correct and all the data is valid.
    // Show all images in the data, the script targets the id found in "debugId" option
    $(image).data('todaysImage').showAll();
    • This method will not work if the element targeted by the "debugId" option doesn't exist. By default, the "debugId" is "imagedebug" which is added while the script is in debug mode. But if you want to display a list somewhere else, just make sure the target exists (Id only, no class) and change the "debugId" option to match it.
      CSS
      #allImages img { height: 100px; width: 100px; } /* make images thumbnail size */
      HTML
      <div id="allImages"></div>
      Script
      // initialization of script
      $('#todaysImage').todaysImage(
      // other options here, including data source
      debugId : 'allImages'
      });
    Troubleshooting
    • If you are noticing that one image isn't showing up or something just doesn't work right, you can try to trouble shoot the problem.
    • Initiate the debug mode in the script as follows:
      // initialization of script
      $('#todaysImage').todaysImage(
      // other options here, including data source
      debug: true,
      defaultDate: '12/24/2010' // choose any date using this format
      });
    • Additionally, you can access the debug mode from the address bar simply by adding the following (in blue) to the end:

      http://myurl.com/randomImage.htm#debug:12/31/2009

      • #debug - actives the debug mode
      • 12/31/2009 - sets the date you want to check (month/day/year)

      To prevent debug from working once you are done with your script, simply set the locked variable in the script to true:
      // initialization of script
      $('#todaysImage').todaysImage(
      // other options here, including data source
      // locks out debug mode from the URL
      locked: true
      });
    • Sample debug output:
      Date used: 11/20/2009
      (11/11), derived range: 11-11 is NOT in range; image = veterans1.jpg
      (11/11), derived range: 11-11 is NOT in range; image = veterans2.jpg
      (11/11), derived range: 11-11 is NOT in range; image = veterans3.jpg
      (11/3rdThur-11/4thThur), derived range: 19-26 is in range; image = thanksgiving1.jpg
      (11/3rdThur-11/4thThur), derived range: 19-26 is in range; image = thanksgiving2.jpg
      (11/3rdThur-11/4thThur), derived range: 19-26 is in range; image = thanksgiving3.jpg
      (11/3rdThur-11/4thThur), derived range: 19-26 is in range; image = thanksgiving4.jpg

      # of currentImages = 4
      current random image = thanksgiving4.jpg
    • Try out these links to see the debug mode in action (my example script isn't locked):

      1. #debug:1.1.2010
      2. #debug:10/31/2009
      3. #debug - This will default to todays date, but edit mode is active