📜 ⬆️ ⬇️

Creating a lightbox for the touch interface on HTML5

Stephen Woods, a flickr frontend engineer, explains how to create a simple gesture-enabled lightbox and gives tips for improving the perception and performance of touch interfaces.



Prerequisite knowledge: intermediate CSS, intermediate advanced JavaScript
Requirements: Android or IOS device
Temporary costs: 2-3 hours
Download source
View demo

')
Lightbox widgets have become standard on the web since the first version of lightbox.js was released in 2005. Lightbox creates a modal dialog box in order to view enlarged images, usually with the “Next” and “Previous” buttons, to move between slides.

Starting with a boom in using touch devices, websites have updated lightboxes to support the interaction of gestures with varying degrees of success. In this tutorial, I'm going to show you how to create a simple gesture-enabled lightbox. In the process, you will learn a little about improving the perceived performance of touch interfaces, as well as a few simple tricks to improve actual performance.

Writing code for touch devices is significantly different from writing code for desktops. You can (and should) combine as much code with desktop as possible, but there will always be significant differences between them.

Comparative tests show that the most common sensory devices are comparable in performance to desktop computers around 1998. They usually have about 256 MB of RAM, CPU performance on a par with the original iMac. The methods that we use to “just work” on the desktop will not work correctly on mobile phones and tablets. Fortunately, these devices are usually well optimized for graphics, especially for moving elements on the screen. IOS and Android 3.0+ devices have hardware graphics acceleration.
In fact, you can think of these devices as crappy computers with decent graphics cards.

We have interacted with our computers in the same way for the past 20 years. We move the mouse pointer and click on the control buttons. Buttons, closing blocks, links, and scroll bars are second nature to users and developers. Touch interfaces represent a completely different set of conventions. One of the most common is “swipe”. When "sliding" several elements are presented as if they are going in a row, and the user can use the "sliding" gesture to move between them.

Gliding is such a common pattern that we don’t even have to tell users about it — when the user sees something like a list, he instinctively tries to scroll through it.

Often we cannot make our code run a little faster, especially when we are dealing with slow connections and slow devices. But we can make the interface seem faster, focusing on perceptual optimizations.

My favorite example of optimizing perceived performance is TiVo. Thirteen years ago, when the first TiVo appeared, they were incredibly slow (16 MB of RAM and 54 MHz CPU!). It could take a painfully long time for something to happen after something was pressed on the remote, especially if they were starting to play or record something. However, no one ever complained that TiVo is slow. In my opinion, this is because of the sound. The most familiar part of the TiVo interface is the melody that sounded after you press any button. That sound played instantly. The engineers at TiVo made the sound load as quickly as possible, so that no matter what happens next, the user knew that the interface was not dead. This short tune told users that their request was heard.
In the network, we have developed an agreement that does the same thing: a loader (spinner). After the click, a spinner appears immediately and in this way, the user receives a message that he has been heard. In mobile phones, we must do otherwise.

Gestures are not discrete actions, like clicks. However, to make the interface seem fast, we need to give users some feedback. As they are gesticulating, we move the interface in some way so that they know that we “hear” them.

Instruments


Highly responsive interfaces require elements to move as quickly as possible. In motion, we show the user that the interface responds to their request. Using JavaScript animations is too slow for this. Instead, we use CSS transformations and transitions: transformations for performance, and transitions for the animation to work without blocking the execution of JavaScript.
In this tutorial, for all movements and animations, I will use CSS transforms and transitions.
Another optimization that I want to use as much as possible is what I call the “write-only DOM”. Reading properties and values ​​from a DOM is complex and usually unnecessary. For lightbox, I will try to combine all reads in the initialization phase. After that, I will maintain state in JavaScript and, if necessary, do simple arithmetic.

Lightbox creation


For this tutorial we will create a page with several thumbnails. Clicking (or tapping with the finger) on the thumbnails will launch the lightbox. After that, in the lightbox, the user will be able to scroll through the image with his finger, and then touch the image to exit the lightbox.

