Category Archives: JavaScript

Creating Slideshows with iScroll 5

05 Jul 2015

iScroll has features that make it useful beyond just scrolling – among many uses it can be used to create slide shows. I created one today at work and am sharing the entire process here. I already had certain parts of this done for other parts of my application so it took me about 10 minutes to get everything wired up. Since this example starts from ground zero it will take a little longer.

Start with your layout

This demo will have four slides and we’ll need them in an unordered list, so we will start with this:

<ul>
	<li><img src="images/image_1.jpg" height="177" width="284" /></li>
	<li><img src="images/image_2.jpg" height="177" width="284" /></li>
	<li><img src="images/image_3.jpg" height="177" width="284" /></li>
	<li><img src="images/image_4.jpg" height="177" width="284" /></li>
</ul>

iFramed below is a page with the above code:

Ok, well, thats a start. We’ll be making a horizontal scroller so we need to style this list into a horizontal line using this CSS:

li {
	list-style-type: none;
	margin: 0px;
	padding: 0px;
	float:left;
}
ul {
	width: 1136px;
	margin: 0px;
	padding:0px;
}

A couple things to note:

  • Notice the UL width – since I’m essentially using it as the “wrapper” for the LI elements I need to set the width to equal the total width of all of the slides so that the list items can layout out in the desired manner.
  • I’m floating the list elements instead of setting their display to “inline-block” to be friendlier with old IE

So then, below is the result (scrolling in the iFramed example is on, thus the horizontal scroll bar):

Setup for iScroll

Next we need to setup everything for iScroll which means that we need to create a wrapper around our horizontal thumbnail list and then style it appropriately. Note these three things:

  1. As mentioned I’ve set the width of the UL element so that its width equals the total width of all of the slides since it is the element that will be scrolled ***inside*** of the wrapping div.
  2. The wrapping div has an id of “slideshow” and is styled such that its “viewport” is the same dimensions as a single slide. Note also that it’s overflow is set to “hidden”. If the UL element didn’t have its width set the wrapping div would have forced each slide to stack which is not what we want – we want the slides to lay in a single row.
  3. Within the CSS note the position:relative lines on both the UL and the #slideshow – these are necessary so that iScroll can do the calculations needed to get everything to work. If you forget about this your iScroll wont work at all.
<head>
<style type="text/css">
	li {
		list-style-type: none;
		margin: 0px;
		padding: 0px;
		float:left;
	}
	
	ul {
                position: relative;
		width: 1136px;
		margin: 0px;
		padding:0px;
	}
	
	#slideshow{
		position: relative;
		height: 177px;
		width: 284px;
		overflow: hidden;
	}
</style>

</head>
<body>
	<div id="slideshow">
		<ul>
			<li><img src="images/image_1.jpg" height="177" width="284" /></li>
			<li><img src="images/image_2.jpg" height="177" width="284" /></li>
			<li><img src="images/image_3.jpg" height="177" width="284" /></li>
			<li><img src="images/image_4.jpg" height="177" width="284" /></li>
		</ul>
	</div>
</body>

The following is the result:

Add iScroll

Unlike prior versions iScroll 5 has its own website from which you can download the library at http://iscrolljs.com/. Below is an example of adding it to a page.

<script type="text/javascript" src="js/iscroll.js"></script>

Next we need to initialize the iScroll on our “slideshow” wrapper. We can do so once the page has been loaded like so (jQuery users can substitute jQuery’s document.ready instead). More information on initializing iScroll can be found here.

That’s it – we currently have a functional iScroll – not yet a slideshow, but we’re getting there. Below is the javascript that made it happen followed by the iFramed example.

Of note here is the iScroll configuration object found on line 7. The scrollX property’s “true” value tells iScroll to scroll horizontally.

<script type="text/javascript" src="js/iscroll.js"></script>
<script type="text/javascript">
  ;(function(ns){
	var _iscroll = null;
	ns.init = function(){
		_iscroll = new IScroll('#slideshow',{
			scrollX: true
		});
	}
  })(this.slideshow = {});
  (function(){
	if(window.addEventListener){
		window.addEventListener('load',slideshow.init,false);
	} else{
		window.attachEvent('onload',slideshow.init); //IE
	}
  })();
