ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
Pagination in Flash - With Page Buttons
http://www.actionscript.org/resources/articles/1035/1/Pagination-in-Flash---With-Page-Buttons/Page1.html
Chris Bristol
I have been using Flash in my web development since version 4. As ActionScript became a more powerful language, I found myself fascinated by the interaction between it and PHP and MySQL. In recent years, I have worked at eliminating the use of the timeline altogether, building everything in pure AS3, and making my code reusable. The fantastic resources of the vast online Flash community has been a great source of help and inspiration. I hope to be able to give back a little of that. You can reach me via my website: Xty Digital Design
 
By Chris Bristol
Published on October 7, 2010
 
In the first tutorial we used the number of items to move through the pages, and that works fine for small amounts of data, but what if you have hundreds of items to look through, or you are displaying pages of text, like a tutorial on ActionScript.org? In those cases it would be better to move through the data by page numbers, and that is what we are going to do here.


The Same Only Different
A lot of the pieces of this tutorial will look familiar to those of you who read the first pagination tutorial. There are enough differences however, that it is worth going through all the code once again. One of the big differences is that we are using buttons to move through the pages. Before we get started on the code itself, let's take a look at one way you can make your buttons in Flash.

Make Your Own Buttons

First thing is to make your buttons in PhotoShop, or whatever your favorite graphics program is. Once you have the buttons ready, open up your fla and go to File -> Import -> Import To Library, and bring your buttons in. You can download a zip file called pagDemoButtonSkins.zip that contains the png files and the original psd file if you want to see the set up. It was made using PhotoShop CS3.

Make sure you have already dragged an instance of Button from the Components to your library. With that in there, you will also have a folder called Component Assests. Open that and then open the Button Skins folder. There you will find all the skins that are associated with your button. These are all the default skins. Right click on the Button_upSkin and choose Duplicate. In the panel that comes up name your new skin Button_upSkinPN. Now check the Export for ActionScript checkbox and click OK.



You will see the new skin appear in the Button Skins folder. Right click on your new skin and choose Edit. In the timeline for this moveiclip add a new layer at the top above the highlight layer and drag your button_up instance from the library to the stage. Align it to the top and the left. Now delete the 3 layers under it on the timeline so that the only thing left is your new layer.

Now do the same steps with the Button_overSkin, Button_downSkin, Button_disabledSkin, Button_selectedDownSkin, Button_selectedOverSkin,and Button_selectedUpSkin, naming them Button_overSkinPN, Button_downSkinPN, etc.

And here's how we use our new buttons:
[as]
myButton.setStyle("upSkin", Button_upSkinPN);
myButton.setStyle("overSkin", Button_overSkinPN);
myButton.setStyle("downSkin", Button_downSkinPN);
myButton.setStyle("selectedUpSkin", Button_selectedUpSkinPN);
myButton.setStyle("selectedOverSkin", Button_selectedOverSkinPN);
myButton.setStyle("selectedDownSkin", Button_selectedDownSkinPN);
myButton.setStyle("disabledSkin", Button_disabledSkinPN);
[/as]

We will be using the Toggle property on our buttons, so I have also created the skins for the _selected options. In all cases for the selected skins - up, down, over - we use our button_down skin.

The Demo Movie

The movie below feeds on 2 XML files containing the song data. I wanted the demo to make use of dynamically gathered data, and XML seemed to be the best choice under the circumstances. How you get your data is unimportant to the pagination concept, it's what we do with it afterwards that counts. For this tutorial I needed a lot more data in order to effectively show off the page numbers, so I cheated and just copied and pasted the entries in the rockMusicPN.xml file until I had enough. I mention this in case you are wondering why the songs repeat.



The Demo Files

Now, download the nextPrevPN.zip file and let's dig into the code and see how this works.

The files included in the nextPrevPN.zip are PagDemoPN.as and pagDemoPN.fla. The easyMusicPN.xml and rockMusicPN.xml files hold the data that we will be reading into the movie. And, you'll find the NextPrevPN.as class and the CustomEvent.as class files inside the ca.xty.myUtils folder system.

The XML files are very simple examples, and so I hope they don't need any detailed explanation, because, once again, I'm not providing any. There are quite a few good tutorials about using Flash and XML and, to do the subject justice, I'd need to make this tutorial even longer than it is. Besides, you will most likely have much more complex data to work with in a real world situation, and consequently a much more complex XML file. What I'll mainly be dealing with is gathering the data from the XML file and packaging it up in a way that's easy for us to use.

There is also a text file called backToTop.txt. This is a tiny bit of JavaScript that we will use together with the ExternalInterface class provided by Flash.

And, as mentioned above there is a zip file with the PhotoShop psd file containing the artwork for the button skins used in this demo.

As usual, the fla is empty, except for a button in the library, our new skins, and the fact that the Document Class property is set to PageDemoPN.

What's not included in the zip file this time out is an fla for CS3 - but don't despair! Creating one is dead easy. Make a new fla in CS3 with a width of 500 and a height of 600. Drag a Button component into the library and set your Document Class to read PagDemoPN - remember, no .as extension, just the class name. This time, you'll have to create your button skins as described above. make sure to use the same names for the skins as I have and you should be fine. With all the files included in the zip in the same folder as your CS3 fla you should be good to go.

Future tutorials will likely be done in Flash CS5 now that I have finally moved up to that version. If you are doing a lot of custom class work, do yourself a favor and move up too. The custom class introspection is worth it all by itself.

So let's jump into the PagDemoPN.as file already!


PagDemoPN.as
In order to make this work we need to have some data to make a results list from. In this example we are using two XML files to provide that data. Open up the pagDemoPN.fla and it's Document Class, PagDemoPN.as. You'll notice in the pageDemoPN.fla that I haven't lied to you, and there is nothing on the the stage and the only thing in the library is an instance of the Button component. Have a peek in the Component Assets -> ButtonSkins folder and you will find all of our custom skins. Under the Document Class setting we have declared PagDemoPN - no .as, just the class name - as the Document Class. Let's switch over to that file.

Starting from the top, we import the necessary classes.

