Showing posts with label Resources. Show all posts
Showing posts with label Resources. Show all posts

Friday, December 21, 2012

Using Google Closure Compiler to write better code

So I ran into a bit of code that I wrote a while back. I thought could be written a lot more efficiently. So I thought, hey Google's Closure Compiler is good at that stuff right? Let see what happens!

So, lets say we started out with this list of contacts:

var list = {
 'friends' : {
  'fred' : '123-4567',
  'lisa' : '123-6789'
 },
 'work' : {
  'joe'  : '234-5678'
 }
}

I need a function to add someone to the list, or update a number, but it need to check if the group already exists or if the person is already listed:

function addName(group, name, number){
 if (list[group] && list[group][name]){
  list[group][name] = number;
 } else {
  if (list[group]){
   list[group][name] = number;
  } else {
   list[group] = {};
   list[group][name] = number;
  }
 }
}

so the addName function starts out by seeing if the group exists, then if the name exists within that group:

if (list[group] && list[group][name]){

if that works, then just add/update the number. If not, then we drop through the else to the next set of evaluations. Does the group exist?

if (list[group]){

Yes? Add the number, if not, keep going to through the next else, from here we know that the group doesn't exist, so we define it, then add the name.

Seems efficient right?... hmm, but I see a lot of list[group][name] = number; in there. Lets see what Google's closure compiler does to it.

But first we need to keep the compiler from changing all the names. Lets pull out the code from within the function and force the compiler to keep our constants (see the first line):

/** @const */ var list = {}, group, name, number;

 if (list[group] && list[group][name]){
  list[group][name] = number;
 } else {
  if (list[group]){
   list[group][name] = number;
  } else {
   list[group] = {};
   list[group][name] = number;
  }
 }

The extra asterisk in the comment is important! (ref). Note, maybe there is a better way to force the compiler to keep your variable names within a function, but I don't know it.

Anyway, click the reset link and paste the code above into the compiler (use the "Simple" setting if it isn't already set). You'll end up with this:

var list={},group,name,number;if(!list[group]||!list[group][name])list[group]||(list[group]={});list[group][name]=number;

If it's too messy for you to clean up yourself, or you have a lot more code, copy that result and paste it into jsbeautifier and you'll end up with nicer formatted code.

Also, it's important to remember that compilers don't necessarily always use curly brackets after if or else statements, so the above code is equivalent to:

var list = {}, group, name, number;
if (!list[group] || !list[group][name]) {
  list[group] || (list[group] = {});
}
list[group][name] = number;

So now we can replace our function wrapper and remove the constants:

function addName(group, name, number){
  if (!list[group] || !list[group][name]) {
    list[group] || (list[group] = {});
  }
  list[group][name] = number;
}

That looks better, but I'm still seeing a bunch of list[group] in there. I'm sure the code can be a bit more DRY (don't repeat yourself). Also do we really need to check list[group][name]? It was checked initially but not checked again...

function addName(group, name, number){
  if (!list[group]) {
    list[group] || (list[group] = {});
  }
  list[group][name] = number;
}

Well now I see that list[group] is checked twice. Let's get rid of that and see what happens:

function addName(group, name, number){
  if (!list[group]) {
    list[group] = {};
  }
  list[group][name] = number;
}

Now lets see what we have. Check if list[group] exists, then add it if it doesn't. Add the number. Wow! Short and sweet. Why didn't I write it like that originally? Oh yeah, I'm still a noob :)

So after some testing, this new cleaned up function works exactly like the original! We went from 10 lines inside of the addName function down to 4! I'm happy, a happy noob!

I threw together this demo for testing.



I hope that my process of learning at least gives you some ideas. I don't claim to be an expert at javascript or jQuery, but I try to learn a little every day :)

As a redneck friend of mine once said, "Dem Google people is smart!"

Saturday, December 17, 2011

All Images Loaded (imagesLoaded)

*NOTE* this code causes infinite loops in IE with image load errors, so I improved and turned this into a real plugin and hosted it on github.


A little jQuery function that checks if all images are loaded. If they all are, the callback function is called. Check out the latest version in this gist.

Script below:
/*
 Check if all images are loaded
 - Callback occurs when all images are loaded
 - image load errors are ignored (complete will be true)
 - Use:
   $('.wrap img').imagesLoaded(function(){
     alert('all images loaded');
   });
*/

jQuery.fn.extend({
  imagesLoaded: function( callback ) {
    var i, c = true, t = this, l = t.length;
    for ( i = 0; i < l; i++ ) {
      if (this[i].tagName === "IMG") {
        c = (c && this[i].complete && this[i].height !== 0);
      }
    }
    if (c) {
      if (typeof callback === "function") { callback(); }
    } else {
      setTimeout(function(){
        jQuery(t).imagesLoaded( callback );
      }, 200);
    }
  }
});

Use it as follows:
$(function(){
  $('.wrap img').imagesLoaded(function(){
    alert('all images loaded');
  });
});

Tuesday, December 6, 2011

jQuery Pathslider

I just created this plugin that is a very basic UI Slider (similar to the jQuery slider), but it doesn't just allow you to move the handle horizontally and vertically, it will follow any shaped curve! If you've ever used Adobe Illustrator or CorelDraw then you probably recognize the Bezier curve below.

The handle, or grip as I call it, can be dragged along the black curve. For now, it only returns values from 0 (green dot) to 100 (red dot).


The plugin is in the early stages of development and still needs a lot of work, but it is usable now.

Check out the demo page, and the builder page. If you would like to help me out, submit an enhancement or issue, or fork a copy of the plugin on Github and send me a pull request! I'd love the input!

Monday, October 31, 2011

Get All indexOf From An Array - Array.allIndexOf()

This array function is an extension of Array.indexOf(), if it exists, and will return all indexes of the search element. It's named allIndexOf(). This code extends javascript and does not require jQuery. It is designed to work in older versions of IE as well, albeit a tiny bit slower.

Originally if you wanted the second instance of a search element, you'd have to call "indexOf" twice. The second time with the starting index of the first result (plus one), or once with a guessed starting index. This should simplify the process for getting any or all of the indexes.

/*
Array.allIndexOf(searchElement)
  Array [Array] - the array to search within for the searchElement
  searchElement [String] - the desired element with which to find starting indexes
*/
(function(){
  Array.prototype.allIndexOf = function(searchElement) {
    if (this === null) { return [-1]; }
    var len = this.length,
    hasIndexOf = Array.prototype.indexOf, // you know, because of IE
    i = (hasIndexOf) ? this.indexOf(searchElement) : 0,
    n,
    indx = 0,
    result = [];
    if (len === 0 || i === -1) { return [-1]; }
    if (hasIndexOf) {
      // Array.indexOf does exist
      for (n = 0; n <= len; n++) {
        i = this.indexOf(searchElement, indx);
        if (i !== -1) {
          indx = i + 1;
          result.push(i);
        } else {
          return result;
        }
      }
      return result;
    } else {
    // Array.indexOf doesn't exist
      for (n = 0; n <= len; n++) {
        if (this[n] === searchElement) {
          result.push(n);
        }
      }
      return (result.length > 0) ? result : [-1];
    }
  };
})();
Use it as follows:
var s = ["red","green","blue","red","yellow","blue","green","purple","red"];
s.allIndexOf("r"); // result [ -1 ]
s.allIndexOf("red"); // result [ 0,3,8 ]
s.allIndexOf("blue"); // result [ 2,5 ]
Try out your own strings in the demo below or full screen.

Get All indexOf From A Search String - String.allIndexOf()

This string function is an extension of String.indexOf() and will return all indexes of a search string. It's named allIndexOf. This code extends javascript and does not require jQuery.

Originally if you wanted the second instance of a search string, you'd have to call "indexOf" twice. The second time with the starting index of the first result (plus one), or once with a guessed starting index. This should simplify the process for getting any or all of the indexes. And yes, I woke up with a strange urge to write this function LOL.

/*
String.allIndexOf(searchstring, ignoreCase)
  String [String] - the string to search within for the searchstring
  searchstring [String] - the desired string with which to find starting indexes
  ignoreCase [Boolean] - set to true to make both the string and searchstring case insensitive
*/
(function(){
  String.prototype.allIndexOf = function(string, ignoreCase) {
    if (this === null) { return [-1]; }
    var t = (ignoreCase) ? this.toLowerCase() : this,
    s = (ignoreCase) ? string.toString().toLowerCase() : string.toString(),
    i = this.indexOf(s),
    len = this.length,
    n,
    indx = 0,
    result = [];
    if (len === 0 || i === -1) { return [i]; } // "".indexOf("") is 0
    for (n = 0; n <= len; n++) {
      i = t.indexOf(s, indx);
      if (i !== -1) {
        indx = i + 1;
        result.push(i);
      } else {
        return result;
      }
    }
    return result;
  }
})();
Use it as follows:
var s = "The rain in Spain stays mainly in the plain";
s.allIndexOf("ain"); // result [ 5,14,25,40 ]
s.allIndexOf("the"); // result [ 34 ]
s.allIndexOf("THE", true); // result [ 0,34 ]
Try out your own strings in the demo below or full screen.

Thursday, October 20, 2011

jQuery Rename Attribute (renameAttr)

I woke up this morning with a very unusual urge to make a rename attribute function for jQuery. I know, right.. that's just too weird! So after messing around with it, I decided that it needed to update the data as well. Here is the code I ended up with:

jQuery.fn.extend({
  renameAttr: function( name, newName, removeData ) {
    var val;
    return this.each(function() {
      val = jQuery.attr( this, name );
      jQuery.attr( this, newName, val );
      jQuery.removeAttr( this, name );
      // remove original data
      if (removeData !== false){
        jQuery.removeData( this, name.replace('data-','') );
      }
    });
  }
});

Use it as follows:
// $(selector).renameAttr(original-attr, new-attr, removeData);

// removeData flag is true by default
$('#test').renameAttr('data-test', 'data-new' );

// removeData flag set to false will not remove the
// .data("test") value
$('#test').renameAttr('data-test', 'data-new', false );

Basically, it adds a new attribute with the same value and removes the old one. It automatically removes the jQuery data for the original attribute unless you set the "removeData" flag to false. Check out this demo

Thursday, October 6, 2011

Adding Swipe Support

I spent some time trying to figure out how to add mobile swipe support without using jQuery Mobile, because the swipe is all I really needed. I don't own an iPhone/iPad/iAnything, so it took a bit of back-and-forth testing - thanks to all the folks over at CSS-Tricks.com - to get this to finally work.



Here is the complete code:

var maxTime = 1000, // allow movement if < 1000 ms (1 sec)
    maxDistance = 50,  // swipe movement of 50 pixels triggers the swipe

    target = $('#box'),
    startX = 0,
    startTime = 0,
    touch = "ontouchend" in document,
    startEvent = (touch) ? 'touchstart' : 'mousedown',
    moveEvent = (touch) ? 'touchmove' : 'mousemove',
    endEvent = (touch) ? 'touchend' : 'mouseup';

target
    .bind(startEvent, function(e){
        // prevent image drag (Firefox)
        e.preventDefault();
        startTime = e.timeStamp;
        startX = e.originalEvent.touches ? e.originalEvent.touches[0].pageX : e.pageX;
    })
    .bind(endEvent, function(e){
        startTime = 0;
        startX = 0;
    })
    .bind(moveEvent, function(e){
        e.preventDefault();
        var currentX = e.originalEvent.touches ? e.originalEvent.touches[0].pageX : e.pageX,
            currentDistance = (startX === 0) ? 0 : Math.abs(currentX - startX),
            // allow if movement < 1 sec
            currentTime = e.timeStamp;
        if (startTime !== 0 && currentTime - startTime < maxTime && currentDistance > maxDistance) {
            if (currentX < startX) {
                // swipe left code here
                target.find('h1').html('Swipe Left').fadeIn();
                setTimeout(function(){
                    target.find('h1').fadeOut();
                }, 1000);
            }
            if (currentX > startX) {
                // swipe right code here
                target.find('h1').html('Swipe Right').fadeIn();
                setTimeout(function(){
                    target.find('h1').fadeOut();
                }, 1000);
            }
            startTime = 0;
            startX = 0;
        }
    });


Now, lets break down what each line does. First, some definitions:

var maxTime = 1000, // allow movement if < 1000 ms (1 sec)
    maxDistance = 50,  // swipe movement of 50 pixels triggers the swipe

    target = $('#box'),
    startX = 0,
    startTime = 0,

The "maxTime" variable is a set to 1000 milliseconds. It is used to compare the touch start and touch end times, and if less than one second (1000 ms) it is considered to be a valid swipe, otherwise it is ignored. Basically, we don't like them slow swipers! Hehe, ok, actually, it to determine if the event ends up being a touch or a swipe.

"maxDistance" is the number of pixels from the touch start point that the mouse or a finger moved to determine if a swipe or touch is occurring.

The "target" variable, is the target element to add swipe support to. For the demo, I added an H1 tag inside of this element to add a swipe message. The "startX" and "startTime" variables are set to zero because the "touchmove" or "mousemove" event looks to see if they are set with the "startX" position and start time. If zero, it ignores the move.

In the next part we figure out of the device has touch support and set up the event names appropriately:

touch = "ontouchend" in document,
startEvent = (touch) ? 'touchstart' : 'mousedown',
moveEvent = (touch) ? 'touchmove' : 'mousemove',
endEvent = (touch) ? 'touchend' : 'mouseup';

The "touch" varible is true if the "ontouchend" event exists, which means touch is supported. All that line is doing is looking to see if that object exists.

The next variables, "startEvent", "moveEvent" and "endEvent" contain the necessary event names for start, move and end, respectively. Which the next lines of code will now bind to:
target
    .bind(startEvent, function(e){
        // prevent image drag (Firefox)
        e.preventDefault();
        startTime = e.timeStamp;
        startX = e.originalEvent.touches ? e.originalEvent.touches[0].pageX : e.pageX;
    })
The "touchstart" or "mousedown" event code contains the following:

"e.preventDefault()" prevents the image drag function in Firefox which actually makes you think you're dragging the image instead of swiping. Now we define "startTime" and "startX" as the start time and position of the swipe. The "startX" needs to get the position of the touch from the touches variable, but only the first one. We don't want to swipe if there are multiple touches (gestures) but it shouldn't work anyway since we're not binding to the gestures event.
.bind(endEvent, function(e){
    startTime = 0;
    startX = 0;
})
Now the "touchend" or "mouseup" event code only clears the start time and position variables. The reason for this, which was a fun and painful lesson, is that these events don't fire when your mouse/finger leaves the element or goes off the screen. So the actual swipe calculations are done in the move event functions.

.bind(moveEvent, function(e){
    e.preventDefault();
    var currentX = e.originalEvent.touches ? e.originalEvent.touches[0].pageX : e.pageX,
        currentDistance = (startX === 0) ? 0 : Math.abs(currentX - startX),
        // allow if movement < 1 sec
        currentTime = e.timeStamp;
    if (startTime !== 0 && currentTime - startTime < maxTime && currentDistance > maxDistance) {
        if (currentX < startX) {
            // swipe left code here
            target.find('h1').html('Swipe Left').fadeIn();
            setTimeout(function(){
                target.find('h1').fadeOut();
            }, 1000);
        }
        if (currentX > startX) {
            // swipe right code here
            target.find('h1').html('Swipe Right').fadeIn();
            setTimeout(function(){
                target.find('h1').fadeOut();
            }, 1000);
        }
        startTime = 0;
        startX = 0;
    }
});

The "touchmove" or "mousemove" events is where the main portion of this swipe code exists. Again, we want to prevent the default functionality of the move/drag. The "currentX" variable is set with the current x position of the finger/mouse. Again, the x position is obtained from the touches variable, if available. The difference "currentDistance" between the starting x and current x position is then determined. We need the absolute value of this number (ignore the negative sign if it is there) to see if it is more than the "maxDistance" variable. Also here is another trick to make sure the move event is occurring after a start event, check to make sure the starting x position isn't zero. If it is, set the "currentDistance" variable to zero. We also need determine the "currentTime" current time of the move event.

Now, we check to make sure the move event occurred after a start event. This can happen is the start event occurs outside of the element or window. So, first we make sure the "startTime" isn't zero, then we check that the time difference from start to current time is less than one second. And finally, we check that the current distance is more than the max distance setting. If it is, then we have a swipe!

YAY finally a swipe! Now to just figure out if it was going to the right or left. We do this by comparing the current position "currentX" to the starting position "startX"; if the current position is less than the start, then it's a swipe to the left and if it is greater, then the swipe was to the right.

Last but not least, we set the start time and position back to zero to prevent multiple firing of the swipe event.

I hope everything had a clear enough explanation. Please feel free to leave a comment or email me (gmail with wowmotty as the user) with questions or if you need further clarification.

Tuesday, July 12, 2011

Adding WordPress Quicktag Buttons to a WP Plugin

Disclaimer: I am a total noob when it comes to WordPress and WordPress plugins.

I was wondering how difficult it would be to add a button to the WordPress Post Editor. I've seen a few plugins do it, but I discovered that they had to add their own version of TinyMCE to do it.

So I searched around on the internet, and found a few methods. But they either stopped working or required you to edit your quicktags.js file to work, then again after each Wordpress update. So I looked into it and I found a solution that works with the current version 3.2 and hopefully future versions without requiring you to edit or replace the quicktags file.

Before

After

Note: This code is intended to be added inside your WordPress plugin. Like I said, I'm a total noob with WordPress so I'm not sure if it would work outside of a plugin.

CSS - This CSS should be outside of the php tags
<style>
/* Simulate the input buttons styles */
#ed_toolbar button {
 display: inline-block;
 vertical-align: middle;
 margin: 0;
 padding: 0;
 min-width: 26px;
 height: 24px;
 -moz-border-radius: 3px;
 -webkit-border-radius: 3px;
 border-radius: 3px;
 border: #c3c3c3 1px solid;
 background: url() repeat-x;
}
#ed_toolbar button:hover {
 border: #aaa 1px solid;
 background: #ddd;
}
/* toolbar button images in span */
#ed_toolbar button span {
 display: inline-block;
 background: transparent no-repeat center center;
 padding: 4px 2px;
 width: 16px;
 height: 16px;
}

/* toolbar custom button image */
#_custom span {
 background-image: url(/wp-content/plugins/my-plugin-directory/my-plugin-icon.png);
}
</style>