</script>

Swipe with your finger or your mouse to see the iScroll work.

Configuring iScroll

Ok, now we start getting into the things that will make our slideshow really work. First, lets make iScroll behave a little differently, lets make it snap to each slide instead of scrolling past slides when you swipe. iScroll has 2 properties that we can pass via a configuration object that will tell it to a) snap to an element and b) not overscroll past the element. They are snap and momentum where the former’s value will be set to li since we are snapping to those elements and for the later we’ll use a value of false as we want to disable overswiping.

The adjusted script:

<script type="text/javascript" src="js/iscroll.js"></script>
<script type="text/javascript">
  ;(function(ns){
	var _iscroll = null;
	ns.init = function(){
		_iscroll = new IScroll('#slideshow',{
			scrollX: true
			snap:'li',
			momentum:false
		});
	}
  })(this.slideshow = {});
  (function(){
	if(window.addEventListener){
		window.addEventListener('load',slideshow.init,false);
	} else{
		window.attachEvent('onload',slideshow.init); //IE
	}
  })();
</script>

Give it a try with these new settings:

SPECIAL NOTE: All these examples are iFramed – so if you do a click-drag with a mouse and drag outside of the iFrame then things get kind of weird. This is normal. The explanation is that since you click-dragged outside of the iFrame itself the window within the iFrame does not know that you released the mouse button. Remember: This is a tutorial with “tightly iFramed” examples. To properly experience these examples try to not be so dramatic with your click-swipes 😉

Add next / previous buttons

Slideshows typically have back and next buttons. Lets add them to our slideshow – we’ll start with some basic layout, we’ll have a left and right button on top of the slide show. Each button is a div and will be positioned over the slideshow on the left and right edges.

Here’s the CSS that accomplishes this followed by the HTML:

	#btnleft, #btnright {
		position:absolute;
		top: 70px;
		height: 40px;
		width: 36px;
		z-index: 100;
		background-size:contain;
	}
	#btnleft{
		background-image: url(images/slideshow_left_arrow.png);
		left: 8px;
	}
	#btnright{
		background-image: url(images/slideshow_right_arrow.png);
		left: 255px;;
	}

And the new html – the button divs are added after the slideshow wrapper:

	<div id="slideshow">
		<ul>
			<li><img src="images/image_1.jpg" height="177" width="284" /></li>
			<li><img src="images/image_2.jpg" height="177" width="284" /></li>
			<li><img src="images/image_3.jpg" height="177" width="284" /></li>
			<li><img src="images/image_4.jpg" height="177" width="284" /></li>
		</ul>
	</div>
	<div id="btnleft"></div>
	<div id="btnright"></div>

This is the result:

Add event handlers to the buttons

You should have noticed by now that I’ve made no mention of jQuery thus far and in fact avoided it for the onload event that inits our iScroll. I didn’t want to use jQuery since it would mean using a big fat library to do trivial things. I know everyone likes jQuery so I’ll make a jQuery version that you can download at the end of this article. In the meantime your take-aways should be how to use iScroll not necessarily how to use jQuery for event delegation or whatever. That said, there are more iSrcoll things to learn which we will get to in a moment.

Back on topic, we need to wire up the “back” and “next” buttons. I’ll first wrap everything in a div and then apply the listeners to that wrapping element. The following code does this for us, click the example that follows the code sample to see it work.

The updated init function:

	ns.init = function(){
		_iscroll = new IScroll('#slideshow',{
			scrollX: true,
			snap:'li',
			momentum:false
		});
		// add event listeners
		if (window.addEventListener){
			document.getElementById('slideshow_wrapper').addEventListener('click',ns.doNav,false);
		} else{
			window.attachEvent('onload',slideshow.init); //IE
		}
	};
	ns.doNav = function(e){
		alert('clicked ' + e.target.id);
	};

And the updated HTML with a DIV element with id “slideshow_wrapper” wrapping everything:

	<div id="slideshow_wrapper">
		<div id="slideshow">
			<ul>
				<li><img src="images/image_1.jpg" height="177" width="284" /></li>
				<li><img src="images/image_2.jpg" height="177" width="284" /></li>
				<li><img src="images/image_3.jpg" height="177" width="284" /></li>
				<li><img src="images/image_4.jpg" height="177" width="284" /></li>
			</ul>
		</div>
		<div id="btnleft"></div>
		<div id="btnright"></div>
	</div>