[as]
package{
    import flash.display.*;
    import flash.events.*;
    import flash.geom.*;
    import flash.net.*;
    import flash.text.*;
    import fl.controls.Button;
    import flash.external.ExternalInterface;
    import ca.xty.myUtils.NextPrevPN;
    import ca.xty.myUtils.CustomEvent;
[/as]

The two custom classes we need are found in the ca.xty.myUtils folder and they are NextPrevPN.as and CustomEvent.as. We will discuss these, and the ExternalInterface class, in more detail as we go along. Next we set up our variables.

[as]
public class PagDemoPN extends MovieClip{
 
    //XML variables
    private var dataLoader:URLLoader = new URLLoader();
    private var xmlData:XML = new XML();
    private var musicInfoArray:Array;
 
    //On screen assets
    private var musicBut1:Button;
    private var musicBut2:Button;
    private var goBtn:Button;
    private var t1:TextField;
    private var ip1:TextField;
 
    //Data display variables
    private var barArray:Array;
    private var titleArray:Array;
    private var composerArray:Array;
    private var descArray:Array;
    private var songLengthArray:Array;
    private var cartArray:Array;
    private var titleHead:TextField;
    private var timeHead:TextField;
    private var composerHead:TextField;
    private var descHead:TextField;
    private var buyHead:TextField;
    private var buyArray:Array;
 
    //Pagination variables
    private var npT:NextPrevPN;
    private var npB:NextPrevPN;
    private var useNP:Boolean = false;
    private var maxNum:int = 7;
    private var numSongs:int;
    private var itemStart:int = 0;
    private var itemsToShow:int;
    private var pageStart:int = 1;
    private var numVisible:int;
    private var nBtn:Boolean;
    private var pBtn:Boolean;
    private var fBtn:Boolean;
    private var lBtn:Boolean;
    private var firstPage:Boolean = true;
    private var resultsTop:Sprite;
    private var resultsHolder:Sprite;
    private var newPage:int;
    private var newPageBut:int;
    private var numPages:int = 8;

    // TextFormats
    private var titleFormat:TextFormat;
    private var titleFormatC:TextFormat;
 
    //X and Y positions
    private var xPos:int;
    private var yPos:int;
[/as]

First up, we get our XML variables ready. We declare a URLLoader to get the XML files off the server. Then we have a an XML object, and finally an array called musicInfoArray which is an array that we will be dumping all the results from the XML files in.

Our On Screen assets include the buttons to grab the music and another button which allows us to set the number of results we want to show at one time. The textfields are a label field and an input field to enter in the number of desired results.

The Data Display variables are the things we will be using to show the results. These consist of arrays and textfields.

The Pagination variables are the things will will be using to keep track of where we are and what is happening within the pagination process. The first two, npT and npB are variables to represent instances of our class NextPrevPN and stand for NextPrevPN Top and NextPrevPN Bottom. The useNP is a Boolean that will tell us whether or not we need to use an instance of the NextPrevPN class by determining the number of results and balancing that against the next variable maxNum. So, if we have 5 results and our maxNum is 10 then we do not need to use the NextPrevPN instance. If the opposite is true, then we need it. Then we have a bunch of integer values to pass information back and forth to the NextPrevPN class, a few more Booleans to let us know where we stand, and finally a couple of Sprites that we will be using as containers to hold our results. Using containers like this will provide us an easy way to remove the results in a single step. Notice that the two variables, maxNum and numPages have their values set. maxNum represents the maximum number of items to show per page, and numPages represents the maximum number of page number buttons to show at one time. You can alter these variables to meet your needs. The other two variables that are preset are itemStart and pageStart. The first time out itemStart will always be 0 and pageStart will always be 1. itemStart is 0 because this variable wants to get the first item in an array, which starts with a 0 index, and pageStart is set to 1 so that it makes sense to your visitors who think that the first page is 1, not 0.

Next up is our constructor.

[as]
public function PagDemoPN(){
 
    //Set up the TextFormats
    titleFormat = new TextFormat();
    titleFormat.color = 0x000000;
    titleFormat.font = "verdana";
    titleFormat.size = 10;
    titleFormat.align = "left";
 
    titleFormatC = new TextFormat();
    titleFormatC.color = 0x000000;
    titleFormatC.font = "verdana";
    titleFormatC.size = 10;
    titleFormatC.align = "center";
 
    //Give our XML Loader a Complete event listener
    dataLoader.addEventListener(Event.COMPLETE, loadData);
 
    //Put our assets on stage
    musicBut1 = new Button();
    musicBut1.x = 20;
    musicBut1.y = 10;
    musicBut1.width = 120;
    musicBut1.height = 20;
    musicBut1.label = "Rock Music (84)";
    musicBut1.addEventListener(MouseEvent.CLICK, musicButHandler);
    addChild(musicBut1);
 
    musicBut2 = new Button();
    musicBut2.x = 150;
    musicBut2.y = 10;
    musicBut2.width = 120;
    musicBut2.height = 20;
    musicBut2.label = "Easy Listening (4)";
    musicBut2.addEventListener(MouseEvent.CLICK, musicButHandler);
    addChild(musicBut2);
 
    t1 = new TextField();
    t1.x = 280;
    t1.y = 10;
    t1.width = 90;
    t1.height = 20;
    t1.text = "Items to Show:";
    t1.setTextFormat(titleFormat);
    addChild(t1);
 
    ip1 = new TextField();
    ip1.x = 375;
    ip1.y = 10;
    ip1.width = 30;
    ip1.height = 20;
    ip1.type = "input";
    ip1.border = true;
    ip1.defaultTextFormat = titleFormatC;
    ip1.text = String(maxNum);
    addChild(ip1);
 
    goBtn = new Button();
    goBtn.x = 410;
    goBtn.y = 10;
    goBtn.width = 30;
    goBtn.height = 20;
    goBtn.label = "Go";
    goBtn.addEventListener(MouseEvent.CLICK, goHandler);
    addChild(goBtn);
 
}
[/as]
First we give our TextFormats some properties. They are nearly identical, with the only difference being the text align property.

Next we add a COMPLETE Event Listener to our XML dataLoader and direct it at a function called loadData.

Now we stick our stage assets in place. These are the buttons that will allow you to choose between Rock Music and Easy Listening. The numbers in parentheses represent the number of songs available in each category. I have taken a short cut here. In a real application, you would be counting the number of incoming results and appending that number to the button label. The last three assets go to make up our number-of-results-to-see-at-once feature. A title field with a label of Items to Show, an input field to gather the answer and the goBtn to make it happen. Our goBtn has an event listener for a CLICK event and takes you to a function called goHandler. Using the page number buttons this time out, you will also be affecting the total number of page buttons when you change the number of items to show. For instance, Rock Music has 84 items. The default is 7 items per page so we have a total of 12 page buttons. If you were to change the number of items to show to say 12, then you would have a maximum of 7 pages.

Now we'll look at the function that handles the user changing the number of items to show per page.

[as]
private function goHandler(e:MouseEvent):void{
    //As long as the input field is not empty, we change the variable maxNum to the contents of the ip1 textfield
    if(ip1.text != ""){
 maxNum = Number(ip1.text);
    }
    //If the boolean firstPage is not true we remove the data already displayed on stage and reset our variables to start over
    if(!firstPage){
 removeChild(resultsTop);
 removeChild(resultsHolder);
 if(useNP){
 removeChild(npB);
 useNP = false;
    }
    firstPage = true;
    itemStart = 0;
    }
    //Check to see if numSongs is greater than the new maxNum and then buildResults again using the the new maxNum
    if(numSongs > maxNum){
 useNP = true;
    }else if(numSongs <= maxNum){
 itemsToShow = numSongs;
    }
    // Now build the results
    buildResults();
}
[/as]

The first thing we check is whether or not or input field, ip1, has anything in it. As long as it is not equal to nothing, we set the variable maxNum to whatever is in the ip1 field. Next we need to determine if the boolean firstPage is true or not. If it is not true, then that means we already have something on stage that we need to get rid of before proceeding. So we remove our two Sprites, resultsTop and resultsHolder. Now we check the useNP variable to see whether we are using an instance of our NextPrevPN on the bottom. If it's true, then we remove that instance as well. Once everything is off stage, we reset the firstPage variable to true, and our itemStart to 0. Finally we check to see if numSongs is greater than the new maxNum, and then buildResults() again using the the new maxNum.

Next we'll look at our music button handler.

[as]
private function musicButHandler(e:MouseEvent):void{
 switch(e.currentTarget.label){
   case "Rock Music (84)":
   dataLoader.load(new URLRequest("rockMusicPN.xml"));
   break;
 case "Easy Listening (4)":
   dataLoader.load(new URLRequest("easyMusicPN.xml"));
   break;
   }
}
[/as]

We use a switch statement, with the label data as the criteria to tell us which button has been clicked, and then load the appropriate XML file. You'll remember that we already set up our listener for the dataLoader and declared it's function to be loadData.

[as]
private function loadData(e:Event):void {
 try{
   xmlData = new XML(e.target.data);
   parseData(xmlData);
 } catch (e:TypeError){
   trace("Unable to load XML");
   }
}
[/as]

Here we assign the incoming data property to our XML object called xmlData, and then send that data to a function called parseData.

[as]
private function parseData(dataInput:XML):void {
 musicInfoArray = new Array();
 var infoList:XMLList = dataInput.song.composer;
 numSongs = infoList.length();
 for(var i:int = 0; i < numSongs; i++){
   var comp:String = dataInput.song[i].composer;
   var track:String = dataInput.song[i].title;
   var tl:String = dataInput.song[i].track_length;
   var d:String = dataInput.song[i].description;
   var musicObj:Object = {Composer:comp, Track:track, TrackLength:tl, Desc:d};
   musicInfoArray.push(musicObj);
 }
 //If the boolean firstPage is not true, then we remove the current data displayed and reset our variables to start over
 if(!firstPage){
   removeChild(resultsTop);
   removeChild(resultsHolder);
   if(useNP){
     removeChild(npB);
     useNP = false;
   }
   firstPage = true;
   itemStart = 0;
 }
 //If numSongs is greater then the maxNum to be shown, then we will need our next/prev feature so set the boolean useNP to true
 //and then buildResults
 if(numSongs > maxNum){
 useNP = true;
 }else if(numSongs <= maxNum){
 itemsToShow = numSongs;
 }
 // Now we build the results
 buildResults();
}
[/as]

We set our musicInfoArray to empty by declaring a new instance of it. This array is going to hold an array of objects, with each object in the array containing all the information we need about one song. Next we declare a temporary variable called infoList that will hold our XMLList data, and we set it to point to the composer block in our XML file. We determine our numSongs variable by giving it the length value of our infoList. Notice here that the length property is declared as a function - length() - not the length property we normally see when getting the length of an array - someArray.length.
Now we use a for loop to gather all the available data and package it into an object - musicObj -, which we then push into our musicInfoArray. Once we have all the data stored away, we run through our check to see if this is the firstPage or not, and whether the numSongs is greater than the maxNum we set. This is the same routine as in our goHandler function we discussed above. Once we are ready, we buildResults().


Putting Our Results To Work
This function is a bit long, so I'm going to break it up with explanations of individual pieces, rather than give you a long block of code and then a long block of explanatory text. Hopefully this won't be confusing and will be easier to follow without making you scroll up and down to match the text with the code.

[as]
private function buildResults():void{
 // If the boolean firstPage is true we start from scratch
 if(firstPage){
 // resultsTop will be a container to hold our next/prev widget and the column titles
 resultsTop = new Sprite();
 resultsTop.x = 0;
 resultsTop.y = 40;
 addChildAt(resultsTop, 0);
 
 xPos = 0;
 yPos = 0;
[/as]

The first thing we do is check our firstPage variable. If it is true, meaning that there is nothing on screen yet, then we create our resultsTop Sprite. This will be the container that will hold all the stuff that makes up our results header. The reason we have two containers - resultsTop and resultsHolder - is because when we go to the second page of a results set we don't need to change the header information. Keeping the two containers separate gives us this flexibility. We position our resultsTop at an x position of 0 and a y position of 40, and use the addChildAt function to place it on the bottom of the display stack, 0. In this demonstration it is not necessary to use the addChildAt, but, depending on what other things you may be adding later on, it can be handy to have these elements underneath everything else. In any case, it does no harm.

Now we set our variables xPos and yPos to 0. These co-ordinates are relative to the resultsTop container, not the stage.

[as]
if(useNP){
    itemsToShow = maxNum;
    npT = new NextPrevPN(numSongs, maxNum, maxNum, 0x000000, 0x000000, numPages);
    npT.x = (stage.stageWidth - (npT.width - 60))/2;
    npT.y = yPos;
    // If you want to show the next/Prev widget on both the top and the bottom of your results give it a name
    npT.name = "NPT";
    npT.addEventListener(CustomEvent.CUSTOM, reBuild, false, 0, true);
    resultsTop.addChild(npT);
 
    yPos += 50;
}
[/as]

We check our variable useNP, and, if it is true, we set up the variables we need to interact with our NextPrevPN class. The variable itemsToShow refers to the maximum number of items to show at one time, and so is set to our maxNum variable.

Now we create our top instance of the NextPrev class, called npT, and pass these variables along to the NextPrevPN's constructor function:
numSongs - the total number of songs in this result set
maxNum - the maximum number of items to show at once
maxNum - represents the items showing, and is the same number at first as the previous variable
0x000000 - this is our text color
0x000000 - this is the text color for the button labels
numPages - this is the maximum number of page buttons we want to show at once in our NextPrevPN instance

Then we position our NextPrevPN instance. The npT's x property is set up to be centered on the page. We take the stage.stageWidth property and subtract the npT's width - 60 and then divide that result by 2. The reason for the -60 from the npT's width is because the last item to be set up in the NextPrevPN class is a button, and even though the width of that button is declared to be 40, Flash seems to stick with it's default button width of 100. So, 100 - 40 = 60. Strange, but true. Our y position is just simply our yPos variable. Now, if you are going to be using both a top and a bottom instance of the NextPrevPN class you will want to give the instance a name. The name value is arbitrary, and I've called it NPT. This is just so we will know which instance - top or bottom - has been clicked. Then we add an event listener using our CustomEvent class, which we will go into in more detail at the end of this function. Finally, we add the npT instance to the resultsTop container, so that it is a child of the resultsTop Sprite, and then we add 50 to our yPos variable.

[as]
titleHead = new TextField();
titleHead.x = xPos;
titleHead.y = yPos;
titleHead.width = 120;
titleHead.height = 20;
titleHead.border = true;
titleHead.text = "Track Title";
titleHead.setTextFormat(titleFormat);
resultsTop.addChild(titleHead);
 
xPos += 120;
 
timeHead = new TextField();
timeHead.x = xPos;
timeHead.y = yPos;
timeHead.width = 50;
timeHead.height = 20;
timeHead.border = true;
timeHead.text = "Time";
timeHead.setTextFormat(titleFormatC);
resultsTop.addChild(timeHead);
 
xPos += 50;
 
composerHead = new TextField();
composerHead.x = xPos;
composerHead.y = yPos;
composerHead.width = 130;
composerHead.height = 20;
composerHead.border = true;
composerHead.text = "Composer";
composerHead.setTextFormat(titleFormat);
resultsTop.addChild(composerHead);
 
xPos += 130;
 
descHead = new TextField();
descHead.x = xPos;
descHead.y = yPos;
descHead.width = 160;
descHead.height = 20;
descHead.border = true;
descHead.text = "Description";
descHead.setTextFormat(titleFormat);
resultsTop.addChild(descHead);
 
xPos += 160;
 
buyHead = new TextField();
buyHead.x = xPos;
buyHead.y = yPos;
buyHead.width = 40;
buyHead.height = 20;
buyHead.border = true;
buyHead.text = "Buy";
buyHead.setTextFormat(titleFormatC);
resultsTop.addChild(buyHead);
 
xPos = 0;
yPos += 61;
[/as]

Now we begin setting up our headings. First comes the titleHead TextField which holds the words "Track Title". This gets added as a child to our resultsTop container and uses the established xPos and yPos variables to position it. We continue on down the line adding the timeHead TextField next, then the composerHead, descHead,and buyHead Textfields.When we are done all that, we reset our xPos to 0 and add 61 to our yPos.

[as]
}else if(!firstPage){
    //If the boolean useNP is true, set up our x and y positions to take the next/prev widget into account
    if(useNP){
 xPos = 0;
 yPos = 111;
    //If the boolean useNP is not true, our x and y positions only need to take the column headers into account
    }else{
 xPos = 0;
 yPos = 61;
    }
}
// Set up our bar colors and make the current color of our barColor variable equal to the first color
 var bar1Color:Number = 0xFFF7E6;
 var bar2Color:Number = 0xFEE4AB;
 var barColor:Number = bar1Color;
 
 // The resultsHolder will be the container to hold all of our results
 resultsHolder = new Sprite();
 resultsHolder.x = xPos;
 resultsHolder.y = yPos;
 addChildAt(resultsHolder, 0);
   
 yPos = 0;
 // Initialize our arrays for the bars and the column headers
 barArray = new Array();
 titleArray = new Array();
 composerArray = new Array();
 descArray = new Array();
 songLengthArray = new Array();
 buyArray = new Array();

// Set up our bar colors and make the current color of our barColor variable equal to the first color
var bar1Color:Number = 0xFFF7E6;
var bar2Color:Number = 0xFEE4AB;
var barColor:Number = bar1Color;
 
// The resultsHolder will be the container to hold all of our results
resultsHolder = new Sprite();
resultsHolder.x = xPos;
resultsHolder.y = yPos;
addChildAt(resultsHolder, 0);
 
yPos = 0;
// Initialize our arrays for the bars and the column headers
barArray = new Array();
titleArray = new Array();
composerArray = new Array();
descArray = new Array();
songLengthArray = new Array();
buyArray = new Array();
[/as]

If this is not the first page, then we don't need to add all the headings, but we need to know whether useNP is true so that we can take the space it uses into account. If useNP is true, then we reset our xPos to 0 and add 91 to our yPos, otherwise if useNP is false we reset our xPos to 0 and only add 61 to our yPos. In case you're wondering where the number 61 comes from, remember that we have our music buttons and our Item to Show occupying the first 40 pixels, then we have our headings which take up another 20 pixels, and then a 1 pixel space just because it looks better.

Next we give our bar1Color and bar2Color variables some color values. These bars will be Sprites that sit underneath each row and help to separate the results from one another with the use of alternating colors. Then we set our barColor variable to the value of bar1Color.

Now we get on with setting up our results list. We start by creating the resultsHolder Sprite to act as the container for the whole mess. We position it using our xPos and yPos variables, then add it the stage using addChildAt and giving it a spot in the display list of 0. If you remember, we set our resultsTop to this same position. With the resultsHolder now occupying 0, our resultsTop is automatically pushed into position 1.

Again, remember that the xPos and yPos positions will now be relative to our resultsHolder container, not the stage, so we reset our yPos to 0. Our xPos is already 0, so we are good to go there. Next we initialize the arrays we'll be using.

[as]
 // Now we display the results
 for(var i:int = itemStart; i < itemsToShow; i++){
 
 // First we create the bar so that it will sit underneath the text
 barArray[i] = createSprite(barColor, 500, 24, xPos, yPos);
 resultsHolder.addChild(barArray[i]);
 // I want the text to center itself vertically inside the bar, so I add 2 pixels to the y position
 yPos += 2;
 // We make an array of text fields to hold our Song Title
 // Note that we use TextFieldAutoSize.LEFT for the autoSize property. With the multiline and wordWrap properties set to true
 // this will force the text field down to accommodate extra text
 titleArray[i] = new TextField();
 titleArray[i].x = xPos;
 titleArray[i].y = yPos;
 titleArray[i].width = 120;
 titleArray[i].height = 20;
 titleArray[i].multiline = true;
 titleArray[i].wordWrap = true;
 titleArray[i].autoSize = TextFieldAutoSize.LEFT;
 titleArray[i].text = musicInfoArray[i].Track;
 titleArray[i].setTextFormat(titleFormat);
 resultsHolder.addChild(titleArray[i]);
 
 xPos += 120;
 // Next we make an array of text fields to hold our Track Length
 songLengthArray[i] = new TextField();
 songLengthArray[i].x = xPos;
 songLengthArray[i].y = yPos;
 songLengthArray[i].width = 50;
 songLengthArray[i].height = 20;
 songLengthArray[i].text = musicInfoArray[i].TrackLength;
 songLengthArray[i].setTextFormat(titleFormatC);
 resultsHolder.addChild(songLengthArray[i]);
 
 xPos += 50;
 // Next we make an array of text fields to hold our Composer names
 // Note that once again we use TextFieldAutoSize.LEFT for the autoSize property
 composerArray[i] = new TextField();
 composerArray[i].x = xPos;
 composerArray[i].y = yPos;
 composerArray[i].width = 130;
 composerArray[i].height = 20;
 composerArray[i].multiline = true;
 composerArray[i].wordWrap = true;
 composerArray[i].autoSize = TextFieldAutoSize.LEFT;
 composerArray[i].text = musicInfoArray[i].Composer;
 composerArray[i].setTextFormat(titleFormat);
 resultsHolder.addChild(composerArray[i]);
 
 xPos += 130;
 // Next we make an array of text fields to hold our Song Description
 // Note that once again we use TextFieldAutoSize.LEFT for the autoSize property
 descArray[i] = new TextField();
 descArray[i].x = xPos;
 descArray[i].y = yPos;
 descArray[i].width = 160;
 descArray[i].height = 20;
 descArray[i].multiline = true;
 descArray[i].wordWrap = true;
 descArray[i].autoSize = TextFieldAutoSize.LEFT;
 descArray[i].text = musicInfoArray[i].Desc;
 descArray[i].setTextFormat(titleFormat);
 resultsHolder.addChild(descArray[i]);
 
 xPos += 160;
 // Now we make an array of buttons to act as our Buy button
 // We use the name property and assign it a value of i so that we can tell which button has been clicked
 buyArray[i] = new Button();
 buyArray[i].x = xPos + 2.5;
 buyArray[i].y = yPos;
 buyArray[i].width = 35;
 buyArray[i].height = 20;
 buyArray[i].label = "Buy";
 buyArray[i].name = i;
 buyArray[i].useHandCursor = true;
 buyArray[i].addEventListener(MouseEvent.CLICK, buyHandler);
 resultsHolder.addChild(buyArray[i]);
[/as]

Using a for loop we start to add our results to our resultsHolder container. First we create the bar so it will sit underneath everything else. Down near the bottom of this class you will find a function called createSprite, which does just that. We pass the parameters of barColor, the width, the height, the xPos and the yPos to the createSprite class. Our bar lives in the barArray array.

Now we add 2 pixels to our yPos. This is because I want to center the text vertically inside the bar which we created. The bar is 24 pixels in height and the text is set up to be 20 pixels in height, so that by adding 2 pixels to our yPos our text will now have a 2 pixel margin on top and bottom. Provided the text is a single line, of course. If it isn't, well, we'll deal with that in a minute.

Now we set up our titleArray, which will be an array of TextFields that will display our track titles. Notice that we give the TextField an initial height of 20, but set it's multiline and wordWrap properties to true and set the autoSize property to LEFT. What this does is maintain the width of the TextField, but, should there be more text than is capable of fitting in the set width, the TextField will automatically adjust it's height to accommodate the extra text. Remember our musicInfoArray where we stored all of our song properties in an object? Here's where we get to use them. The first track title will be taken from the musicInfoArray[i].Track. Here i is the first object in the musicInfoArray and .Track is one of the properties of that object. Then we set our TextFormat property and finally we add it to our resultsHolder container.

The next bit of information we are going to display is our song length, and since this value won't ever exceed the width we've set for this TextField, we don't have to use the autoSize property. Again, the information we want is inside of musicInfoArray[i].Tracklength property. Using the same techniques, we add in our composerArray and descArray information.

When we get down to the buyArray it is a wee bit different. First off it is going to be an array of buttons. If you recall, the buyHead TextField was set up to be 40 pixels in width. These buttons are going to be 35 pixels wide, but I don't want them jammed up to the left with a gap on the right. I want them centered horizontally within the 40 pixels. So, we add 2.5 to the xPos. The other thing to note is that we are giving the buttons a name property and setting that property to the value of i. We do this so that when a button is clicked we can use the name property value as an index to get at whatever other information we want that is inside the musicInfoArray. Lastly we add it to our resultsHolder container.

[as]
 // Because one of the three text fields using the autoSize property could be larger than our bar height
 // we need to check and see which one is the biggest and then adjust the bar height to suit
 // Once we've figured out the new bar height, we adjust our y position so that the next item will line up properly
 if(descArray[i].height > 20 || titleArray[i].height > 20 || composerArray[i].height > 20){
 if(descArray[i].height >= titleArray[i].height && descArray[i].height >= composerArray[i].height){
 barArray[i].height = descArray[i].height + 2;
 yPos += barArray[i].height - 2;
 }else if(titleArray[i].height >= descArray[i].height && titleArray[i].height >= composerArray[i].height){
 barArray[i].height = titleArray[i].height + 2;
 yPos += barArray[i].height - 2;
 }else if(composerArray[i].height >= descArray[i].height && composerArray[i].height >= titleArray[i].height){
 barArray[i].height = composerArray[i].height + 2;
 yPos += barArray[i].height - 2;
 }
 // If none of the text fields is larger than the bar height, we adjust our y position so that the next item will line up properly
 }else{
 yPos += 23;
 }
 // We want the bars to be alternating colors so we switch the barColor by determining which color is it currently and then assigning
 // it the opposite color
 if(barColor == bar1Color){
 barColor = bar2Color;
 }else{
 barColor = bar1Color;
 }
 // Reset out x position to 0 and add in the next item
 xPos = 0;
 }
 // Once all the items are on stage, if we are using a bottom next/prev widget we add some space to our y position
 yPos += 37;
[/as]

We went to a lot of trouble to accommodate unknown lengths of text. In the example you'll notice that some of the descriptions are pretty long. Now that we have all that set up, we have to deal with it. We have three TextFields set to autoSize, so we need to check each of those TextFields to see if they have grown beyond their default height.

The first if statement checks to see if the descArray[i] TextField's height is greater than 20, or if the titleArray[i] TextField's height is greater than 20, or if the composerArray[i] TextField's height is greater than 20. If any of those conditions is true, then we start our secondary checks to see which of our TextFields is the highest. We want to increase the height of barArray[i] to match the highest of the three TextFields. So, if descArray[i].height is greater than or equal to titleArray[i].height and descArray[i].height is greater than or equal to composerArray[i].height, then the descArray is the highest, so we set the barArray[i].height to equal the descArray[i].height and add 2 just to keep our little margin, at least on top. Now we recalculate the yPos to take this new circumstance into account by taking the current yPos and adding in the new barArray[i].height minus the 2 pixels we added for the margin. If the descArray isn't the highest, then we run the same tests on the other two arrays. And, if all the arrays remain within the 20 pixels height, then we simply add 23 pixels to our existing yPos and carry on.

The next thing we want to do is change the bar color. We want to alternate the colors so things stand out nicely, so what we do is check to see which color the current barColor is, and then change to the other one. So if barColor is equal to bar1Color then set barColor to equal bar2Color, and vice versa.

The last thing we want to do before returning to the top of the loop and adding the next item is reset our xPos to 0.

Once we've looped through all the results, we add 37 to our yPos. The 37 is just an arbitary number to give us a wee bit of space. It can be as much or a little as you want it to be.

[as]
// We check once again to see if the boolean useNP is true and if the boolean firstPage is true
// If both of those variables are true, we set up the next/prev widget the same as the top next/prev
// except for the name property
// Our y position is made up from the current yPos (which was set to 0 when we started to build the results) plus the height of the elements
// at the very top plus a 10 pixel margin, so in this case we need 75 pixels for the top bits plus 10 for the margin
if(useNP && firstPage){
    itemsToShow = maxNum;
    npB = new NextPrevPN(numSongs, maxNum, maxNum, 0x000000, 0x000000, numPages);
    npB.x = ((stage.stageWidth - (npB.width - 60))/2);
    npB.y = yPos + 85;
    npB.name = "NPB";
    npB.addEventListener(CustomEvent.CUSTOM, reBuild, false, 0, true);
    addChildAt(npB, 0);
    // If the boolean useNP is true, but firstPage is false, we need only to reposition our next/prev widget
}else if(useNP && !firstPage){
    npB.y = yPos + 85;
}
// Once the first page is set up we set our boolean firstPage to false
firstPage = false;
}
[/as]

Now, if you are using both a top and a bottom instance of the NextPrevPN class here is where you'll want to add in the second instance. First check to make sure that useNP is true and that firstPage is true. Then set it up exactly the same as the first instance, except that you will give it a different name to differentiate it from the top instance. In this case I've called it NPB. The y position is set to yPos + 85 in this case because our elements on top use 75 pixels, our yPos is currently set to reflect the resultsHolder container and I want a 10 pixel margin between the resultsHolder and the npB instance.

Alternately, if useNP is true, but firstPage is not, then the only thing we will have to do is adjust the existing instance of npB's y position using the same rational as explained just above.

Finally we set our firstPage variable to false.

Next up is our buyHandler function.

[as]
private function buyHandler(e:MouseEvent):void{
 var musicIndex:int = e.target.name;
 trace("Item Clicked = " + musicIndex);
}
[/as]

This function is much simplier than it would be under normal operating conditions. Here it just shows you which item has been clicked. If you click the first item it will trace 0. Why 0? Because arrays use a zero based index. So to get the track title of the first item we would use musicInfoArray[musicIndex].TrackTitle, where musicIndex is equal to 0. Presumably you would at this point be gathering information to enable someone to buy the item they have clicked, but that code is up to you to put into place depending on your circumstances. I'm just demonstrating how to get at that information.

The next function comes into play when any of the buttons, First, Prev, Page Buttons, Next or Last are clicked. It is called inside of the NextPrevPN class. Remember that when we set up our instances npT and npB we added an event listener with the function set to reBuild. So, when you click any of those buttons, the NextPrevPN class manipulates the data and sends the updated data back to our Document Class via our CustomEvent.

[as]
public function reBuild(e:CustomEvent):void{
    pageStart = e.arg[0];
    newPage = e.arg[1];
    newPageBut = e.arg[2]
    itemStart = e.arg[3];
    itemsToShow = e.arg[4];
    nBtn = e.arg[5];
    pBtn = e.arg[6];
    fBtn = e.arg[7];
    lBtn = e.arg[8];
    removeChild(resultsHolder);
    if(e.currentTarget.name == "NPT"){
         npB.manualUpdate(pageStart, newPage, newPageBut, itemStart, itemsToShow, nBtn, pBtn, fBtn, lBtn);
    }else if(e.currentTarget.name == "NPB"){
         npT.manualUpdate(pageStart, newPage, newPageBut, itemStart, itemsToShow, nBtn, pBtn, fBtn, lBtn);
         if (ExternalInterface.available) {
            ExternalInterface.call("backToPageTop", "Got This Far");
         }else{
             trace e("ExternalInterface not available");
         }
    }
    buildResults();
}
[/as]

The first thing to make note of is that this is declared as a public function. This is because it is being called from our NextPrevPN class so we need it to have inter-class operability, and the access-modifier "public" does that. This uses our CustomEvent class to allow the event listener to pass a parameter. First I'll run through this function and then we'll take a side trip to have a look at our CustomEvent class.

The pieces of information we need in order to update the resultsHolder and the unclicked instance of NextPrevPN are taken from the array that is first prepared in the NextPrevPN class and then passed back. So, pageStart is set to equal e.arg[0], newPage is set to equal e.arg[1] and so forth. Then we remove the existing resultsHolder in preparation for rebuilding it.

To update the unclicked NextPrevPN instance, we first determine which instance was clicked. So if e.currentTarget.name equals NPT, then we need to update the bottom instance, and vice versa if the name equals NPB. Now, if you are using a single instance of the NextPrevPN class, and therefore have not given it a name, you need not worry about an error creeping in here because Flash assigns every instance a name by default, and our if statements will simply be false and the program will move on. Once we know which instance has been clicked, we send the information gathered to the other instance and use the built in function in the NextPrevPN class called manualUpdate which will set the parameters in that instance to reflect the parameters in the instance that was clicked. We'll have a closer look at the manualUpdate function once we get to the NextPrevPN class.

If the instance of the NextPrevPN class that was clicked was the bottom one, then we use the ExternalInterface class to connect to a small bit of JavaScript we place on the html page holding our movie. What's it for? If you have ever been on a web page that used pagination to break up a large number of search results, and were at the bottom of the page when you clicked next, did you end up back at the top of the page when the new set of results loaded? I get really grumpy if I don't get automatically taken back to the top of the next or previous page, so the way I make sure this doesn't happen to other people using my creations is to add the following bit of code to the html page and call the JavaScript function from Flash using the ExternalInterface. You will find this code in the backToTop.txt file included in the zip file.

Inbetween the <head> and </head> tags put the following:
<script type="text/javascript">
 function backToTop(){
 self.scrollTo(0,0);
 }
</script>

That simple function causes the page to scroll to a left position of 0 and a top position of 0. You can adjust these values if, for instance, you had a 100 pixel header on the page. The values would then be self.scrollTo(0, 100);.

Now that everything is lined up, we go back and do the buildResults() again.

The final function createSprite does just that.

[as]
private function createSprite(color:int, w:int, h:int, x:int, y:int):Sprite{
 var s:Sprite = new Sprite();
 s.graphics.beginFill(color);
 s.graphics.drawRect(0, 0, w, h);
 s.graphics.endFill();
 s.x = x;
 s.y = y;
 s.alpha = 1;
 addChild(s);
 return s;
}
[/as]

We pass in the color, width, height, x position, and y position and using this information the function draws a rectangle and returns us a shiny new Sprite.

Now let's take a look at our CustomEvent class before we move on and examine the NextPrevPN class.

Custom Events

This class is pleasantly shorter than the last class we looked, eh? First of all, let me say that this class comes from an article I found at <a href="http://www.learningactionscript3.com/2008/11/11/passing-arguments-with-events/" target="_blank">this site</a>, which contains a terrific explanation of all the pieces involved, so I will only go over it briefly here.

[as]
package ca.xty.myUtils {
import flash.events.Event;
   
 public class CustomEvent extends Event {
 public static const CUSTOM:String = "custom";
 public var arg:*;
 
 public function CustomEvent(type:String, customArg:* = null, bubbles:Boolean = false, cancelable:Boolean = false) {
 super(type, bubbles, cancelable);
 this.arg = customArg;
 }
 
 public override function clone():Event {
 return new CustomEvent(type, arg, bubbles, cancelable);
 }
 
 public override function toString():String {
    return formatToString("CustomEvent", "type", "arg", "bubbles", "cancelable", "eventPhase");
 }
 
 }
}
[/as]



This class differs in some other ways as well. All of the classes we have created in this tutorial and the others I have done are custom classes which extend Sprite or MovieClip. This is the first class which have looked at which extends another class besides the default Sprite or MovieClip, in this case, the Event class. The reasoning behind this is that we want to use all the capabilities of the Event class as they are, but we want to add the ability, or extend it, to be able to pass back an argument within the event listener.
The variables we declare are both set to public as we will be needing them outside of this class. The first one is a static constant named CUSTOM which is typed to a string. The second one is called arg and is typed with an asterisk so that it can be any type of variable from a String to and Array and anything inbetween. This functionality enables you to reuse this class anytime you have a situation where you need to pass an argument back.
In the constructor we accept the parameters of type, which will be our CUSTOM variable, our customArg, which can be of any type and is optional because we have given it a default value of null, along with the bubbles and cancelable parameters which are also optional.
We have two public functions which override the clone function and toString functions found in the original Event class. To use this class you do something like this:
someObject.addEventListener(CustomEvent.CUSTOM, someFunction, false, 0, true);
And to dispatch the event you do this:
dispatchEvent(new CustomEvent(CustomEvent.CUSTOM, yourArgs));
We'll see this in action when we look at the NextPrevPN class next.



The Work Horse - NextPrevPN.as
This is where all the magic happens. First off we import all the classes we need.

[as]
package ca.xty.myUtils {
    import flash.display.*;
    import flash.text.*;
    import flash.events.*;
    import flash.utils.*;
    import flash.net.*;
    import fl.controls.Button;
    import ca.xty.myUtils.*;
[/as]

Now we set up our variables.

[as]
public class NextPrevPN extends MovieClip {
       
    private var nextBtn:Button;
    private var prevBtn:Button;
    private var firstBtn:Button;
    private var lastBtn:Button;
       
    private var _numItems:int;// This is our total number of items
    private var _maxItems:int;// This is the number of items we want to show at one time
    private var itemStart:int;// This is the we use in our for loop to tell us which point in the array to start counting at
    private var _itemsToShow:int;// This is the number of items we are currently showing
    private var _txtColor:Number;// Text color
    private var _btnColor:Number;// Text color for the button
    private var _nBtn:Boolean;// This variable lets us know whether the Next button is currently enabled
    private var _pBtn:Boolean;// This variable lets us know whether the Prev button is currently enabled
       
    private var buttonArray:Array;
    private var numPages:uint;
    private var _numButtons:uint;
    private var curPageBut:int = -1;
    private var newPageBut:int = 0;
    private var curPage:int = -1;
    private var newPage:int;
    private var firstPage:int;
    private var lastPage:int;
    private var pageStart:int;
    private var buttonCalc:int;
       
    private var statsArray:Array;// This is the array we will be passing back to the main class via our Custom Event
       
    //Display text fields
    private var st1:TextField;
    private var st2:TextField;
    private var st3:TextField;
    private var st4:TextField;
    //Our text formats
    private var showingFormat:TextFormat;
    private var showingFormatC:TextFormat;
    private var butFormat:TextFormat;
    private var butDownFormat:TextFormat;
    //Our x and y position variables
    private var xPos:int;
    private var yPos:int;
[/as]

    The comments tell the story pretty well. From the main class we are receiving the Total Number of Items, the Numbers of Items to Show at Once, the visual Number of Items to be Shown, the Text Color, the Button text color and the number of buttons to show at once.

[as]
public function NextPrevPN(numItems:int, maxItems:int, itemsToShow:int, txtColor:Number, btnColor:Number, numButs:uint){
    // Add our passed parameters to our class variables
    _numItems = numItems;
    _maxItems = maxItems;
    _itemsToShow = itemsToShow;
    _txtColor = txtColor;
    _btnColor = btnColor;
    _numButtons = numButs;
           
    numPages = Math.ceil(_numItems/_maxItems);
    if(numPages < _numButtons){
        _numButtons = numPages;
    }
    buttonCalc = Math.floor(_numButtons/2);
[/as]

First we transfer the incoming parameters in the constructor to the declared class variables. Then we figure out our total number pages by dividing our total items (_numItems) by the items to show per page (_maxItems) and use Math.ceil to round up. We want to round up because if you have 24 items and you are showing a maximum of 10 items per page you will have 2 full pages and a third page with 4 items on it. If we rounded down we would loose those last 4 items. Once we establish the number of pages we check that against the number of buttons we have decided to show (_numButtons). If numPages is less than _numButtons then we set _numButtons to equal numPages. The variable buttonCalc takes our _numButtons and divides it by 2 and then uses Math.floor to round it down. What the buttonCalc variable is going to do for us is set the number of pages we will move the page menu by when we reach the visible end of the buttons. It is best if your _numButtons (number of buttons showing on the screen) is an even number, but if it's not then the Math.floor will make sure we don't call for, say, 4 1/2 buttons. To see what I am talking about go to the demo movie and click on the last visible page button, number 8. Notice how the number 8 button moves back to approximately the middle, removing buttons 1- 4 from the beginning of the button menu, and making visible buttons 9 - 12. Our _numButtons variable is set to 8, so our buttonCalc is 4 and that is what causes the first 4 buttons to disappear and the next 4 buttons in the series to appear.

[as]
// Set up our text formats
showingFormat = new TextFormat();
showingFormat.color = _txtColor;
showingFormat.align = "left";
showingFormat.size = 10;
showingFormat.font = "verdana";
           
showingFormatC = new TextFormat();
showingFormatC.color = _txtColor;
showingFormatC.align = "center";
showingFormatC.size = 10;
showingFormatC.font = "verdana";
           
butFormat = new TextFormat();
butFormat.color = _btnColor;
butFormat.size = 10;
butFormat.font = "verdana";
           
butDownFormat = new TextFormat();
butDownFormat.color = 0xff0000;
butDownFormat.size = 10;
butDownFormat.font = "verdana";
// Set our initial x and y positions
xPos = 0;
yPos = 20;
           
// Set up our first page button
firstBtn = new Button();
firstBtn.x = xPos;
firstBtn.y = yPos;
firstBtn.width = 40;
firstBtn.height = 24;
firstBtn.useHandCursor = true;
firstBtn.label = "First";
firstBtn.setStyle("upSkin", Button_upSkinPN);
firstBtn.setStyle("overSkin", Button_overSkinPN);
firstBtn.setStyle("downSkin", Button_downSkinPN);
firstBtn.setStyle("disabledSkin", Button_disabledSkinPN);
firstBtn.setStyle("textFormat", butFormat);
firstBtn.addEventListener(MouseEvent.CLICK, npHandler);
firstBtn.enabled = false;
addChild(firstBtn);
           
xPos += 45;
       
// Set up our previous button
prevBtn = new Button();
prevBtn.x = xPos;
prevBtn.y = yPos;
prevBtn.width = 60;
prevBtn.height = 24;
prevBtn.useHandCursor = true;
prevBtn.label = "<< Prev";
prevBtn.setStyle("upSkin", Button_upSkinPN);
prevBtn.setStyle("overSkin", Button_overSkinPN);
prevBtn.setStyle("downSkin", Button_downSkinPN);
prevBtn.setStyle("disabledSkin", Button_disabledSkinPN);
prevBtn.setStyle("textFormat", butFormat);
prevBtn.addEventListener(MouseEvent.CLICK, npHandler);
prevBtn.enabled = false;
addChild(prevBtn);
           
xPos += 65;
           
var x1:int = xPos;
[/as]

After we add some properties to our TextFormats, we set the initial value of xPos and yPos to 0 and then we begin to build our NextPrevPN menu. We have a First Page button, and next to it a Previous button. Notice that we are using our custom button skins here. At the very begining we won't have any pages that come before this, and we are already on the first page, so we initially set both of these buttons to enabled = false. We are going to need a reference point to tell us where the page buttons start on the X axis, so we set up the x1 variable and set it equal to the current xPos position.

[as]
// Our variable itemStart will always be 0 at the beginning
itemStart = 0;
pageStart = 0;
buttonArray = new Array();
var j:uint = pageStart + 1;
for(var i:int = 0; i < _numButtons; i++){
    buttonArray[i] = new Button();
    buttonArray[i].x = xPos;
    buttonArray[i].y = yPos;
    buttonArray[i].width = 28;
    buttonArray[i].height = 24;
    buttonArray[i].toggle = true;
    buttonArray[i].label = j;
    buttonArray[i].name = i;
    buttonArray[i].useHandCursor = true;
    buttonArray[i].setStyle("upSkin", Button_upSkinPN);
    buttonArray[i].setStyle("overSkin", Button_overSkinPN);
    buttonArray[i].setStyle("downSkin", Button_downSkinPN);
    buttonArray[i].setStyle("selectedUpSkin", Button_selectedUpSkinPN);
    buttonArray[i].setStyle("selectedOverSkin", Button_selectedOverSkinPN);
    buttonArray[i].setStyle("selectedDownSkin", Button_selectedDownSkinPN);
    buttonArray[i].setStyle("textFormat", butFormat);
    buttonArray[i].addEventListener(MouseEvent.CLICK, pageListener);
    addChild(buttonArray[i]);
    xPos += 28;
    j++;
}
firstPage = parseInt(buttonArray[0].label);
lastPage = parseInt(buttonArray[_numButtons - 1].label);
newPageBut = 0;
newPage = 1;
butState();
           
var x2:int = xPos;
[/as]

 Now we build the page buttons. On the first time through our variables itemStart and pageStart will be zero. Then we initialize our buttonArray and set up a variable j to equal pageStart + 1. We will use j to set the label on our buttons. Even though we as programmers need to start things at 0, our visitors expect the first page to be 1.
Using a for loop we create enough page buttons to satisfy our _numButtons variable. Note that we are setting the toggle property to true and that we are making use of our custom button skins once again. Because toggle is true, we need to use our selected states skins as well.
Once we have the buttons in place, we initialize our firstPage and lastPage variables using parseInt to take our button labels and turn them back into integers. The newPageBut and newPage variables are initialized as well. We will be using these in our function butState(). This function will act as our toggle button handler. Normally a toggle button is used by clicking it once and then clicking it again to release the state, but we don't want people to have to unclick our page buttons, we only want the toggle feature to show them visually which page button is clicked. Lastly, we create a variable called x2 as a reference to the ending position of our button menu.

[as]
xPos += 5;
           
           
// Set up our Next button
nextBtn = new Button();
nextBtn.x = xPos;
nextBtn.y = yPos;
nextBtn.width = 60;
nextBtn.height = 24;
nextBtn.useHandCursor = true;
nextBtn.label = "Next >>";
nextBtn.setStyle("upSkin", Button_upSkinPN);
nextBtn.setStyle("overSkin", Button_overSkinPN);
nextBtn.setStyle("downSkin", Button_downSkinPN);
nextBtn.setStyle("disabledSkin", Button_disabledSkinPN);
nextBtn.setStyle("textFormat", butFormat);
nextBtn.addEventListener(MouseEvent.CLICK, npHandler);
addChild(nextBtn);
           
xPos += 65;
           
// Set up our Next button
lastBtn = new Button();
lastBtn.x = xPos;
lastBtn.y = yPos;
lastBtn.width = 40;
lastBtn.height = 24;
lastBtn.useHandCursor = true;
lastBtn.label = "Last";
lastBtn.setStyle("upSkin", Button_upSkinPN);
lastBtn.setStyle("overSkin", Button_overSkinPN);
lastBtn.setStyle("downSkin", Button_downSkinPN);
lastBtn.setStyle("disabledSkin", Button_disabledSkinPN);
lastBtn.setStyle("textFormat", butFormat);
lastBtn.addEventListener(MouseEvent.CLICK, npHandler);
addChild(lastBtn);
[/as]

To give us a bit of room we add 5 to our xPos and then put our Next and Last buttons in place.

[as]
xPos = x1 + (((x2 - x1) - 220)/2);
yPos -= 20;
   
// Set up our text fields to show the results
st1 = new TextField();
st1.x = xPos;
st1.y = yPos;
st1.width = 80;
st1.height = 20;
st1.text = "Total Items:";
st1.setTextFormat(showingFormatC);
addChild(st1);
           
xPos += 80;
       
st2 = new TextField();
st2.x = xPos;
st2.y = yPos;
st2.width = 30;
st2.height = 20;
st2.text = String(_numItems);
st2.setTextFormat(showingFormatC);
addChild(st2);
               
xPos += 30;
           
st3 = new TextField();
st3.x = xPos;
st3.y = yPos;
st3.width = 80;
st3.height = 20;
st3.text = "Total Pages:";
st3.setTextFormat(showingFormatC);
addChild(st3);
               
xPos += 80;
               
st4 = new TextField();
st4.x = xPos;
st4.y = yPos;
st4.width = 30;
st4.height = 20;
st4.text = String(numPages);
st4.setTextFormat(showingFormatC);
addChild(st4);
               
}
[/as]

To set up the text fields to show us our total items and total pages information we need to first calculate our xPos. It will equal x1 (starting reference) plus x2 (ending reference) minus x1 minus 220 ( the total width of the 4 text fields, st1, st2, st3 and st4) divided by 2. This will center it within the button menu. Because I want to place it on top of the button menu we subtract 20 from our current yPos. That done, we set up our text fields and we are good to go.

[as]
private function pageListener(e:MouseEvent = null):void{
    if(e != null){
        newPage = parseInt(e.target.label);
        newPageBut = e.target.name;
    }
    if(newPage > 1){
        prevBtn.enabled = true;
        firstBtn.enabled = true;
    }else if(newPage <= 1){
        prevBtn.enabled = false;
        firstBtn.enabled = false;
    }
       
    if(newPage >= numPages){
        nextBtn.enabled = false;
        lastBtn.enabled = false;
    }else if(newPage < numPages){
        nextBtn.enabled = true;
        lastBtn.enabled = true;
    }
           
    if(newPage == firstPage && newPage > 1){
        if(newPage - buttonCalc > 0){
            var extraPage:int = pageStart % buttonCalc;
            if(extraPage > 0){
                pageStart -= extraPage;
                newPageBut += ((pageStart - buttonCalc) + extraPage);
            }else{
                pageStart -= buttonCalc;
                newPageBut += (pageStart + buttonCalc);
            }
        }else if(newPage - buttonCalc == 0){
            if(pageStart - buttonCalc >= 0){
                pageStart -= buttonCalc;
                newPageBut += (pageStart + buttonCalc);
            }else{
                pageStart = 0;
                newPageBut = newPage - 1;
            }
        }else if(newPage - buttonCalc < 0){
            newPageBut = pageStart;
            pageStart = 0;
        }
        buttonArray[0].selected = false;
        buildPageButtons();
    }else if(newPage == firstPage && newPageBut == 0){
        pageStart = 0;
        buildPageButtons();
    }
           
    if(newPage == lastPage && (newPage + buttonCalc) <= numPages){
        pageStart += buttonCalc;
        newPageBut -= buttonCalc;
        buttonArray[_numButtons-1].selected = false;
        buildPageButtons();
    }else if(newPage == lastPage && numPages - newPage > 0){
        pageStart += (numPages - newPage);
        if((pageStart - buttonCalc) < 0){
            newPageBut -= pageStart;
        }else{
            newPageBut -= (pageStart - buttonCalc);
        }
        buttonArray[_numButtons-1].selected = false;
        buildPageButtons();
    }else if(newPage == lastPage && (numPages - newPage) == 0){
        pageStart = (numPages - _numButtons);
        buildPageButtons();
    }
    butState();
    itemStart = (_maxItems * newPage) - _maxItems;
    _itemsToShow = _maxItems * newPage;
       
    if(newPage > curPage){
        if(_itemsToShow >= _numItems){
            _itemsToShow = _numItems;
        }
    }
           
    if(newPage < curPage){
        if(_itemsToShow > _maxItems){
            var mod = _itemsToShow % _maxItems;
            if(mod > 0){
                _itemsToShow -= mod;
                _itemsToShow += _maxItems;
            }
        }
           
        if(_itemsToShow <= _maxItems && _numItems >= _maxItems){
            _itemsToShow = _maxItems;
        }
    }
           
    statsArray = new Array();
    statsArray.push(pageStart);
    statsArray.push(newPage);
    statsArray.push(newPageBut);
    statsArray.push(itemStart);
    statsArray.push(_itemsToShow);
    statsArray.push(nextBtn.enabled);
    statsArray.push(prevBtn.enabled);
    statsArray.push(firstBtn.enabled);
    statsArray.push(lastBtn.enabled);
    dispatchEvent(new CustomEvent(CustomEvent.CUSTOM, statsArray));
           
    curPage = newPage;
}
[/as]

The function pageListener is the event handler for the page buttons we just set up. It has a lot to keep its eye on. Notice that the MouseEvent is set to equal null. The reason we do this is so that we can call it from a MouseEvent, someone clicks a page button, or we can call it without the use of a button click. We'll be doing that in the function npHandler which is the event listener for the First Page, Previous, Next and Last Page buttons.

The first thing we check is whether or not e is equal to null. If it is not equal to null, meaning that a page button was clicked, then we set the newPage variable to equal the label of the page button that was clicked. We turn it from a String into an Integer using the parseInt function. Then we set the newPageBut to equal the button name property.

If newPage is greater than 1 then we enable the prevBtn and the firstBtn, otherwise if it is less than or equal to 1 we disable the prevBtn and the firstBtn.

If the newPage is greater than or equal to numPages, meaning that we are at the end of the series of page buttons, we set the nextBtn and the lastBtn to be disabled, otherwise we enable both of them.

Now, if newPage is equal to firstPage ( this is the first page button showing, not necessarily page number 1) and newPage is greater than 1, we then check to see if newPage minus buttonCalc is greater than 0. If it is, then we create the variable extraPage as an integer and set it to be equal to the modulo of pageStart and buttonCalc, which is the remainder of pageStart divided by buttonCalc. Now, if extraPage is greater than 0, pageStart minus equals extraPage which is another way of saying that pageStart equals pageStart minus extraPage. Then the newButPage variable is set to equal pageStart minus buttonCalc plus extraPage. If extraPage is not greater than zero, then pageStart minus equals buttonCalc, and newPageBut plus equals pageStart plus buttonCalc.

Now, if newPage minus buttonCalc is equal to 0, then we check to see if pageStart minus buttonCalc is greater than or equal to 0. If that's true, then pageStart minus equals buttonCalc and newPageBut plus equals pageStart plus buttonCalc. If, however, pageStart minus buttonCalc is not greater than or equal to 0, then pageStart equals 0 and newPageBut equals newPage minus 1.

And finally, if newPage minus buttonCalc is less than 0, then newPageBut equals pageStart and pageStart equals 0. Now we set the 0 index of our buttonArray's selected property to false, and call the function buildPageButtons.

We are not done checking yet. If our first if statement: newPage == firstPage && newPage > 1 . If that was false then we want to see if newPage is equal to firstPage and if numPages minus newPage is equal to 0. If that is true, then pageStart equals numPages minus _numButtons, and then we call the function buildPageButtons.

After all that checking is done, we call the butState function to update the page buttons selected states. The we recalculate our itemStart, setting it to equal the (_maxItems times newPage) minus _maxItems. And we set our _itemsToShow variable to equal _maxItems times newPage.

More checking. If newPage is greater than curPage, and if _itemsToShow is greater than or equal to _numItems, then _itemsToShow equals _numItems.

However, if newPage is less than curPage, and if _itemsToShow is greater than _maxItems, then we create a var called mod which equals the modulo (remainder) of _itemsToShow divided by _maxItems. Now, if mod is greater than zero, then _itemsToShow minus equals mod and _itemsToShow plus equals _maxItems. What is happening here is that we are getting rid of the remainder we attached when we were moving forward through the pages.

One last check: if _itemsToShow is less than or equal to _maxItems and if _numItems is greater than or equal to _maxItems, then set _itemsToShow to be equal to _maxItems. This makes sure that we don't run past the value of _numItems.

Now that we have checked everything out and adjusted the variables accordingly, we package up the variables into a new instance of the statsArray. These are the items that we will need to have to properly update the other instance of our NextPrevPN, whether it is the top instance or the bottom one. Once the statsArray is ready, we use our Custom Event to pass it back to the document class.

The last thing we need to do is set our curPage variable to equal our newPage variable.

Next we'll look at the handler for our First Page, Prev, Next and Last Page buttons.

[as]
private function npHandler(e:MouseEvent):void{
    switch(e.target.label){
        case "Next >>":
            newPageBut++;
            newPage++;
            pageListener();
            break;
        case "<< Prev":
            newPageBut--;
            newPage--;
            pageListener();
            break;
        case "First":
            newPage = 1;
            firstPage = 1;
            newPageBut = 0;
            pageListener();
            break;
        case "Last":
            newPage = numPages;
            lastPage = numPages;
            newPageBut = (_numButtons - 1);
            pageListener();
            break;
    }
}
[/as]

We are using a switch statement, with the criteria to compare, being the button's label property.

In the first case "Next >>" we know that the Next button has been clicked. We add a value of 1 to the newPageBut and the newPage variables using the short form ++, and we call the pageListener function. You'll recall the the pageListener function had it's MouseEvent parameter set to equal null. Here is where we get to use that feature. Because the function is not being called through a MouseEvent, if that parameter was not set to have a default value of null, we would not be able to call the pageListener function from here.

In the second case "<< Prev" we know that the prev button has been clicked and we want to do the same things except that instead of adding a value of 1 to our variables, we are subtracting 1.

In the third case "First" we know that the First Page button has been clicked. Now we want to reset our variables to reflect our first page, so we make newPage equal to 1, firstPage equal to 1, newPageBut equal to 0 - yes, 0, because here we are wanting it to be the first item in an array and arrays start counting at 0. Now we call our pageListener function again.

In the fourth case "Last", we know that the Last Page button has beed clicked and we want to do the same things as in the First Page button, but we need to set our variables to reflect the last page. So, newPage equals numPages, lastPage equals numPages and newPageBut equals _numButtons minus 1. Again, remember that newPageBut is referencing an array.

Remember the statsArray we just packaged up? The next function we are going to look at makes use of that.

[as]
public function manualUpdate(pageSt:int, nPage:int, nPageBut:int, sItem:int, itemsTS:int, nBtn:Boolean, pBtn:Boolean, fBtn:Boolean, lBtn:Boolean):void{
    pageStart = pageSt;
    newPage = nPage;
    newPageBut = nPageBut;
    itemStart = sItem;
    _itemsToShow = itemsTS;
    nextBtn.enabled = nBtn;
    prevBtn.enabled = pBtn;
    firstBtn.enabled = fBtn;
    lastBtn.enabled = lBtn;
    buildPageButtons();
    butState();
}
[/as]

Back in our document class we have the public function reBuild(e:CustomEvent), which uses our CustomEvent to gather the information from the statsArray and then furthers that information to whichever of the two instances of the NextPrevPN class requires updating. The manualUpdate function is the receiver of that information. Notice that it is a public class. Using the access-control modifier "public" allows the function to be called from outside the NextPrevPn class. And here we see the true beauty of using classes. The instances of the NextPrevPN class we are using are called npT and npB, for top and bottom. Even though they are both products of the same class, they are essentially separate, so that when we call the function manualUpdate using the instance npB - npB.manualUpdate() - we are only updating the bottom instance's information, even though the same code is doing the work.

Back on track, we gather the incoming variables and transfer their information to our class variables and then call the buildPageButtons() function to update the visual information.
[as]
private function buildPageButtons():void{
    var j:uint = pageStart + 1;
    for(var i:uint = 0; i < _numButtons; i++){
        buttonArray[i].label = j;
        j++;
    }
    firstPage = parseInt(buttonArray[0].label);
    lastPage = parseInt(buttonArray[_numButtons - 1].label);
}
[/as]

Here we simply rebuild the page buttons menu using the updated information we receieved in the manualUpdate function. Once the menu is done, we update our firstPage and lastPage variables to reflect the current first and last pages.

The last job we do in the manualUpdate function is call the butState() function to update the selected status of the page buttons.

[as]
private function butState():void{
    if(curPageBut != -1){
        if(curPageBut != newPageBut){
            buttonArray[curPageBut].selected = false;
            buttonArray[curPageBut].setStyle("textFormat", butFormat);
            buttonArray[newPageBut].selected = true;
            buttonArray[newPageBut].setStyle("textFormat", butDownFormat);
        }
    }else{
        buttonArray[newPageBut].selected = true;
        buttonArray[newPageBut].setStyle("textFormat", butDownFormat);
    }
    curPageBut = newPageBut;
}
[/as]

The butState function looks after our selected toggle buttons. It uses the two variables curPageBut and newPageBut. You can probably guess what these variables are, but just for thoroughness I'll explain. The curPageBut holds the information of which button is currently selected. When a visitor clicks another button that becomes the newPageBut. So, in our butState function we first check to see that curPageBut is not equal to -1. If it were equal to -1 that would mean that there is currently no button selected, and therefore we would not want to deselect nothing. Now, if curPageBut does not equal -1, then we want to know whether or not curPageBut is equal to newPageBut. If curPageBut does not equal newPageBut then we can proceed to deselect the currently selected button by using the curPageBut as the index in the buttonArray and setting that button instance's selected property to false. Then we use the same technique to set the textFormat style of the currently selected button back to butFormat. Next we use the newPageBut as the index of the buttonArray and set that button instance's selected property to true and it's textFormat property to butDownFormat. If, on the other hand, it turns out the curPageBut and newPageBut are equal, then we do nothing. I am never quite sure why users click buttons that are already selected, but they do, so we must be prepared.

The else statement handles the situation where the curPageBut is equal to -1. In this example we initially set the value of curPageBut to 0 since we are showing the first page right from the get go, but there are other situations where you can use the butState function where none of the buttons are initially selected and then this will come in handy.

Lastly we set the curPageBut to equal the newPageBut so that we will know what to deselect the next time we visit the butState function.

And that's it for the NextPrevPN class. As per usual, if there is something I have not explained well enough or you have additional questions, my inBox is always open!

One Last Paragraph

Whenever I start writing one of these tutorials it always seems like it's going to take forever to put together all the files, zip files and demo movies, but when I finally get to this last paragraph, and I'm ready to click the Please Approve This Tutorial button, the first thing I always think of is, What I am I going to write about next? A glutton for punishment? Maybe. This time out I am going to ask you, the loyal readers of ActionScript.org, what you would like to see a tutorial about. Leave me suggestions in the comments sections, or write to me directly. I'll pick the most interesting suggestion, which hopefully will be something I don't know much about so that we can all learn something.