When creating an interface for gestures, be aware of the importance of perceived performance. In a lightbox, this means that the slides should move because of a finger sliding on the screen. When the user stops gesturing, the slides should move to the next position, or return to the previous one if there is not enough slip length.

Return animation is critical. Thanks to it, the user will never think that the interface is dead.

Start


Create the following files:
lightbox/
reset.css
slides.css
slides.html
slides.js




Template


HTML will be simple. This is not just for the sake of a demo. A complex DOM tree is by definition slower. Styles, DOM element extraction, and visual effects are increasingly heavy with a more complex DOM tree. Since we are aiming at “crappy computers,” every bit counts, so it’s important to keep everything simple from the start.

I am using reset.css from Eric Meyer to start resetting CSS. I also customize the viewport so that it does not scale.

I turned off the native “click to zoom” so that it does not interfere with gestures. Correct response to the click will be implemented in JavaScript. “Click to zoom” deserves a separate lesson, so we will drop it now.

On the JS side, I use zepto.js, a very lightweight platform with jQuery syntax. In fact, there is no need for any platform, but this will speed up the work on some tasks a little. For actual gesture interactions, we will use built-in APIs.

 <div class="main"> <div class="welcome"> <h1>Welcome to an amazing carousel!</h1> <p>This is an example of a nice touch interface</p> </div> <div class="carousel"> <ul> <li> <a href="http://www.flickr.com/photos/protohiro/6664939239/in/photostream/"> <img data-full-height="427" data-full-width="640" src="http://farm8.staticflickr.com/7142/6664939239_7a6c846ec9_s.jpg"> </a> </li> <li> <a href="http://www.flickr.com/photos/protohiro/6664957519/in/photostream"> <img data-full-height="424" data-full-width="640" src="http://farm8.staticflickr.com/7001/6664957519_582f716e38_s.jpg"> </a> </li> <li> <a href="http://www.flickr.com/photos/protohiro/6664955215/in/photostream"> <img data-full-height="640" data-full-width="427" src="http://farm8.staticflickr.com/7019/6664955215_d49f2a0b18_s.jpg"> </a> </li> <li> <a href="http://www.flickr.com/photos/protohiro/6664952047/in/photostream"> <img data-full-height="426" data-full-width="640" src="http://farm8.staticflickr.com/7017/6664952047_6955870ecb_s.jpg"> </a> </li> <li> <a href="http://www.flickr.com/photos/protohiro/6664948305/in/photostream"> <img data-full-height="428" data-full-width="640" src="http://farm8.staticflickr.com/7149/6664948305_fb5a6276e5_s.jpg"> </a> </li> </ul> </div> </div> </body> <script src="zepto.min.js" type="text/javascript" charset="utf-8"></script> <script src="slides.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript" charset="utf-8"> //this code initializes the lightbox and shows it when the user //clicks on a slide $(document).ready(function(){ var lightbox = new saw.Lightbox('.carousel'); $(document).on('click', 'a', function(e){ e.preventDefault(); lightbox.show(this.href); }); }); </script> </html> 


Stylize Thumbnails