Click the arrows….

What I’ve done is set things to capture the click events as they bubble up the DOM to the event listeners (a technique referred to as event delegation).

Add logic to the navigation buttons

Our buttons will advance the slideshow either forward or backward. To do this we need to be able to determine the direction the user wants to go. If you clicked the navigation buttons in the above example you can see that the alert contained the id of the clicked element, so I’ll use that to determine which button was clicked. Below is the modified doNav function that keys in on the id’s of our buttons:

    ns.doNav = function(e){
	if (e.target.id === 'btnleft'){
	    // move the sideshow to the left
	} else {
	    // move the slideshow to the right
	}
    };

The next thing we need is a way of advancing the slideshow in one direction or another. iScroll provides a method for this purpose called next() to advance the slideshow forward and prev() to advance the slideshow to a previous slide. There is also goToPage() but I’ll stick with the previous two methods – you can of course opt to use the later method to take advantage of the easing property that it exposes.

This is all we need to get the navigation buttons working. See the code below followed by the working example.

    ns.doNav = function(e){
	if (e.target.id === 'btnleft'){
	    iscroll.prev(); // scroll left
	} else {
	    iscroll.next(); // scroll right
	}
    };

Dimming the navigation buttons

One thing that you might think is missing is disabling the buttons in some way to reflect that you are either at the beginning or end of the slideshow. By digging into the iScroll object we can find some properties that will make it easy for us to do this.

First we will need to know what the current slide number is. That information is held in the iScroll object that can be referenced via our private _iscroll variable. Log your iScroll object to the console and note the currentPage property. Expand it and you will see the following:

currentPage:{
    pageX:0, // this holds the slide number for horizontal iScrolls
    pageY:0, // this holds the slide number for vertical iScrolls
    x:-0,
    y:0
}

As you can see the pageX property holds the slide number for horizontal scrollers. Given that fact we would access the current slide number by doing this:

var slideNumber = _iscroll.currentPage.pageX;

Next we need to know how many slides there are. Of course we know that there are 4 but iScroll also holds this for us in the iScroll object’s pages property. Lets save it for later use:

var slideLength = _iscroll.pages.length;

Below is a function that dims the navigation buttons when they are no longer usable due to the slideshow being either at the beginning or end of the slide list (if you are using jQuery you will want to add or remove css classes to the elements instead of the in-line editing that I’m doing):

    // show or dim the navigation buttons
    function _hideShowNavButtons(e){
	var slideNumber = _iscroll.currentPage.pageX;
	var slideLength = _iscroll.pages.length - 1;
	// show or hide the next and previous buttons according to where we are in the slideshow
	document.getElementById('btnleft').style.opacity = slideNumber === 0 ? .3 : 1;
	document.getElementById('btnright').style.opacity = slideNumber === slideLength ? .3 : 1;
    }

To make the above work we need to fire it when the slideshow has stopped scrolling – thus we’ll hook into iScroll’s scrollEnd event. We’ll do this within the init() function which has been modified below:

    // init the app
    ns.init = function(){
	_iscroll = new IScroll('#slideshow',{
	    scrollX: true,
	    snap:'li',
	    momentum:false
	});
		
	// add scrollend event to iscroll
	_iscroll.on('scrollEnd',_hideShowNavButtons); // NEW LINE!! dim or show the buttons on "scrollEnd"
		
	// add event listeners
	if (window.addEventListener){
	    document.getElementById('slideshow_wrapper').addEventListener('click',ns.doNav,false);
	} else {
	    window.attachEvent('onload',slideshow.init); //IE
	}
    };

Lastly, we need to code the “Previous” button so that it is dimmed to begin with. I hardcode that into the init() function via this line (as before use jQuery or your favorite library instead as needed):

// set the default state for the left/previous button
document.getElementById('btnleft').style.opacity = .3;

Here is the result:

We’re done! You could of course add a lot more features such as notes or slide numbering, etc. Right-click and inspect the iFrame holding the final example above or see the complete source printed out below.