PHP - Add this to your plugin php
<?php
add_action('admin_footer', 'my_plugin_add_button');

function my_plugin_add_button(){
echo <<<EOT
<script>/* <![CDATA[ */
var j, n,
 last = edButtons.length,
 tbar = '';

// add a new editor button as follows:
// edButtons[edButtons.length] = new edButton('BUTTON ID', 'BUTTON TEXT', 'OPENING TAG', 'CLOSING TAG', 'ACCESSKEY');
// Example: edButtons[edButtons.length] = new edButton('_h1', 'H1', '<h1>', '</h1>', -1);
edButtons[edButtons.length] = new edButton('_h1', 'h1', '<h1>', '</h1>', "h");
edButtons[edButtons.length] = new edButton('_h2', 'h2', '<h2>', '</h2>', -1);
edButtons[edButtons.length] = new edButton('_h3', 'h3', '<h3>', '</h3>', -1);
edButtons[edButtons.length] = new edButton('_h4', 'h4', '<h4>', '</h4>', -1);
edButtons[edButtons.length] = new edButton('_h5', 'h5', '<h5>', '</h5>', -1);
edButtons[edButtons.length] = new edButton('_h6', 'h6', '<h6>', '</h6>', -1);
edButtons[edButtons.length] = new edButton('_custom', '<button>', '[myplugin_short_code]', '', -1);

// Don't change anything below
// ***************************
for ( j = last; j < edButtons.length; j++) {
 n = edButtons[j];
 if (/<button>/g.test(n.display)) {
  // matched opening tag => add a button
  tbar += '<button id="' + n.id + '" type="button" class="ed_button" onclick="edInsertTag(edCanvas, ' + j + ');"><span></span></button>';
 } else {
  // add an input
  tbar += '<input type="button" id="' + n.id + '" accesskey="' + n.access + '" class="ed_button" onclick="edInsertTag(edCanvas,' + j + ');" value="' + n.display.replace(/\"/g,'\"') + '" />';
 }
}
document.getElementById('ed_toolbar').innerHTML += tbar;
/* ]]> */</script>
EOT;
}
?>

Saturday, June 18, 2011

jQuery Tablesorter, The Missing Docs

If you need support for my fork of tablesorter, please contact me here:

The Missing Docs for jQuery Tablesorter 2.0

These docs have been incorporated into my

All Defaults


Appearance
cssHeader: "header"The CSS style used to style the header in its unsorted state.
cssAsc: "headerSortUp"The CSS style used to style the header when sorting ascending.
cssDesc: "headerSortDown"The CSS style used to style the header when sorting descending.
cssChildRow: "expand-child"This allows you to add a child row that is always attached to the parent. See the children rows section below.
widgetZebra: { css: ["even", "odd"] }Set the CSS classes used for zebra striping the table. See the updating zebra striping section below.
widthFixed: falseIndicates if tablesorter should apply fixed widths to the table columns. This is useful for the Pager companion. Requires the jQuery dimension plugin to work. See the main demo page
onRenderHeader: nullThis function is called when classes are added to the th tags. You can use this to append HTML to each header tag. See the Render Header section below.
Sort Options
sortInitialOrder: "asc"A string of the inital sorting order can be "asc" or "desc". This is overridden bt the sortList settings.
sortForce: nullUse to add an additional forced sort that is prepended to sortList. For example, sortForce: [[0,0]] will sort the first column in ascending order. See the main demo page
sortList: []Add an array of the order the table should be initially sorted; e.g. sortList: [[0,1], [1,0]]. The first part "[0,1]" will sort the first column (zero based index) in decending order and the second part "[1,0]" is to sort the second column in ascending order. See the main demo page.
sortAppend: nullUse to add an additional default sorting rule, that is appended to the sortList.
Parsers & Widgets
headers: {}See the Change how you sort a column section below or see the main demo page
textExtraction: "simple"See the Text Extraction section below or see the main demo page
sortLocaleCompare: trueBoolean flag indicating whenever to use String.localeCampare method or not. This is only used when comparing text strings.
parsers: {}Internal list of all of the parsers. See a complete list of parsers below.
widgets: []Initialize widgets using this option (e.g. widgets : ['zebra'], or custom widgets widgets: ['zebra', 'myCustomWidget']; see this demo on how to add a custom widget.)
selectorHeaders: 'thead th'jQuery selectors used to find the header cells. You can change this, but the table will still need the required thead and tbody before this plugin will work properly.
headerList: []Internal list of each header element as selected using jQuery selectors in the selectorHeaders option.
dateFormat: "us"Set the date format. See the dataFormat section below.
Interaction
sortMultiSortKey: "shiftKey"The key used to select more than one column for multi-column sorting. Defaults to the shift key. Other options might be ctrlKey, altKey. See the main demo page
cancelSelection: trueIndicates if tablesorter should disable selection of text in the table header (TH). Makes header behave more like a button.
Unknown
decimal: '/\.|\,/g'Regex to find the decimal "." for U.S. format and "," for European format... but I can't find it being used anywhere in the plugin.
Debugging
debug: falseBoolean flag indicating if tablesorter should display debuging information usefull for development. This displays information in the console, if available, or in alerts. See the main demo page

Update Default Settings

Update any of the default settings above using this format:
$(selector).data("tablesorter").{default name} = newValue;
Here are a few examples:
// Change sortForce
$("table").data("tablesorter").sortForce = [[1,0]];
// it appears that the only way to update the table is to resort it
$("table").trigger("sorton", [[0,1]]);
// Add zebra sorting after the table has initialized
$("table").data("tablesorter").widgets = ["zebra"];
// there is a method to apply widgets!
$("table").trigger("applyWidgets");

Child Rows

To add a row that remains attached to a parent row, add the "expand-child" class to the row.

You can add some scripting to hide these child rows and have them expand when you click on a link; this is the original reason for the css name.

Here is the original mod page, the code was added into the TableSorter core but remains undocumented.

This makes the tablesorter plugin keep these rows together. This css class can be changed using the "cssChildRow" option.

Here is some example markup:
<table width="100%" border="1">
  <thead>
    <tr>
      <th>Item #</th>
      <th>Name</th>
      <th>Available</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>12345</td>
      <td>Toy Car</td>
      <td>5</td>
    </tr>
    <tr class="expand-child"> <!-- this row will remain attached to the above row, and not sort separately -->
      <td colspan="3">
        It's a toy car!
      </td>
    </tr>
    <tr>
      <td>23456</td>
      <td>Toy Plane</td>
      <td>2</td>
    </tr>
    <tr class="expand-child"> <!-- this row will remain attached to the above row, and not sort separately -->
      <td colspan="3">
        It's a toy plane!
      </td>
    </tr>
    <tr class="expand-child"> <!-- this row will remain attached to the above two rows, and not sort separately -->
      <td colspan="3">
        and it flies!
      </td>
    </tr>
  </tbody>
</table>


Update Zebra Striping After Sorting

Actually the zebra striping widget does this for you automatically.

By default the odd and even rows are styled using "odd" and "even" class names ({ css: [ "even", "odd" ] }).

Change these css class names using the widgetZebra option as follows:
$(function(){
  $("table").tablesorter({
    widgets: ["zebra"], // initialize zebra striping of the table
    widgetZebra: { css: [ "normal-row", "alt-row" ] }
  });

Modify the Header Markup

The onRenderHeader option allows you to add a function that which can modify the HTML of each header tag. In the example below, the header cell (th) content is wrapped with a span tag to allow for better styling (source).
$(function(){
  $("table").tablesorter({
    onRenderHeader: function (){
      this.wrapInner('<span class="roundedCorners"></span>');
    }
  });
});
and you'll end up with this (only the thead is shown):
<thead>
  <tr>
    <th class="header"><span class="roundedCorners">Column 1</span></th>
    <th class="header"><span class="roundedCorners">Column 2</span></th>
  </tr>
</thead>


Change how you sort a column

The plugin attempts to detect the type of data that is contained in a column, but if it can't figure it out then it defaults to alphabetical.

You can easily override this by setting the header argument (or column parser).
$(function(){
  $("table").tablesorter({
    headers: {
      0: { sorter: false },      // disable first column
      1: { sorter: "digit" },    // sort second column numerically
      4: { sorter: "shortDate" } // sort the fifth column by date (e.g. mm/dd/yyyy if the date format is "us")
    }
  });
});
*Note* the header number starts with zero (zero based index).

Here is a list of available sorter values/parsers:

sorter: falsedisable sort for this column.
sorter: "text"Sort alphabetically.
sorter: "digit"Sort numerically.
sorter: "currency"Sort by currency value (supports "£$€").
sorter: "ipAddress"Sort by IP Address.
sorter: "url"Sort by url.
sorter: "isoDate"Sort by ISO date (YYYY-MM-DD or YYYY/MM/DD).
sorter: "percent"Sort by percent.
sorter: "usLongDate"Sort by date (U.S. Standard, e.g. Jan 18, 2001 9:12 AM).
sorter: "shortDate"Sort by a shorten date (see "dateFormat").
sorter: "time"Sort by time (23:59 or 12:59 pm).
sorter: "metadata"Sort by the sorter value in the metadata - requires the metadata plugin.

Change the Date Format (dateFormat)

"us""mm-dd-yyyy" or "mm/dd/yyyy"
"uk""dd-mm-yyyy" or "dd/mm/yyyy"
"dd/mm/yy" or
"dd-mm-yy"
Sort by short year (it appears to sort by day first, not the year)




Change the Sorted Columns

There are three options to determine the sort order and this is the order of priority:
  1. sortForce forces the user to have this/these column(s) sorted first (null by default).
  2. SortList is the initial sort order of the columns.
  3. SortAppend is the default sort that is added to the end of the users sort selection (null by default).
The value of these sort options is an array of arrays and can include one or more columns. The format is an array of instructions for per-column sorting and direction in the format: [[columnIndex, sortDirection], ... ] where columnIndex is a zero-based index for your columns left-to-right and sortDirection is 0 for Ascending and 1 for Descending. A valid argument that sorts ascending first by column 1 and then column 2 looks like: [[0,0],[1,0]]
$(function(){
  $("table").tablesorter({
    sortForce  : [[0,0]],        // always sort first column first
    sortList   : [[1,0], [2,0]], // initial sort columns (2nd and 3rd)
    sortAppend : [[3,0]]         // always add this sort on the end (4th column)
  });
});
*NOTE* When writing this post, I didn't realize that the sortAppend option was listed as an option, but the code for it did not exist. This problem is fixed in my fork of tablesorter (example).

Text Extraction

This is demonstrated in the Dealing with markup inside cells demo page, but the example uses vanilla javascript to find the desired text, like this:
$(function(){
  $("table").tablesorter({
    textExtraction: function(node) {
      return node.childNodes[0].childNodes[0].innerHTML;
    }
  });
});
But you could make your life easier and just make the node into a jQuery object and then "find" what you need:
$(function(){
  $("table").tablesorter({
    textExtraction: function(node) {
      return $(node).find("span:last").text();
    }
  });
});
Now if the text you are finding in the script above is say a number, then just include the headers sorter option to specify how to sort it. Also in this example, we will specify that the special textExtraction code is only needed for the second column ("1" because we are using a zero-based index). All other columns will ignore this textExtraction function.


*NOTE* When writing this post, I didn't realize that the textExtraction option did not allow defining it per column, as shown below, but this has been fixed in the forked github version - example.
$(function(){
  $("table").tablesorter({
    textExtraction: {
      1: function(node) {
           return $(node).find("span:last").text();
      }
    },
    headers: {
      1: { sorter : "digit" }
    }
  });
});


Methods

See examples of each further down
sortonResort the table using new sort parameters.
updateUpdate the stored tablesorter data and the table.
appendCacheUpdate a table that has had its data dynamically updated; used in conjunction with "update"
updateCellUpdate a table cell in the tablesorter data.
applyWidgetIdApply a widget (wrapped in square brackets) to the table one time only.
applyWidgetsApply previously selected widgets to the table - will update the widget with new sort.

Resort the table - "sorton" method

See the full example in the tablesorter docs: Sort table using a link outside the table
// Choose a new sort order
var sort = [[0,0],[2,0]];
// Note that the sort value below is inside of another array (inside another set of square brackets)
$("table").trigger("sorton", [sort]);

Update the Table

See the full example in the tablesorter docs: Appending table data with ajax
// Add new content
$("table tbody").append(html);
// let the plugin know that we made a update
$("table").trigger("update");
// set sorting column and direction, this will sort on the first and third column
var sorting = [[2,1],[0,0]];
// sort on the first column
$("table").trigger("sorton", [sorting]);

Append Cache

If you dynamically change the table content, more than just one cell like in the "updateCell" example above, you may possibly have to trigger two events: "update" and "appendCache".

This answer is from a StackOverflow answer
// Table data was just dynamically updated
$(table)
  .trigger("update")
  .trigger("appendCache");

Update a Table Cell

Example from these alternate tablesorter docs: Updating the table cache - the demo doesn't work, but I've tested the example code below and it works properly.
$(function() {
  $("table").tablesorter();
  $("td.discount").click(function(){
      // randomize a number
      var discount = '$' + Math.round(Math.random() * Math.random() * 100) + '.' + ('0' + Math.round(Math.random() * Math.random() * 100)).slice(-2);
      $(this).text(discount);
      // update the table, so the tablesorter plugin knows its value
      $("table").trigger("updateCell",[this]);
      // set sorting column and direction, this will sort on the first and third column
      var sorting = [[3,1]];
      // sort on the first column
      $("table").trigger("sorton",[sorting]);
      return false;
  });
});
<table class="tablesorter" cellspacing="1">
  <thead>>
    <tr>
      <th>first name</th>
      <th>last name</th>
      <th>age</th>
      <th>total</th>
      <th>discount</th>
      <th>date</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>peter</td>
      <td>parker</td>
      <td>28</td>
      <td class="discount">$9.99</td>
      <td>20%</td>
      <td>jul 6, 2006 8:14 am</td>
    </tr>
    <tr>
      <td>john</td>
      <td>hood</td>
      <td>33</td>
      <td class="discount">$19.99</td>
      <td>25%</td>
      <td>dec 10, 2002 5:14 am</td>
    </tr>
    <tr>
      <td>clark</td>
      <td>kent</td>
      <td>18</td>
      <td class="discount">$15.89</td>
      <td>44%</td>
      <td>jan 12, 2003 11:14 am</td>
    </tr>
    <tr>
      <td>bruce</td>
      <td>almighty</td>
      <td>45</td>
      <td class="discount">$153.19</td>
      <td>44%</td>
      <td>jan 18, 2001 9:12 am</td>
    </tr>
    <tr>
      <td>bruce</td>
      <td>evans</td>
      <td>22</td>
      <td class="discount">$13.19</td>
      <td>11%</td>
      <td>jan 18, 2007 9:12 am</td>
    </tr>
  </tbody>
</table>

Apply a New Widget "applyWidgetId"

Apply a widget to the table. In this example, we apply the zebra striping to the table after it has been initialized.
$(function(){
  // initialize tablesorter without the widget
  $("table").tablesorter();
  
  // click a button to apply the zebra striping
  $("button").click(function(){
    $('table').trigger('applyWidgetId', ['zebra']);
});

Apply Chosen Widgets "applyWidgets"

Reapply a widget to the table. In this example, we apply the zebra striping to the table after it has been initialized.

This is basically an alternate method to the "applyWidgetId", but in the example below you can add or remove widgets from the chosen list.
// Update the list of widgets to apply to the table (add or remove)
$("table").data("tablesorter").widgets = ["zebra"];
$("table").trigger('applyWidgets');




Triggered Events

sortStartThis event fires when the tablesorter plugin is about to start resorting the table.
sortEndThis event fires when the tablesorter plugin has completed resorting the table.


Bind to the "sortStart" and/or the "sortEnd" events as shown on the example page
$(function(){
  // initialize the tablesorter plugin
  $("table").tablesorter();
  // bind to sort events
  $("table")
    .bind("sortStart",function() {
      $("#overlay").show();
    })
    .bind("sortEnd",function() {
      $("#overlay").hide();
    });
});
Or here is another example from StackOverflow
$(function(){
  $("table")
    .tablesorter()  
    .bind("sortEnd", function(){  
      $("table tr").removeClass("block");
      // adds the "block" class to every 8th row
      $("table tr:nth-child(8)").addClass("block");
    });
});

Saturday, April 23, 2011

Visual Event Bookmarklet

I've been using Visual Event bookmarklet to help quickly visualize which events are attached to a page while troubleshooting problems.

I do have one problem with the script. It adds too many layers so you can't get to the lower layers. Here is a screen shot of how the last post I made looks with Visual Event (on the left) and the second screen shot is how it looks after the script is applied (on the right):

I made a github gist and a jsFiddle demo of this script. To use it, just add the following script into the Firebug console to enable the script:
var veColors = [ 'black', 'orange', 'purple', 'green', 'blue', 'yellow', 'red' ],
 veColorLength= veColors.length - 1,
 veLayerIndex = 0;
 
function showVeLayer(nxt){
 veLayerIndex += (nxt) ? 1 : -1;
 if (veLayerIndex > veColorLength) { veLayerIndex = 0; }
 if (veLayerIndex < 0) { veLayerIndex = veColorLength; }

 var veLayer =  $('.Event_bg_' + veColors[veLayerIndex]);
 if (veLayer.length === 0 ) { showVeLayer(nxt); return; }

 $('.Event_bg_' + veColors.join(', .Event_bg_')).hide();
 veLayer.show();
};

$(document).keyup(function(e){
 switch(e.which){
  case 39: case 40: // right/down
   showVeLayer(true);
   break;
  case 37: case 38: // left/up
   showVeLayer();
   break;
 }
});

Tuesday, December 21, 2010

Keycaster

Keycaster is a jQuery plugin was written to add a keystroke and mouse visualizer to your browser window. It is similar to keycastr for the mac, except this only works inside the browser window.


This plugin is still in the alpha stage because it doesn't show all shifted keys properly and some key symbols do not work in all browsers (written mainly to work in Firefox). Also I plan on making this script work as a bookmarklet so you can just click on a link to activate it.

Update: You can now add this script to any page using this bookmarklet - Keycaster - just drag it into your browser bookmarks and click on it whenever you need to visualize your mouse or keyboard actions.

Tuesday, August 24, 2010

Tooltips - Jatt (Just another tooltip)

This is an updated version of my other tooltip script. With this version, you can:
  • Dynamically modify the tooltip using metadata.
  • Set a tooltip direction (8 directions: n, ne, e, se, s, sw, w & nw).
  • Tooltip content can be obtained from a selected object attribute, a different object on the same page, or via ajax.
  • Ajax calls can include jquery selectors to target specific page content.
  • Removed support for the dhtmltooltip script (the code is there, but commented out).
Adding the Tooltips
First make sure you have jQuery installed on your site, download the latest version from jQuery. Then add the following CSS and script to your site, or you can download the script here.

Basic setup
<style type="text/css">
.tooltip, .preview, .screenshot { cursor:pointer; }
#tooltip { width: 250px; }
#tooltip, #preview {
 color:#dddddd;
 background:#222222;
 border: 1px solid #333333;
 padding:5px;
 opacity: 0.9;
 filter: alpha(opacity=90);
 text-align:left;
 border-radius: 1em;
 -moz-border-radius: 1em;
 -webkit-border-radius: 1em;
 display:none;
}
</style>
<script src="jatt.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
 $.jatt();
});
</script>
Using the Tooltips
  • Basic Tooltip

    basic tip (a)<a class="tooltip { direction: n; width: 100px; background: #222; color: #ddd; }" href="#" title="Tooltip (a) Content">basic tip (a))</a>
    External page (a)<a class="tooltip { direction: ne; width: 200px; }" href="http://wowmotty.blogspot.com/2009/06/new-and-improved-jquery-tooltips.html #Blog1_cmt-3390748906554490641">External page (a)</span>
    Object on page (div)
    <div class="tooltip { direction: e; width: 150px; background: #9bff8f; color: #333; }" rel="#tip1">Object on page (div)</div>
    <div id="tip1" style="display:none">Hi</div>
    <img class="tooltip { direction: se; width: 200px; background: #808080; color: #000; opacity: 1; }" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJB6BDlv3S2lwtKXWg7geoqgt0S-d-efE1yluu0NikMlLfjVEFMiq4N_wlTHkPItSGDbT2xPhgvUnIwGsTI0zj9BtKGN8elCL2GTMaRX0NHE-jfjSRtm0sCP_ja9ih3snjzxFNgpuheKA/s320/hideshow.gif" title="Tooltip (img) Content" >
    Link (span)<span class="tooltip { followMouse: false; direction: s; width: 300px; background: #008080; color: #ddd; }" title="Tooltip (span) Content">Link (span)</span>
    • class="tooltip" (required) - This class activates the tooltip
    • class="tooltip { metadata }" (optional) - Add any of the predefined options (listed below) or any css style to apply to the tooltip.
    • title="Tooltip content" (optional) - The title attribute should contain whatever you want to display inside the tooltip. This can include HTML but Do NOT use quotes in the content. If you are using HTML tags, replace the quotes with single quotes, e.g. <img src='hideshow.gif'>. If you must use quotes to surround some text, use the HTML escape code &quot; - this text has &quot;quotes&quot; around it.
    • rel="selector" (optional) - If the "title" attribute is empty, the script will look in the "rel" attribute for a selector that points to an object containing the tooltip content (see the "External object (div)" example above).
    • href="page.htm #target span:first" (optional) - If the "title" and "rel" attributes are both empty, the script will look in the "href" attribute for a URL to an external page. The URL can be followed by an id or class selectors pointing to the tooltip contents (see the "External page (a)" example above).
  • Preview Tooltips

    <a class="preview { direction: e; opacity: 1; text-align: center; }" href="URL to full size image" title="Google's Logo"><img src="URL to image thumbnail"></a>
    • class="preview" (required) - This class activates the tooltip which puts the linked image (from the href) into a tooltip. The tooltip size is adjusted automatically to fit the image.
    • class="preview { metadata }" (optional) - Add any of the predefined options (listed below) or any css style to apply to the tooltip.
    • href="URL to full size image" (required) - URL to the image, for previewing in the tooltip and the URL you go to when you click on the image.
    • title="TOOLTIP CONTENT" (optional) - This content becomes the image caption located below the image inside the tooltip. Note: Do NOT use quotes in the content. If you are using HTML tags, replace the quotes with single quotes, e.g. <img src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdOsFaiNIiNqDl5RhAACLxqpiShH9pnrYBFSZqPUpn2hzCiUT8yo99KC8xKqcQMwKywNN6S1jq9bjJRZ-YULgjR-lYdf10O0-dtosiT7UfhBXJnwRuVIKgDZuc0bgkOf4tUoRag0Pp3SA/s320/Nax2-10.gif'>. If you must use quotes to surround some text, use the HTML escape code &quot; - this text has &quot;quotes&quot; around it.
    • <img src="URL to image thumbnail"> (required) - This will be the thumbnail of the URL image (in orange), or you can pick any image as I did here. Or you can replace the <img> with text.
  • Screenshot Tooltips

    LMYC<a class="screenshot { direction: e; opacity: 1; text-align: center; } " href="http://www.guildportal.com/Guild.aspx?GuildID=194525&TabID=1643295" title="<center>Loch Modan Yacht Club</center>" rel="http://www.axiomfiles.com/Images/Spotlight/194525.jpg">LMYC</a>
    Google<a class="screenshot { direction: e; }" href="http://www.google.com" title="<center>Google</center>" rel="#">Google</a>
    • class="screenshot" (required) - This class activates the tooltip that gets the site screenshot image from the rel attribute (in light blue)
    • href="URL" (required) - URL of the target website
    • title="Tooltip content" (optional) - This content becomes the screenshot (image) caption inside the tooltip. Note: Do NOT use quotes in the content. If you are using HTML tags, replace the quotes with single quotes, e.g. <img src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdOsFaiNIiNqDl5RhAACLxqpiShH9pnrYBFSZqPUpn2hzCiUT8yo99KC8xKqcQMwKywNN6S1jq9bjJRZ-YULgjR-lYdf10O0-dtosiT7UfhBXJnwRuVIKgDZuc0bgkOf4tUoRag0Pp3SA/s320/Nax2-10.gif'>. If you must use quotes to surround some text, use the HTML escape code &quot; - this text has &quot;quotes&quot; around it.
    • rel="Website_Screenshot.jpg" or rel="#" (required) This attribute can contain either the URL to the screenshot image of the site to which you have the link pointing. Yes, you will have to take, resize and upload the screenshot image yourself OR you can now use rel="#" to have Websnapr.com generate a thumbnail of the site for you.
    • text or image (required) - Text or image that becomes the clickable link to the site.
Customizing
  • Initializing the script (showing all default settings)

    $(document).ready(function(){
     $.jatt({
      // options that can be modified by metadata
      direction      : 'n',     // direction of tooltip
      followMouse    : true,    // tooltip follows mouse movement
      content        : 'title', // attribute containing tooltip text
      speed          : 300,     // tooltip fadein speed
      local          : false,   // if true, the script attachs the tooltip locally; if false, the tooltip is added to the body
      xOffset        : 20,      // x distance from mouse (no negative values)
      yOffset        : 20,      // y distance from mouse (no negative values)
      zIndex         : 1000,    // z-index of tooltip
    
      // options not supported by metadata
      live           : false,                 // use live event support?
      metadata       : 'class',               // attribute that contains the metadata, use "false" (no quotes) to disable the metadata.
      activate       : 'mouseenter focusin',  // how tooltip is activated
      deactivate     : 'mouseleave focusout', // how tooltip is deactivated
    
      // change tooltip, screenshot and preview class
      tooltip        : '.tooltip',             // tooltip class
      screenshot     : 'a.screenshot',        // screenshot class
      preview        : 'a.preview',           // preview class
    
      // tooltip & preview ID (div that contains the tooltip)
      tooltipId      : 'tooltip',             // ID of actual tooltip
      previewId      : 'preview'              // ID of screenshot/preview tooltip 
     });
    });
  • List of Options


    KeyValue (default shown)Description
    direction'n'Choose the direction of the tooltip (in quotes; choose from n, ne, e, se, s, sw, w, nw).
    followMousetrueWhen hovering over the object with a tooltip, the tooltip will follow the mouse if true. If false, it will be positioned relative to the object.
    content'title'Attribute containing the tooltip content. By default it is the "title" attribute.
    speed300Time in milliseconds for the tooltip to fade in. By design the tooltip has no fade out because it is removed immediately.
    localfalseWhere the tooltip is attached. By default it is false and will attach to the document body; if true, it will be attached before the object.
    xOffset20Number of pixels between the tooltip and the mouse (or object if followMouse = false) in the X direction.
    yOffset20Number of pixels between the tooltip and the mouse (or object if followMouse = false) in the Y direction.
    zIndex1000Adjust this if the tooltip is below another object.
    livefalseSet to true if you add tooltip objects to your page dynamically (added after the page is loaded).
    metadata'class'Location of the tooltip metadata. By default it will look in the class attribute. If set to "false" it will disable the metadata.
    activate'mouseenter focusin'The event that causes the tooltip to display information.
    deactivate'mouseleave focusout'The event that causes the tooltip to be removed.
    tooltip'.tooltip'The class used to activate the tooltip. And the ID of the actual tooltip.
    screenshot'a.screenshot'The class used to activate the screenshot script which shows an image associated with the URL. And this is the ID of both the screenshot & preview tooltips.
    preview'a.preview'The class used to activate the preview script which shows the an image in the tooltip from the href.
    tooltipId'tooltip'The ID of the actual tooltip.
    previewId'preview'The ID of the actual preview/screenshot tooltip (the share the same one).
Known Issues
  • Fixed:If the tooltip contents go out of the viewport, the script attempt to adjust the tooltip to make it visible. If it moves the tooltip under the mouse, the tooltip will flicker and become unreadable. This occurs when the east or west direction is used, so limit use of 'east' when the tooltip is near the right side and 'west' when it is near the left. I'll look into fixing this problem in future versions.
  • The tooltip script won't obtain cross-domain ajax requests by default, but I have included James Padolsey's cross-domain ajax script (jquery.xdomainajax.js) which should enable it. Just include the script in the head of your document.
  • If you find any other issues, please submit an issue to Jatt's github repository.

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.

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