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.