<!DOCTYPE html>
<html><head><title></title>

<style type="text/css">
    li {
	list-style-type: none;
	margin: 0px;
	padding: 0px;
	float:left;
    }
	
    ul {
	position: relative;
	height: 177px;
	width: 1136px;
	margin: 0px;
	padding:0px;
    }
	
    #slideshow{
	position: relative;
	height: 177px;
	width: 284px;
	overflow: hidden;
    }
    #btnleft, #btnright {
	position:absolute;
	top: 70px;
	height: 40px;
	width: 36px;
	z-index: 100;
	background-size:contain;
    }
    #btnleft{
	background-image: url(images/slideshow_left_arrow.png);
	left: 8px;
    }
    #btnright{
	background-image: url(images/slideshow_right_arrow.png);
	left: 255px;
    }
</style>

<script type="text/javascript" src="js/iscroll.js"></script>
<script type="text/javascript">
;(function(ns){
    // hold a reference to the iScroll object
    var _iscroll = null;
	
    // init the app
    ns.init = function(){
	_iscroll = new IScroll('#slideshow',{
	    scrollX: true,
	    snap:'li',
	    momentum:false
        });
		
        // add scrollend event to iscroll
        _iscroll.on('scrollEnd',_hideShowNavButtons);
		
	// set the default state for the left/previous button
	document.getElementById('btnleft').style.opacity = .3;
		
	// add event listeners
	if (window.addEventListener){
	    document.getElementById('slideshow_wrapper').addEventListener('click',ns.doNav,false);
	} else {
	    window.attachEvent('onload',slideshow.init); //IE
	}
    };
	
    // handle navigation button clicks
    ns.doNav = function(e){
	if (e.target.id === 'btnleft'){
	    _iscroll.prev(); // scroll left
	} else {
	    _iscroll.next(); // scroll right
	}
    };
	
    // show or hide the navigation buttons
    function _hideShowNavButtons(e){
	var slideNumber = _iscroll.currentPage.pageX;
	var slideLength = _iscroll.pages.length - 1;
	// show or hide the next and previous buttons according to where we are in the slideshow
	document.getElementById('btnleft').style.opacity = slideNumber === 0 ? .3 : 1;
	document.getElementById('btnright').style.opacity = slideNumber === slideLength ? .3 : 1;
    }
	
})(this.slideshow = {});

;(function(){
    if (window.addEventListener){
	window.addEventListener('load',slideshow.init,false);
    } else {
	window.attachEvent('onload',slideshow.init); //IE
    }
})();
</script>
</head>
<body>
    <div id="slideshow_wrapper">
	<div id="slideshow">
	    <ul>
		<li><img src="images/image_1.jpg" height="177" width="284" /></li>
		<li><img src="images/image_2.jpg" height="177" width="284" /></li>
		<li><img src="images/image_3.jpg" height="177" width="284" /></li>
		<li><img src="images/image_4.jpg" height="177" width="284" /></li>
	    </ul>
	</div>
	<div id="btnleft"></div>
	<div id="btnright"></div>
    </div>
</body>
</html>

Creating Image Maps From Canvas-Derived Coordinates

10 Jun 2015

Here’s a cool thing – I came across a situation where I was stacking identically-sized transparent PNG’s on top of each other but needed to be able to select their visible areas. The layered nature of z-ordering the images prevented us from getting beyond the top-most layer (actually, not image tags but z-ordered divs with background-images, but for all intents and purposes its the same issue).

The first thought was to use HTML5 Canvas and then track the coordinate of the users click/touch event to figure out what they were clicking on. A nice start but our browser requirements included old IE which prevented us from using Canvas so we were stuck with the PNG stack. In addition to dealing with old IE the requirements of the project meant also that we couldn’t simply merge all the PNG’s and then simply create an image map because:

  • Our client would be uploading the UI-related PNG’s into the system – both on and off states
  • We couldn’t rely on the client to be smart enough to draw image maps within the admin console each time an image was uploaded, especially since some of the art bumped or “went under” some of the other art within the PNG stack.

