When I first decided to make my site, I envisioned a site that I would just build and then, once complete, would easily maintain with almost no effort. However, at the same time I wanted a lot of flexibility as well.
From my original design, I wanted to use a slideshow on the main page of my site that would cycle through recent things that I wanted to share. I wanted it to be highly customization and functional yet at the same time very maintainable and easily update-able. It was at that point that I decided that I would probably use a jQuery slideshow that loaded html asset files as content of the slideshow. I thought that something like this had already been built so I decided to search for a pre-written slideshow script online. Unfortunately after 2 hours of searching, I couldn’t find a single one. The closest I could find was a slideshow script that loaded can take a list of image files and dynamically load them into a slideshow. However, this wasn’t what I wanted. I wanted the text in my slide show to be select-able and I wanted to ability to insert other types of content such as forms or buttons.
Before we continue…
So before we continue. I just wanted to define what I was looking for and why I was looking for it. What I was looking for was a jQuery slideshow plugin that would take a list of html files as arguments and then proceed to asynchronously load these html files and present them in a nice little slideshow. The reason why I was looking for something like this was because I wanted two things:
- Flexibility. The ability to do whatever I wanted in each slide. Whether it be displaying a video, an image or text or even all three, I wanted my slideshow to be able to host any type of content because I have a passion for design and I did not want to design under any form of constraints in the future.
- Maintainability. I wanted this slideshow plugin to be easily maintainable. To be able to add 100 slides if I wanted to while being able to produce each of the 100 slides as productively as I would had there been only one slide. In other words, productivity or difficulty to create/add more slides should not go up (on a per slide basis) as the number of slides increased (although realistically, I would probably only have 2-5 slides).
By loading html files into a slideshow, we a) guarantee full flexibility and b) guarantee that our code will not much messier as we add more slides and that the code will be just as maintainable with 1 slide as it is with 100 slides. Now that I got the explanation of why I (or you) would want to do this, let’s get to work…
The hard part…
Had I been a designer and not a programmer, I probably would have settled for one of the slideshow solutions that can take in a list of divs as input. But I’m not a designer (or at least not just a designer) but a programmer, so I decided to take the tougher but more amusing route, that is roll out my own slideshow plugin in javascript and after a few minutes of thinking I thought up of what I wanted to do.
So first, I thought of the way I would like to pass in files to my slideshow. I decided that I would created a div that represented where my slideshow would go and that I would add each slide as a div inside this div. The slide div(s) would then have a class set to ‘slide’ and the title of the div will be set to the actual file of the slide to load. I chose to do things this way because it allowed me to add more things into the slideshow div other than slides for even more flexibility. I also decided that my slideshow plugin will take in, as an argument, a <ul> element that the plugin will populate with the slide buttons. This would result in something like this in the html of the finished product:
1 2 3 4 5 |
<div id="slideshow-content"> <div class="slide" title="slides/slide1.html"></div> <div class="slide" title="slides/slide2.html"></div> </div> <ul id="slideshow-controls" class="align-right"></ul> |
Then I wrote out what will be the skeleton of our script:
1 2 3 4 5 6 7 8 9 10 11 |
(function($){ var startDynamicSlideshow = function(element, options) { var $elem = $(element); // we will write the main logic here... }; $.fn.startDynamicSlideshow = function(options) { return startDynamicSlideshow(this, options); }; })(jQuery); |
Now that we have our skeleton, we need to think about what we need to get our slideshow working.
After a bit of more thinking, what I decided on a list of things to do in order to get this slideshow plugin working:
- Traverse the div that we will use to host the slideshow to determine what slides to load and where they are located.
- Load the slides one by one asynchronously.
- Display our slideshow and transition between slides.
- Add controls and allow the user to control which slide to show. (Optional)
And also a few things we would probably like want the user of this plugin to have control over. They are: the speed of the slideshow, the speed of the transition and as we discussed earlier, a optional <ul> element that we can populate with the controls to the slideshow. To do this we need to add the following code:
1 2 3 4 5 6 |
// Merge config settings var config = $.extend({ speed: 1000, timeout: 4000, controls: null, }, options || {}); |
Step 1: Parsing and preparing the slide information
So first, to get the children of the slideshow container div we can write $elem.children(".slide") which will return a list of child elements that have the ‘slide’ class. Then to traverse the list we can simply use $.each(). Putting it all together we get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
... var slides = []; $.each($elem.children(".slide"), function(index, val) { var div = $(this); var slideLoc = div.attr("title"); div.removeAttr("title"); config.numSlides++; div.css({ position: 'absolute', top: 0, left: 0, zIndex: 0, width: "100%", height: "100%", padding: 0, margin: 0, }); slides.push({loaded: false, slideDir: slideLoc, holder: div}); }); ... |
So in the above code, we first construct an empty list of slide items that we will use to hold information regarding the slide. We then traverse the list of div(s) within the slideshow div that had the ‘slide’ class (as we discussed). Then, for each of the elements, we save the location of the html file to load and then clear the title attribute since we no longer need it. We then do a bit of work to set the css of the div (since we will populate this div with the actual slide later on, we need to set up it’s style so that it displays correctly). Finally we construct an object to store the slide info and add it to our slides list and with that we are done part 1!
Step 2: Loading the slides
After we have a list of slide information, it’s time to put it to use! To load our slides asynchronously, we are going to user the jQuery function .load() which will actually do all of the hard work for us (yay :D). Couple that with some clever code and we are already half way done:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... var loadIndex = 0; var slideToLoad = slides[0]; slideToLoad.holder.css('z-index', 1); var onLoaded = function() { loadIndex++; if (loadIndex < slides.length) { slideToLoad = slides[loadIndex]; slideToLoad.holder.load(slideToLoad.slideDir, onLoaded); } }; slideToLoad.holder.load(slideToLoad.slideDir, onLoaded); ... |
So what we are doing here is, we create some variables to keep track of which slide we are currently loading. We also ensure that the first slide we will load is shown on top of all the others ( slideToLoad.holder.css('z-index', 1);). After that we proceed to load the first slide. Now here is where the magic happens. When we load our first slide, we pass .load() a function to call once the first slide is finished loading. Then this function is called, it checks if there is another slide to load. If there is, it loads it and sets itself as the function to call after the load is done. Otherwise it will simply do nothing, therefore breaking the load chain. Remember that this is all happening asynchronously so we don’t really need to worry how long the load will take.
Step 3: Display our slideshow and transition between slides
We are now venturing into the more difficult and complicated areas of implementing this slideshow so hang on tight! So the main idea in solving step 3 is to first save our current slide position and change this position every so often. The idea isn’t difficult but the implementation might be. For simplicity sakes, I will only show you how you would go about implementing this with a css fade transition (because one, since it uses css, it’s guaranteed to be efficient and produce very smooth animations no matter the device, and two, because it’s relatively simple). Anyways, without further ado I give you:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
... var where = 0; // This is the style we apply when we fade out the current slide... var styles = { 'transition': 'opacity ' + config.speed + 'ms', opacity: 0 }; // This is the initial slide style var stylesCleared = { opacity: 1 }; var fadeTimerId; var changeSlides = function($newSlide) { $oldSlide.css('z-index', 1); $newSlide.css('z-index', 0); $oldSlide.stop().css(styles); $newSlide.stop().css(stylesCleared); // now wait for animation to end if (fadeTimerId != undefined) { clearTimeout(fadeTimerId); } fadeTimerId = setTimeout(function(){ $oldSlide.css('z-index', 0); $newSlide.css('z-index', 1); }, config.speed); } var SlideShowTimer = (function() { var timerFn = function(){ var $newSlide = slides[where + 1 == slides.length ? 0 : where + 1].holder; changeSlides($newSlide); where++; if (where >= slides.length) { where = 0; } }; var timerHandle; return { start: function() { timerHandle = setInterval(timerFn, config.timeout); }, reset: function() { clearInterval(timerHandle); this.start(); } }; })(); SlideShowTimer.start(); ... |
Alright, before we dissect this massive block of code, we need to first understand how we would do a fade transition. Let’s start with an example. Let’s say we have slide A showing and we want to transition to slide B. Then what we want to do is to put slide B behind slide A then we fade out slide A by animating it’s opacity to 0. And voila we are done.
Now that we know how to do this animation. Let’s write it up. To begin we create two styles. One for when the animation is happening (we turn on transitions so that our opacity change will animate and set opacity to 0) and another style for a static slide (just set opacity to 1). We then write the function to actually perform this change called changeSlides() which takes in the slide to change to and transitions into that slide. The javascript here is just as we said. We ensure that the old slide is in front of the old slide. We then stop their animations (if they are animating) and apply their respective styles. We then post a function to be run once the animation is over so switch their z-index (we also cancel a scheduled function if there was one).
Next we create a timer object to loop through the slides automatically by using .setInterval(). We also implement a few functions to control our timer (which we will use later).
Step 4: Adding controls (Optional)
Just using what we have, we can already create a working slideshow; however, the user would have no control over which slide is showing and no way of going to a certain slide! So we must add some controls to give the user control of the slideshow.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
... if (config.controls !== null) { var $pageButtons; var $controls = config.controls; var controlColor = '#909090'; // Ensure that our controls will be displayed horizontally $controls.css('display', 'inline'); // Add each slide as an item in the list... // Note that we set the data-target attr. to the index of the slide // We will use this information later... for(var i = 0; i < slides.length; i++){ $controls.append('<li class="page" data-target="'+i+'"></li>'); } // Style for each of the control button... var style = { display: "inline-block", margin: '0 2px 0 2px', width: '12px', height: '12px', background: '#fff', cursor: 'pointer', 'border-radius': '20px', 'border-style': 'solid', 'border-width': '3px', 'border-color': controlColor, 'transition': 'all 0.3s ease-in-out' }; $pageButtons = $controls.find('.page'); // We apply our style to each control button $pageButtons.css(style); // We also add a nice effect when the user hovers over the button $pageButtons.hover( function() { $(this).css('background',controlColor); }, function() { if (!$(this).hasClass("active")) { $(this).css('background','#fff'); } } ); // We now introduce the function we will use to force change slides var setCurrentSlide = function(currentSlideIndex) { // First we reset our SlideShowTimer SlideShowTimer.reset(); // If we are already on the selected slide, do nothing... if (where === currentSlideIndex) return; // Otherwise we can just call the changeSlides() function on the selected slide changeSlides(slides[currentSlideIndex].holder); where = currentSlideIndex; }; $pageButtons.bind('click',function(){ // Remember that 'data-target' attr? We now extract that and use it to // switch to the correct slide var target = $(this).attr('data-target'); setCurrentSlide(parseInt(target)); }); var oldPosition = 0; // This function will update the controls to reflect the new slide position... var updateControls = function(newPosition) { var oldSelectedButton = $pageButtons.eq(oldPosition); var selectedButton = $pageButtons.eq(newPosition); // We change the fill color of the old select button to white oldSelectedButton.css('background','#fff'); // We also remove it's active class (used only for marking purposes) oldSelectedButton.removeClass("active"); // We then change the new selected slide control's fill color selectedButton.css('background', controlColor); // We then add the active class to mark this element as active selectedButton.addClass("active"); // We then update the oldPosition var oldPosition = newPosition; } // Finally we update the controls so that the first slide is selected... updateControls(0); } ... |
Since this block of code is pretty long, I just decided to embed my explanation of the code as comments. Take a good read through. We also need to update the changeSlides() for switching slides so that it will update our controls as well, so our new changeSlides() will look like (note the highlighted lines):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var changeSlides = function($newSlide) { if (config.controls !== null) { updateControls($newSlide.index()); } $oldSlide.css('z-index', 1); $newSlide.css('z-index', 0); $oldSlide.stop().css(styles); $newSlide.stop().css(stylesCleared); // now wait for animation to end if (fadeTimerId != undefined) { clearTimeout(fadeTimerId); } fadeTimerId = setTimeout(function(){ $oldSlide.css('z-index', 0); $newSlide.css('z-index', 1); }, config.speed); } |
Finally…
We are done! If you assembled all of this code together you should end up with something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
(function($){ var startDynamicSlideshow = function(element, options) { var $elem = $(element); // Merge config settings var config = $.extend({ speed: 1000, timeout: 4000, controls: null, }, options || {}); var slides = []; $.each($elem.children(".slide"), function(index, val) { var div = $(this); var slideLoc = div.attr("title"); div.removeAttr("title"); config.numSlides++; div.css({ position: 'absolute', top: 0, left: 0, zIndex: 0, width: "100%", height: "100%", padding: 0, margin: 0, }); slides.push({loaded: false, slideDir: slideLoc, holder: div}); }); var loadIndex = 0; var slideToLoad = slides[0]; slideToLoad.holder.css('z-index', 1); var onLoaded = function() { loadIndex++; if (loadIndex < slides.length) { slideToLoad = slides[loadIndex]; slideToLoad.holder.load(slideToLoad.slideDir, onLoaded); } }; slideToLoad.holder.load(slideToLoad.slideDir, onLoaded); var where = 0; // This is the style we apply when we fade out the current slide... var styles = { 'transition': 'opacity ' + config.speed + 'ms', opacity: 0 }; // This is the initial slide style var stylesCleared = { opacity: 1 }; var fadeTimerId; var changeSlides = function($newSlide) { if (config.controls !== null) { updateControls($newSlide.index()); } $oldSlide.css('z-index', 1); $newSlide.css('z-index', 0); $oldSlide.stop().css(styles); $newSlide.stop().css(stylesCleared); // now wait for animation to end if (fadeTimerId != undefined) { clearTimeout(fadeTimerId); } fadeTimerId = setTimeout(function(){ $oldSlide.css('z-index', 0); $newSlide.css('z-index', 1); }, config.speed); } var SlideShowTimer = (function() { var timerFn = function(){ var $newSlide = slides[where + 1 == slides.length ? 0 : where + 1].holder; changeSlides($newSlide); where++; if (where >= slides.length) { where = 0; } }; var timerHandle; return { start: function() { timerHandle = setInterval(timerFn, config.timeout); }, reset: function() { clearInterval(timerHandle); this.start(); } }; })(); SlideShowTimer.start(); if (config.controls !== null) { var $pageButtons; var $controls = config.controls; var controlColor = '#909090'; // Ensure that our controls will be displayed horizontally $controls.css('display', 'inline'); // Add each slide as an item in the list... // Note that we set the data-target attr. to the index of the slide // We will use this information later... for(var i = 0; i < slides.length; i++){ $controls.append('<li class="page" data-target="'+i+'"></li>'); } // Style for each of the control button... var style = { display: "inline-block", margin: '0 2px 0 2px', width: '12px', height: '12px', background: '#fff', cursor: 'pointer', 'border-radius': '20px', 'border-style': 'solid', 'border-width': '3px', 'border-color': controlColor, 'transition': 'all 0.3s ease-in-out' }; $pageButtons = $controls.find('.page'); // We apply our style to each control button $pageButtons.css(style); // We also add a nice effect when the user hovers over the button $pageButtons.hover( function() { $(this).css('background',controlColor); }, function() { if (!$(this).hasClass("active")) { $(this).css('background','#fff'); } } ); // We now introduce the function we will use to force change slides var setCurrentSlide = function(currentSlideIndex) { // First we reset our SlideShowTimer SlideShowTimer.reset(); // If we are already on the selected slide, do nothing... if (where === currentSlideIndex) return; // Otherwise we can just call the changeSlides() function on the selected slide changeSlides(slides[currentSlideIndex].holder); where = currentSlideIndex; }; $pageButtons.bind('click',function(){ // Remember that 'data-target' attr? We now extract that and use it to // switch to the correct slide var target = $(this).attr('data-target'); setCurrentSlide(parseInt(target)); }); var oldPosition = 0; // This function will update the controls to reflect the new slide position... var updateControls = function(newPosition) { var oldSelectedButton = $pageButtons.eq(oldPosition); var selectedButton = $pageButtons.eq(newPosition); // We change the fill color of the old select button to white oldSelectedButton.css('background','#fff'); // We also remove it's active class (used only for marking purposes) oldSelectedButton.removeClass("active"); // We then change the new selected slide control's fill color selectedButton.css('background', controlColor); // We then add the active class to mark this element as active selectedButton.addClass("active"); // We then update the oldPosition var oldPosition = newPosition; } // Finally we update the controls so that the first slide is selected... updateControls(0); } }; $.fn.startDynamicSlideshow = function(options) { return startDynamicSlideshow(this, options); }; })(jQuery); |