Now add some cute little thumbnails and some other visual effects:

 html { background: #f1eee4; font-family: georgia; color: #7d7f94; } h1 { color: #ba4a00; } .welcome { text-align: center; text-shadow: 1px 1px 1px #fff; } .welcome h1 { font-size: 20px; font-weight: bold; } .welcome { -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ -moz-box-sizing: border-box; /* Firefox, other Gecko */ box-sizing: border-box; /* Opera/IE 8+ */ margin:5px; padding:10px; box-shadow: 2px 2px 5px rgba(0,0,0,0.5); border-radius: 5px; } .carousel { margin:5px; } .carousel ul li { height: 70px; width: 70px; margin: 5px; overflow: hidden; display: block; float: left; border-radius: 5px; box-shadow: 1px 1px 2px rgba(0,0,0,0.5), -1px -1px 2px rgba(255,255,255,1); } 


Main lightbox


Lightbox JavaScript should do several different things:


Service functions

Instead of writing "-webkit-transform" and "translate3d" again and again, I will create several utility functions to do this work for me.

 function prefixify(str) { var ua = window.navigator.userAgent; if(ua.indexOf('WebKit') !== -1) { return '-webkit-' + str; } if(ua.indexOf('Opera') !== -1) { return '-o-' + str; } if(ua.indexOf('Gecko') !== -1) { return '-moz-' + str; } return str; } function setPosition(node, left) { // node.css('left', left +'px'); node.css(prefixify('transform'), "translate3d("+left+"px, 0, 0)"); } function addTransitions(node){ node.css(prefixify('transition'), prefixify('transform') + ' .25s ease-in-out'); node[0].addEventListener('webkitTransitionEnd', function(e){ window.setTimeout(function(){ $(e.target).css('-webkit-transition', 'none'); }, 0) }) } function cleanTransitions(node){ node.css(prefixify('transition'), 'none'); } 


Our lightbox widget will be initialized when the page loads to speed up the processes. Initialization is to find all the thumbnails on the page and create a data model. We will wait for the lightbox to be provided to create HTML and attach event handlers.

Initialization

For lightbox, I use a constructor that takes a block selector as its only parameter.

 //clean namespacing window.saw = (function($){ //the lightbox constructor function Lightbox (selector) { var container_node = $(selector), wrapper, chromeBuilt, currentSlide = 0, slideData =[], boundingBox = [0,0], slideMap = {}; function init(){ //init function } return { show: show, hide: hide }; } return { Lightbox:Lightbox }; }($)); 


The “init” function captures all the “li” tags, finds the thumbnails and writes information to the “slideData” array. At the same time, I create a “slideMap” object that displays “href” thumbnails in the “slideData” array. This allows me to quickly search for data on a click, without having to cycle through all the data in the array or decorate the DOM with additional information.

 function init(){ var slides = container_node.find('li'); slides.each(function(i, el){ var thisSlide = {}, thisImg = $(el).find('img'); thisSlide.url = thisImg.attr('src'); thisSlide.height = thisImg.attr('data-full-height'); thisSlide.width = thisImg.attr('data-full-width'); thisSlide.link = $(el).find('a').attr('href'); //push the slide info into the slideData array while recording the array index in the slideMap object. slideMap[thisSlide.link] = slideData.push(thisSlide) - 1; }); } 


The rest of the initialization takes place in the "show" method.

 //this is the function called from the inline script function show(startSlide){ if(!chromeBuilt){ buildChrome(); attachEvents(); } wrapper.show(); //keep track of the viewport size boundingBox = [ window.innerWidth, window.innerHeight ]; goTo(slideMap[startSlide]); } 


Shell creation

The buildChrome function creates an HTML wrapper for the lightbox and then sets the semaphore so that the wrapper is not rebuilt every time the user hides or shows the lightbox. For ease of use, I created a separate template function for the HTML itself:

 var wrapperTemplate = function(){ return '<div class="slidewrap">'+ '<div class="controls"><a class="prev" href="#">prev</a> | <a class="next" href="#">next</a></div>'+ '</div>'; } function buildChrome(){ wrapper = $(wrapperTemplate()).addClass('slidewrap'); $('body').append(wrapper); chromeBuilt = true; } 


The final step in creating a shell is adding an event handler for the links “Next” and “Previous”:

 function handleClicks(e){ e.preventDefault(); var targ = $(e.target); if(targ.hasClass('next')) { goTo(currentSlide + 1); } else if(targ.hasClass('prev')){ goTo(currentSlide - 1); } else { hide(); } } function attachEvents(){ wrapper.on('click', handleClicks, false); } 


The lightbox shell is now ready to work with multiple slides. In my show function, I call goTo (), to load the first slide. This function shows the slide identified by the parameters, but it also slowly creates the slides, in case I need them. (Important: do not set the function "goTo" in lower case, because "goto" is a reserved word in JavaScript).

Slideshow

Now the slide on which I am looking is in the viewing area, and the previous and next slides are to the left and right of the visible screen area, respectively. When the user taps the Next button, the current slide goes to the left, and is replaced by the next slide.

 //for the slides, takes a "slide" object function slideTemplate(slide){ return '<div class="slide"><span>'+slide.id+'</span><div style="background-image:url('+slide.url.replace(/_s|_q/, '_z')+')"></div></div>'; } 


I use it instead, because (at least for now) mobile browsers draw much slower than with a background image. When working with mobile devices, speed is usually preferable. Accessibility issues can easily be solved with the help of the " ARIA Role ".

By itself, the “buildSlide” function is more complex. In addition to moving data through the slide template, the code must make the slides fit in the viewport. This is a simple task of figuring out how much to scale an image if it doesn't fit. We can allow the browser to handle resizing.

 function buildSlide (slideNum) { var thisSlide, s, img, scaleFactor = 1, w, h; if(!slideData[slideNum] || slideData[slideNum].node){ return false; } var thisSlide = slideData[slideNum]; var s = $(slideTemplate(thisSlide)); var img = s.children('div'); //image is too big! scale it! if(thisSlide.width > boundingBox[0] || thisSlide.height > boundingBox[1]){ if(thisSlide.width > thisSlide.height) { scaleFactor = boundingBox[0]/thisSlide.width; } else { scaleFactor = boundingBox[1]/thisSlide.height; } w = Math.round(thisSlide.width * scaleFactor); h = Math.round(thisSlide.height * scaleFactor); img.css('height', h + 'px'); img.css('width', w + 'px'); }else{ img.css('height', thisSlide.height + 'px'); img.css('width', thisSlide.width + 'px'); } thisSlide.node = s; wrapper.append(s); //put the new slide into the start poisition setPosition(s, boundingBox[0]); return s; } 




goTo

“GoTo” moves the requested and adjacent slides to the viewing area.

 function goTo(slideNum){ var thisSlide; //if the slide we are looking for doesn't exist, lets just go //back to the current slide. This has the handy effect of providing //"snap back" feedback when gesturing, the slide will just animate //back into position if(!slideData[slideNum]){ return; } thisSlide = slideData[slideNum]; //build adjacent slides buildSlide(slideNum); buildSlide(slideNum + 1); buildSlide(slideNum - 1); //make it fancy addTransitions(thisSlide.node); //put the current slide into position setPosition(thisSlide.node, 0); //slide the adjacent slides away if(slideData[slideNum - 1] && slideData[slideNum-1].node){ addTransitions(slideData[slideNum - 1 ].node); setPosition( slideData[slideNum - 1 ].node , (0 - boundingBox[0]) ); } if(slideData[slideNum + 1] && slideData[slideNum + 1].node){ addTransitions(slideData[slideNum + 1 ].node); setPosition(slideData[slideNum + 1 ].node, boundingBox[0] ); } //update the state currentSlide = slideNum; } 


At the moment, the lightbox is more or less functional. We can go to the next and previous slide, we can hide and show it. It would be ideal to know when we reach the first or last slide: you can, for example, display the gray controls. This applies to both desktops and touch devices.

Adding gesture support


Most touch devices have built-in photo viewers. These various applications, following the original iPhone photo viewer, created an agreement for the interfaces: sliding your finger to the left shows the next slide. I have seen several implementations of such an interaction that do not give feedback at all - the slides are simply replaced when the gesture is completed. The right approach is to give live feedback. As the user slides his finger, the slides should move with him, and, depending on the direction, the next or previous slide should appear. This creates the illusion that the user is pulling a strip of photos.



Handling touch events

Many libraries, including Zepto, include support for sensory events. In general, I do not recommend using them. When processing touch events, you update elements along with custom gestures. When the delay is noticeable to the user, it causes a feeling of a slow interface. One of the main reasons why we used libraries for events is to provide normalization in browsers. All mobile browsers that support touch events have the same API.

There are three sensory events that we consider in this example: “touchstart”, “touchmove” and “touchend”. There is also a touchcancel event when the gesture is interrupted for some reason (for example, a push message). When developing you should process them correctly.

 function attachTouchEvents() { var bd = document.querySelector('html'); bd.addEventListener('touchmove', handleTouchEvents); bd.addEventListener('touchstart', handleTouchEvents); bd.addEventListener('touchend', handleTouchEvents); } 


The event handler receives a TouchEvent object. The “touchstart” and “touchmove” events contain the “touches” property, which is the object of the “Touch” array. To slide a finger, only one property is needed: “clientX”. It contains the value of the touch position relative to the upper left corner of the page.

iOS devices support up to eleven simultaneous touches. Android (before Ice Cream Sandwich) supports only one. Most interactions require only one touch. More complex gestures make you worry about multiple touches.

The "handleTouchEvents" function

First, we define several variables outside this function to support states:

 var startPos, endPos, lastPos; 


The following branch is based on the property type of the event object:

 function handleTouchEvents(e){ var direction = 0; //you could also use a switch statement if(e.type == 'touchstart') { } else if(e.type == 'touchmove' ) { } else if(e.type == 'touchend) { } 


The touchstart event starts at the beginning of any touch event, so use it to record where the gesture started, which will come in handy later. Get rid of any transitions that may still be in the tags.

 if(e.type == 'touchstart') { //record the start clientX startPos = e.touches[0].clientX; //lastPos is startPos at the beginning lastPos = startPos; //we'll keep track of direction as a signed integer. // -1 is left, 1 is right and 0 is staying still direction = 0; //now we clean off the transtions if(slideData[currentSlide] && slideData[currentSlide].node){ cleanTransitions(slideData[currentSlide].node); } if(slideData[currentSlide + 1] && slideData[currentSlide + 1].node){ cleanTransitions(slideData[currentSlide + 1].node); } if(slideData[currentSlide - 1] && slideData[currentSlide -1].node){ cleanTransitions(slideData[currentSlide -1].node); } } else if(e.type == 'touchmove' ) { 


In “touchmove”, determine how much touch has passed along “clientX”, and then move the current slide to the same distance. If the slide moves to the left, also move the next slide; if to the right, move the previous slide accordingly. In this way, you only move two blocks, but this creates the illusion that the whole strip is moving.

 }else if(e.type == 'touchmove'){ e.preventDefault(); //figure out the direction if(lastPos > startPos){ direction = -1; }else{ direction = 1; } //make sure the slide exists if(slideData[currentSlide]){ //move the current slide into position setPosition(slideData[currentSlide].node, e.touches[0].clientX - startPos); //make sure the next or previous slide exits if(direction !== 0 && slideData[currentSlide + direction]){ //move the next or previous slide. if(direction < 0){ //I want to move the next slide into the right position, which is the same as the //current slide, minus the width of the viewport (each slide is as wide as the viewport) setPosition(slideData[currentSlide + direction].node, (e.touches[0].clientX - startPos) - boundingBox[0]); }else if(direction > 0){ setPosition(slideData[currentSlide + direction].node, (e.touches[0].clientX - startPos) + boundingBox[0]); } } } //save the last position, we need it for touch end lastPos = e.touches[0].clientX; }else if(e.type == 'touchend'){ 


At the end of the slides, it is necessary to determine further behavior: go in a circle or stop on the last slide. If you leave the user on the last slide, then when you try to scroll through the slide, you need to return him back to his position, thus giving the user feedback on why the slide has not changed.



 }else if(e.type == 'touchend'){ //figure out if we have moved left or right beyond a threshold //(50 pixels in this case) if(lastPos - startPos > 50){ goTo(currentSlide-1); } else if(lastPos - startPos < -50){ goTo(currentSlide+1); }else{ //we are not advancing, so we need to "snap back" to the previous position addTransitions(slideData[currentSlide].node); setPosition(slideData[currentSlide].node, 0); if(slideData[currentSlide + 1] && slideData[currentSlide + 1].node){ addTransitions(slideData[currentSlide + 1]); setPosition(slideData[currentSlide + 1].node, boundingBox[0]); } if(slideData[currentSlide - 1] && slideData[currentSlide - 1].node){ addTransitions(slideData[currentSlide - 1]); setPosition(slideData[currentSlide - 1].node, 0 - boundingBox[0]); } } } 


Now all the main elements are in place. You have a simple touch lightbox!

From the translator:
If you find any translation errors, please unsubscribe in private messages. Thank.

Source: https://habr.com/ru/post/143211/


All Articles