I thought we could still use Canvas if we applied it to the process of uploading the images – knowing that we essentially had small bitmaps “floating” within a larger transparent PNG I realized that we might be able to get some useful data from a Canvas, maybe enough to determine what should be clickable and what shouldn’t.

A quick Google search revealed this post at stackoverflow. It describes a “Marching Squares Edge Detection” algorithm that when applied to my needs would give me an array of coordinates that could be easily converted into an image map.

Detecting Multiple Edges

Looking at the edge detection algorithm showed that it detects the edge for the first shape that it encounters, ignoring anything else in the Canvas even though other shapes may exist. The fix here was to remove the shape that it finds and then run the algorithm again, repeating the find/remove process until nothing else is found. This is done while saving the boundary of each shape so that when the entire process was done we could use that data to create the image maps.

As for removing the first shape that was encountered I wrote the following – it draws a shape on the canvas exactly where the found shape is using that shape’s boundary and then overwrites it with a fill. Since I set the Canvas globalCompositeOperation to destination-out the end result is that the shape is removed from the canvas thus allowing me to find the next shape’s boundary as the previous shape no longer exists.

    function _removeBitmap(){
        var i, len;
	// draw outline path
	_ctx.globalCompositeOperation = 'destination-out';
	_ctx.beginPath();
	_ctx.moveTo(_points[0][0],_points[0][1]);
	for (i=1,len=_points.length;i<len;i++){
    	    var point = _points[i];
	    _ctx.lineTo(point[0],point[1]);
	}
	_ctx.closePath();
	_ctx.fill();
	_ctx.globalCompositeOperation = 'source-over';
    }

Detecting if the Canvas is Empty

Next, before I call the edge detection function again I first need to know if the Canvas is empty – so here’as another function that looks to see if the alpha of any pixel is set below a certain threshold. Why a threshold? Well, it seems that even though I can’t see the bitmaps that I removed with the destination-out composite operation that there may still be pixels here and there that do exist though are effectively invisible. A threshold settles that particular issue, you may need to play with it on your own if you use this code.

    ns.isEmpty = function(){
        var data = _ctx.getImageData(0,0,_canvas.width,_canvas.height).data;
        var emptyThreshold = 20; // maximum allowed alpha before a pixel is considered "empty"
        var i, l;
        var retVal = true;
        // maxAlpha: what is the highest alpha? most times its not zero.
        // log this to the console to see what the max alpha is, 
        // then set "emptyThreshold" accordingly.
        var maxAlpha = 0;  
	for (i=0,l=data.length; i < l; i += 4){
            // for debugging purposes
            maxAlpha = data[i + 3] > maxAlpha ? data[i + 3] : maxAlpha; 
		if(data[i + 3] > emptyThreshold){
                retVal = false;
	    }
	}
        return retVal;
    }

Yes, I know about loading up a blank canvas, getting its base64 via toDataURL and then trying to compare against it to see if a Canvas is empty – but note again how the composite operation left some pixels behind which means that comparing against a truly blank canvas wouldn’t work.

Working Example

Here’s a working example – inspect the iFramed page and note the absence of any image maps, then click “Start”. What you will see is:

  • Each image is loaded into the Canvas
  • The edge detection script finds the first “floating” shape, I remove it and then run the edge detection again, repeating until i determine that the image is now empty of any “solid” shapes
  • The next image is loaded and the process repeats
  • Once all edges have been found the image map is added to the DOM

NOTE: click start and let the sample run through all of the edge detection for everything (slower on mobile as all of the images load synchronously). It will be done when all the pieces display. From there you can click the other buttons.

You will note that the Canvas is still in this proof-of-concept after all of the edge detection is completed and the image maps are added – the final implementation of this stacks all images via absolute positioning without any Canvas elements. The top-most image of the stack has the image map applied to it. In this way we are able to automate the creation of the image maps within the browser when each image is uploaded into the system via the purpose-built CMS and not need to worry about the client using some sort of drawing tool to create the image maps themselves.

Finished Code

Here’s the result – separated from the edge detection stuff which I broke out into its own file that you can download from here while viewing the source of the example to learn how the parts were assembled.

;(function(ns,$){

    var _canvas, _ctx, _cw, _points, _imgData, _data;
    var _allPaths = [];
    var _img = new Image();
	_img.crossOrigin = 'anonymous';
	_img.onload = _drawImgToCanvas;

    function _drawImgToCanvas(){
	_ctx.drawImage(_img,_canvas.width/2-_img.width/2,_canvas.height/2-_img.height/2);
        _findArea();
    }

    function _findArea(){
        _imgData = _ctx.getImageData(0,0,_canvas.width,_canvas.height);
        _data = _imgData.data;
        _points = marchingSquares.contour();// call the marching ants algorithm
	_allPaths[_allPaths.length] = _points;// store the area in the _allPaths collection
	_removeBitmap();// remove the shape so we can move on to the next one
    }

    function _removeBitmap(){
        var i, len;
	// draw outline path
	_ctx.globalCompositeOperation = 'destination-out';
	_ctx.beginPath();
	_ctx.moveTo(_points[0][0],_points[0][1]);
	for (i=1,len=_points.length;i<len;i++){
	    var point = _points[i];
	    _ctx.lineTo(point[0],point[1]);
	}
	_ctx.closePath();
	_ctx.fill();
	_ctx.globalCompositeOperation = 'source-over';
        
        if (ns.isEmpty()){
            _createMap();
        } else {
            _findArea();
        }
    }
	
    ns.isEmpty = function(){
	var data = _ctx.getImageData(0,0,_canvas.width,_canvas.height).data;
        var emptyThreshold = 20; // maximum allowed alpha before a pixel is considered "empty"
        var i, l;
        var retVal = true;
        var maxAlpha = 0; // what is the highest alpha? most times its not zero. log this to console to see what the max alpha is, set "emptyThreshold" accordingly.
	for (i=0,l=data.length; i < l; i += 4){
            maxAlpha = data[i + 3] > maxAlpha ? data[i + 3] : maxAlpha; // for debugging purposes
	    if(data[i + 3] > emptyThreshold){
                retVal = false;
	    }
	}
        return retVal;
    }
	
    function _createMap(){
	var mapTPL = '%areas%';
	var areasTpl = '';
	var areas = '';
	var map = '';
	var coordsList = '';
	for (var h=0,len=_allPaths.length;h<len;h++){
 	    coordsList = '';
	    for (var i=0,len2=_allPaths[h].length;i<len2;i++){
		coordsList += _allPaths[h][i].join(',');
		coordsList += i != _allPaths[h].length -1 ? ',' : '';
	    }
	    areas += areasTpl.replace('%coords%',coordsList);
	}
	map = mapTPL.replace('%areas%',areas);
	$('#mapWrapper').html(map);
    }
    
    ns.returnData = function(){
	return _data;
    }
	
    ns.returnCW = function(){
	return _cw;
    }
	
    ns.init = function(canvasID,imgSrc){
	_canvas = document.getElementById(canvasID);
	_ctx = _canvas.getContext('2d');
	_cw = _canvas.width;
	_img.src = imgSrc;
    }
	
})(this.mapFromCanvas = this.mapFromCanvas || {}, jQuery);

Passing Objects without Reference

30 Apr 2015

Here’s a little thing that I do to prevent passing object references when I really just want a copy of the object itself (a deep copy). This is super simple – much simpler than enumerating an object in order to reveal/copy its properties and values into a new object.

Here is an example of a closure that has a private variable called _data. There is a getter function called getData() that is meant to return the value of _data.

;(function(ns){

    var _data = {title:'test data'};
    
    ns.getData = function(){
        return _data;
    }

})(this.test = this.test || {});

Pasting the above in Chrome’s console you can see what happens when I create the variable dog to receive _data. I attempt to change the value of dog.title and see that it changes but when I view _data via the getter function I find that _data has changed even though it is encapsulated within the “test” namespace.

obj_by_ref_01

JavaScript passes objects around by reference, not by value, so what happened above is not a surprise. In layman’s terms dog maintains a reference back to the object _data which means that dog.title is in reality _data.title. Therefore modifying one is really modifying the other. Our intent is to pass the object as if it were a value without any reference back to where it came from.

What I need then is a way to break the reference chain so that I can receive the desired object as a “value” (kind of) instead of as a reference. I do that by stringifying the object into JSON and then parsing the JSON back into an object. As soon as the object is translated into JSON the reference no longer exists. Converting it back to an object via JSON.parse() gives me the desired “reference-free” deep copy of _data.

Here’s the same code with the JSON methods added:

;(function(ns){

    var _data = {title:'test data'};
    
    ns.getData = function(){
        return JSON.parse(JSON.stringify(_data));
    }

})(this.test = this.test || {});

And doing the exact same thing in Chrome’s console reveals that _data is untouched whenever dog.title is changed.

obj_by_ref_02

Preventing Webview Bounce and Keyboard Layout Shift

11 Apr 2015

Using libraries over frameworks means that there are few things that you have to do yourself. Preventing webview bounce – as can be seen in mobile Safari – is one of them. This is especially true since you are most likely using a scrolling library such as iScroll. Webview-bounce and iScroll don’t play well together. This issue turns out to be an easy one to solve. simply add this to your project:

document.ontouchmove = function(e){e.preventDefault()};

The next irritation for us web app developers is how the keyboard moves the screen around when it appears. It will move your layout to ensure that the focused input element is visible. The caveat is that iOS will not return the layout to its previous position if the user touches the layout to put away the keyboard instead of tapping the “hide keyboard” button. Even the “hide keyboard” button may not return the view to normal.

This is also easily solved, add this to your project:

var repositioner = null;
$('body').on('blur','input,area',function(){
  repositioner = setTimeout(function(){window.scrollTo(0,0;},200);
});
$('body').on('focus','input,area',function(){
  clearTimeout(repositioner);
});

Why the setTimeout? Well, lets say we only set a listener for the blur event. When an input element gains focus iOS will display the keyboard and shift the entire view up. With the keyboard still visible you could touch and thereby give focus to other input elements. But, giving focus to a different element blurs the previously focused element. This causes the view to bounce up on the blur event and then back down on the focus per what iOS feels like doing. Basically, the view bounces up and down as you touch form elements (this wont happen if you use the iOS keyboard’s built-in input tabbing).

By giving mobile Safari a 200 milisecond timeout we prevent the bouncing screen when touching input elements. Once the user puts away the keyboard the timer is quick and repositions your web app without a perceptible delay.

Creating UI Using HTML5 Data Attributes

28 Mar 2015

As you may know part of the HTML5 spec includes the use of “data” attributes within HTML elements. For example:

<div data-animal="frog"></div>

You can see the naming convention – the first part must be “data-” and the part after can be anything. This feature provides a mechanism for storing information within your elements for whatever your use-case happens to be.

jQuery provides and easy way of accessing the HTML5 “data” attribute via the data() method as shown here:


<p data-color="red">What color am I hiding?</p>

console.log($(p).data('color')); // outputs "red" to the console

One way to use this is as part of a UI methodology that looks for specific “data” tags in response to user interaction. Thus your application will know that when the user clicks on an element that the element in question is a button (for example) and that the button should trigger specific functionality.

My personal convention for “buttons” and the like is an “action” data tag – data-action=”doSomething”. When I don bother using a framework my applications are always look for this type of attribute within elements. This quickly becomes important if you delegate events to an element higher in the DOM, for example, the body tag.

The sample code below shows this kind of setup.

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         // do something
      }
   });
*lt;/script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">Random Colored Text</div>
      </div>
   </div>
</body>
</html>

All well and good – if the user clicks on the text “Randomly Colored Text” then the changeColor function is fired.

Look at this arrangement:

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         // do something
      }
   });
</script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">Randomly Colored Text</div>
         <div>Hello</div>
         <div data-action="changeSize">Randomly Sized Text</div>
      </div>
   </div>
</body>
</html>

Clicking on either of the DIVs will trigger the “changeColor” function – and in fact, clicking on ***any*** div on the page will call that function – and that includes the wrapping DIVs. We need a way to discern which DIV element is actually something that we care about.

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         if (!$(e.target).data('action')){
            return;
         }
         // do something 
      }
   });
</script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">Randomly Colored Text</div>
         <div>Hello</div>
         <div data-action="changeSize">Randomly Sized Text</div>
      </div>
   </div>
</body>
</html>

On line 7 we’re now checking to see if the data attribute “action” exists in the div that triggered the event. If it doesn’t exist we stop the flow of execution via the return statement. That’s a good start – lets take it one step further as we still need to know how to “changeColor” (line 18) or “changeSize” (line 20).

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         if (!$(e.target).data('action')){
            return;
         }
         if ($(e.target).data('action') === 'changeColor'){
            console.log('change the fonts color here');
         }
         if ($(e.target).data('action') === 'changeSize'){
            console.log('change the fonts color here');
         }
      }
   });
</script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">Randomly Colored Text</div>
         <div>Hello</div>
         <div data-action="changeSize">Randomly Sized Text</div>
      </div>
   </div>
</body>
</html>

Now we have it – not only are we looking to see if any DIV element that has been clicked is something that we should pay attention to but we are also looking for specific data attributes from which we need to do something (lines 10 and 13). In this case we’re attempting to change the color of the text or change the size of the text.

If you elect to you could always just look for the specific data attributes and not do a check for the “action” data attribute – whatever you like to do – my personal approach is a larger topic making use of both “checks” – see jQuery, PubSubs and Event Delegation.

Something that will mess things up for you is the common practice of nesting multiple elements within a wrapping “button” DIV.

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         if (!$(e.target).data('action')){
            return;
         }
         if ($(e.target).data('action') === 'changeColor'){
            console.log('change the fonts color here');
         }
         if ($(e.target).data('action') === 'changeSize'){
            console.log('change the fonts color here');
         }
      }
   });
</script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">
            <div class="homeIcon"></div>
            <div class="buttonText">
               Randomly Colored Text
            </div>
         </div>
         <div>Hello</div>
         <div data-action="changeSize">Randomly Sized Text</div>
      </div>
   </div>
</body>
</html>

The data-action attribute is still on the button wrapper as seen on line 23. The difference is that we now have two DIV elements within that wrapper – they will be the target of any click events and thus our code will skip over that button and not do anything. You may be tempted to add data-action attributes to everything inside of your button wrapper but that gets messy. Instead lets add some more code to our initial checking of whether or not the data-action attribute exists. Lets add something that iterates through the parents of the div looking to see if one of them contains the data attribute that we are looking for.

if (!$(e.target).data('action')){
    // if e.target does not have a data action, search through a max of 2 parents to find it
    var obj = e.target;
    var max = 0;
    while (!$(obj).data('action') && max != 2){
        obj = $(obj).parent();
        ++max;
    }
    if (!$(obj).data('action')){
        return;
    } else {
        e.target = obj;   
    }
}

Here’s whats happening here – the code looks first for a data-action attribute. If it is not found it then proceeds to look at the parent elements to see if they have them. If not found in one parent it then looks at that parent’s parent, and so on. We have a maximum ceiling here of 2 levels of parent searching. Plenty for this example and for most applications.

Once the parent with a data-action attribute is found the object that represents that parent is used to replace the click event’s “target” – as an FYI all event objects store the element that was clicked, holding them in the event object’s “target” property. By replacing the target – e.target – with the parent object that ***does*** contain the required data attribute we allow our code to continue what it was doing previously.

The full example:

<html><head><title></title>
<script>
   $(document).ready({
      $('body').on('click','div',changeSomething);

      function changeSomething(e){
         if (!$(e.target).data('action')){
             // if e.target does not have a data action, search through a max of 2 parents to find it
             var obj = e.target;
             var max = 0;
             while (!$(obj).data('action') && max != 2){
                 obj = $(obj).parent();
                 ++max;
             }
             if (!$(obj).data('action')){
                 return;
             } else {
                 e.target = obj;   
             }
         }
         if ($(e.target).data('action') === 'changeColor'){
            console.log('change the fonts color here');
         }
         if ($(e.target).data('action') === 'changeSize'){
            console.log('change the fonts color here');
         }
      }
   });
</script>
</head>
<body>
   <div id="wrapper">
      <div>
         <div data-action="changeColor">
            <div class="homeIcon"></div>
            <div class="buttonText">
               Randomly Colored Text
            </div>
         </div>
         <div>Hello</div>
         <div data-action="changeSize">Randomly Sized Text</div>
      </div>
   </div>
</body>
</html>