Notice: This material is excerpted from Special Edition Using JavaScript, ISBN: 0-7897-0789-6. The electronic version of this material has not been through the final proof reading stage that the book goes through before being published in printed form. Some errors may exist here that are corrected before the book is published. This material is provided "as is" without any warranty of any kind.
In Chapter 4, "JavaScript Objects," we learned about JavaScript arrays and their close relationship with JavaScript objects. That same chapter also taught us how to create functions to do meaningful work. In this chapter, we build upon that foundation and learn how to create complex arrays, functions, and objects. In the process of this exploration, we reveal some of the hidden power of JavaScript.
We begin with a discussion of variable declarations, and their relationship to the critical topic of function parameters and their validation. We also build up an extremely valuable function, known as word(), which is used in several subsequent efforts.
Associative arrays are one of JavaScript's most powerful features. They are also one of the most error prone. The second part of this chapter revisits the topic of associative arrays and examines extensions to them. Since arrays are the only way to store and access information in a database-like manner, we also learn how to fill arrays from lists and use extended arrays.
The final section of this chapter develops two important objects-the Image object and the Text object. The Image object stores a great deal of information about an image, and can also draw that image as a plain image, framed image, or linked image. The Text object stores text and also information about how the text is to be formatted. Both of these objects are developed as arrays of objects, similar to a small database. Both can also be easily extended for your own purposes.
In this chapter you learn about the following:
We have already explored the distinction between global variables, which are visible in every function, and local variables, which are only visible in the function in which they are declared. If you want to declare variables global to the whole window, define them outside of any function. Whether y precede the declaration with var or not it is still global. However, any declarations that you make inside of functions should be preceded with var if you want them to remain local. As you know, it is not strictly necessary to declare variables within functions. JavaScript treats any unknown variable it sees as a new variable, as if you had declared it. If you do not explicitly declare such a variable, or do not precede it with var, it becomes a window global variable. Check your functions very carefully for variable declarations. If you accidentally make a variable a global variable by not using var it can cause no end to trouble.
Tip. Particular care should be taken with variables having common names, such as i, j or iter. Always check the declaration of these variables carefully.
It is unfortunate that the rules for the use of var are confusing. Remember that placing var in front of a variable declaration restricts it to the scope in which it is declared. The scope of a variable is the context in which that variable can be used. In the case of a local variable declared in a function, the scope of that variable is the function itself. In the case of a window script, it is the window. If you do not use var in a function or a window script, the variable's scope is global-it can be used anywhere in the window.
Although Javascript does not force you to declare or initialize variables before you begin function statements, it is often very useful to do so. First, it allows you to document the purpose of each variable. This is not only good programming practice, it is also genuinely useful. When you return to a function six months after you wrote it you may not remember what the variable countme does, unless you explicitly document it as // array index variable. Second, if you explicitly declare each variable then you have the opportunity to initialize them to some meaningful default value.
The basic rules for JavaScript functions and their parameters and variables are described in the "Objects, Properies and Methods in JavaScript" section of Chapter 4.
If you declare global variables within a function, you must execute the function first for the variables to be defined.
Figure 9.1 illustrates this idea. When the first button is pressed an alert dialog box results, which tells you that there is an undefined variable. Since the variable was declared with var inside a function, it is local and "out of scope" to anything outside the function. The other three buttons all access global variables which have been defined outside of a function or within a function without the use of var. Pressing these buttons gives you the value of the variable.
Fig. 9.1 JavaScript alerts you when you have used an undefined variable.
Functions, particularly ones that carry out critical tasks like opening windows, should always check to see if their parameters are valid. This usually involves making sure that they are the correct type and/or are within some expected range. First and foremost, then, we need routines to determine if the parameters are valid. Strings are relatively safe as parameters, but numbers and booleans are not. This is because JavaScript often converts a non-string, such as 5, to the corresponding string "5," but it only converts to numerical or boolean values if it can. If a string parameter is specified as the number 5, no error occurs, because the numerical value 5 and the string "5" may be freely converted to one another. If a numerical parameter is specified as the string "five," an error very definitely occurs, because JavaScript does not know how to convert that string to a number..
If you discover a bad parameter, you have two choices. You can simply abandon all processing, or you can change the bad parameter to some safe default. It is often preferable to do the latter, and place an error message in a variable that calling routines can check. The library functions which we will develop later in this chapter use two of these; they are aWinErr and aStringErr. Of course, there is nothing you can do to keep the user from entering something totally unanticipated in a text field. The best you can do is try to limit the destruction that ensues.
Now let's examine a few functions from our repetoire to see how they work. Three checking functions are presented: one for numbers or restricted characters, one for booleans, and one for character encoding/decoding.
The function isIn, shown in listing 9.1, is multipurpose. It can check to see if all of the characters in a parameter string are also found within a comparison string. If the comparison string is empty, it uses a string of digits (0-9) as the comparison string. This function returns a boolean value to indicate if the comparison succeeded or failed. It can also strip unwanted characters from the parameter value and return a string that contains only acceptable characters.
Listing 9.1 A Multipurpose Parameter Checking Function function isIn(astr,nstr,strip) // astr : item in question // nstr : allowable character string; defaults to numbers // strip: determines whether return is true/false // or only allowable characters. Defaults to false. { //declare and initialize variables to make sure they stay local var cc='' var dd='' var bstr = '' var isit var i = 0 // make error string empty aStringErr = '' //force number to a string astr = '' + astr //default to checking for a number if (nstr== null || nstr == '') nstr = '1234567890' //make sure that 'strip' is a boolean strip = (isBoolean(strip)) //force to string; remember, this can return a boolean strip += '' //NOT a boolean--complain if (strip == 'false') { strip = false aStringErr = '"Value" must be (T/t)rue or (F/f)alse.' aStringErr += 'It is neither. Defaulting to false.' } //now that everything is set up, let's get down to business isit=false // begin loop which cycles through all of the characters in astr for (i = 0 ; i < astr.length ; i++) { cc=astr.substring(i, i+1) // begin loop which cycles through // all of the characters in nstr for (j =0 ; j< nstr.length ; j++) { dd = nstr.substring(j, j + 1) isit = false if (cc == dd) // so far so good { isit = true bstr += cc // accumulate good characters break // no need to go further } } // end of j loop //you found a mismatch; disqualify the item immediately //unless you are going to strip the string. if (isit == false && strip == 'F') break; else continue } // end of i loop if (strip=='T') return bstr // return stripped string else return isit // or return true/false (boolean) }
The isIn() function takes three parameters. The parameter astr is the item to be checked. The parameter nstr contains the set of characters which are permissible in astr. By default, nstr will be "1234567890" which will have the effect of checking to see if astr is a numerical quantity. The boolean parameter strip indicates if unacceptable characters (characters not in nstr) should be stripped out of astr.
The first portion of the isIn() function makes sure that astr and nstr are both strings, and initializes nstr to its default value if it was given as the empty string. It also insures that strip is boolean (the function isBoolean() is described under "Boolean Validation," below). The for loop then examines every character in astr. The substring function extracts each character into the local variable cc and checks to see if that character may be found in nstr using a second for loop. If any unacceptable characters are found the isit variable is set to true. This for loop is also responsible for building a new string, in the local variable bstr, which contains only acceptable characters.
If the strip parameter is true then the function will return the stripped string bstr at the end of its processing. An example of this is shown in figure 9.2. The characters 'l' and 'v' are out of range, and are removed. If strip was false then the function returns a boolean value which indicates whether the astr parameter contained any unacceptable characters. This case is illustrated in figure 9.3, using the same set of string values.
FIG. 9.2 JavaScript functions may convert unacceptable parameters to an acceptable form
FIG. 9.3 JavaScript functions may choose to reject parameters which are out of range
When we write code it is easy to forget exactly how we are supposed to pass a variable. One of the nice things about Javascript the language is that it is relatively untyped when compared with strongly typed languages like Ada or Pascal. In JavaScript the same function can return a Boolean or a string or a number with impunity. The calling routine may have some trouble though; it complains if you try to hand it a string when it thinks it should get a number. One way out of this dilemma is to forcibly convert the variable to the form you want. You can force an arbitrary value to be a string by concatenating it with an empty string. You can force an arbitrary value to be an integer with parseInt().
The "String Object" section of Chapter 5 discusses string conversion rules and methods in detail.
This kind of type confusion occurs frequently with boolean values. Should a function return true or "true" or "t" or "T" to indicate success? Listing 9.2 shows the isBoolean() routine. It takes the first character of a putative boolean, changes it to uppercase, and checks to see if it is "T" or "F". If the resulting value is either "T" or "F" then the function returns true or false, respectively. If the resulting value is neither "T" nor "F" then isBoolean returns false. Note that only the first letter of the astr parameter is examined, so that fear, Fortran and felicity will all be interpreted as false, while tundra and TECO will be seen as true.
Listing 9.2 A Boolean Validation Function function isBoolean(astr) //astr is the object to check { var isit='' astr +='' if (astr == null || astr == '') isit= false else { astr = astr.substring(0,1) // just get first letter astr = astr.toUpperCase() // make it caps if (astr != "T" && astr != "F") { // unacceptable value entered isit = false } else //returns value which caller can test for true/false //without having to do substrings, etc. isit= astr } //return is mixed: can be either a boolean or a string. return isit }
Most languages have a means of defining a character as a numeric code and, conversely, converting the code back to the character. The ASCII standard is often used for this conversion these days, just as EBCDIC was used 20 years ago. All these character encoding systems, such as ASCII, represent each character as a unique numeric value. In ASCII, for example, the space character has the decimal character code 32 (or 0x20 in hexadecimal), the letter 'b' has the decimal code 98 (0x62) and the punctuation mark ampersand '&' has the decimal code 38 (0x26). There are many such character encoding systems, some of also include characters from languages other than English.
Chapter 11 discusses the Unicode international character encoding standard in the section entitled "The Java Language."
In fact, if we had functions to convert between the character respresentation and the numerical representation then many forms of parameter validation would become much easier. For example, an isNumber function, which attempts to determine if a parameter is a number, becomes extremely easy to write. One just examines each variable's numerical ASCII code to see if it is in the range represented by the numerical codes for the characters '0' through '9'.
These two conversion functions are relatively easy to write. We first need to construct a string that contains all of the printable ASCII characters. We will construct such a string (called the charset string) with a utility function called makeCharsetString(). To convert from the character representation to the numerical representation we search for the character within the charset string. The numerical representation of that character is its index in the charset string, plus 32. The additional 32 is needed since that is the numerical code of the first printable ASCII character, the space character.
To convert from the numerical representation we reverse the process. We subtract 32 from the numerical value and then extract the character in the charset string at that location. Listing 9.3 shows the asc function, which converts from character to numeric code, while listing 9.4 shows the chr function, which converts from numeric code to character.
Note. Case always matters in character encoding. The numerical code for an uppercase letter will always be different from that of the corresponding lower case letter. For example, 'b' is 98 (0x62) in ASCII, while 'B' is 66 (0x42).
Listing 9.3 A Function to Return the ASCII Code of a Character ? function asc(achar) //achar character whose ascii code you want { var n = 0 var csstr = makeCharsetString() //get ascii char string //alert(csstr) n = csstr.lastIndexOf(achar) //printable characters begin at 32 with [space] return n + 32 }
Listing 9.4 A Function to Return a Character Given an ASCII Code From c9-testr.htm function chr(x) { var ar = '' var astr = makeCharsetString() //get ascii string //alert(astr) result = '' if (x >= 32 ) // printable { x = x - 32 ar = astr.charAt(x) result = ar } else // non printable, return text representation { if ( x == 9 ) result = 'tab' if ( x == 13 ) result = 'return' if ( x == 10 ) result = 'linefeed' } return result }
Note that there are some strategically placed alert() calls in these functions that are commented out. These alerts are for debugging pruposes, to ensure that the functions are actually delivering what you want. Note also that the chr() function does not handle most of the control characters, whose numeric codes are below 32. The only control characters which it tells you about are the tab, a return, and linefeed characters. This can be used in an ugly but useful way to insert a carriage return/linefeed combination into a string using the expression + chr(13) + chr(10).
There is one additional function we will present. This is the extremely useful word() function, which extracts an indexed phrase from a delimited string. A delimited string is one containing one or more entries separated by a special character referred to as the delimiter or separator. For example, the string "My:name:is:Hanover:Fiste" contains five components-My, name, is, Hanover, and Fiste-separated by the delimiter colon (:).
Note. Delimited strings are used frequently in JavaScript and other loosely typed languages. Delimited strings are convenient because they allow multiple elements to be represented as a single string. Many spreadsheet and database programs, for example, can export their data as delimited strings. The comma (,) and colon (:) characters are frequently used as delimiters.
The word() function is shown in listing 9.5. It takes three parameters: the delimited string inwhat, the delimiter sep, and the index which of the component required. It returns the component requested. Thus, if you ask word() for the third component of the string in the preceding paragraph, it returns "is." Figure 9.4 shows some sample output from the word() function. In this case we have used the '@' character as the delimiter and asked for the second item in the string. Note that the word() function uses 1-based indexing.
Listing 9.5 The Delimited String Processing Function word() function word(sep,which,inwhat) // separator character // which word/pharase // text in which to look { //alert(inwhat) var n = 0 // start of a phrase var wstr = 0 // holds substring var i = 0 // loop counter var s = 0 // start of winning phrase var f = 0 // end of winning phrase for (i = 1 ; i < which ; i++) { n = inwhat.indexOf(sep,n) // look for separator if (n < 0 ) // if you do not find it { return '' // return is empty string break // jump out of loop } n++ // otherwise, loop again } // now we should be a the right place if ( n >= 0) // ... but do this only if we { // found the separator //alert(n + '==' + wstr) var s = n // phrase starts with n, now s var f = inwhat.indexOf(sep,n) // get next instance of sep if (f < 0 ) f = inwhat.length // but if there is none ... wstr = inwhat.substring(n,f) // must be last phrase in string } //alert(f + '--' + wstr) return wstr // return string; it will be // empty if sep was not found. }
Fig. 9.4 The word() function extracts an indexed phrase from within a character delimited string.
The word() function does its job by using a simple algorithm. The first for loop in this function uses the string method indexOf() to find each occurrence of the delimiter character sep in the input string inwhat. The local variable n is used to hold the current position of the delimiter, while the iteration variable i counts the number of such delimiters found so far. If fewer than the required number which are found then the word() function returns the empty string.
If the required number are present then the if test just after the for loop will succeed. At this point n is positioned at the beginning of the element which we want; it is now necessary to locate the end of that element. This will either be the next occurrence of the delimiter, or the end of the inwhat string itself. The local variable f is used to hold the location of the end of the element. Once n and f have been computed the word() function uses the substring() method to store the desired element in the local variable wstr, which is then returned as the value of the word() function.
You may recall from chapter 4 that functions are developed by declaring them within a script. This section briefly reviews some of things to remember when writing functions, and then delves a little deeper into some of the fine points of functions in JavaScript. This material is developed further in the next section on associative arrays.
Make sure that you declare your most elementary functions earliest in the header script. This ensures that later functions are able to use them. The same rule applies to objects. Make sure that you do not reference any objects that have not yet been created in any of your functions. Functions in a header script cannot see objects created by the HTML on your page, nor can they see objects created by code executing later in the script. If there is something that must be done with an HTML generated object, place it in a footer script.
Proper error checking is also very important. If a function fails, provide a mechanism for the calling routine to detect the failure. If you can, try to keep the damage to a minimum. It is a sign of very poor design when the Netscape Navigator JavaScript Error dialog box comes up immediately after your page is loaded. One major problem with using functions has to do with passing the parameters in incorrect order. If some function expects three parameters that represent two numerical values and a string, but you give it a numerical value, a string, and then the second numerical value, an error very likely occurs. A similar problem arises when a calling function misinterpretes what should be passed in a parameter. If a function relies on unchecked input from a user, check that the parameter is at least of the right type. If you detect that it is incorrect, flag it, and, if you can, fix it.
Make sure that all local variables within a function are preceding by var. If a function inadvertantly declares a global variable which has the same name as a local variable in another function, inexplicable behavior will often result. In particular, function parameters may become garbled. This type of error is particularly difficult so diagnose, so particular care should always be taken with variable declarations.
Javascript cannot read (or write) local data files, but it can load and use HTML files. Therefore, even though you cannot create a JavaScript function library in an ordinary file, you can write a function library in an HTML header and load that library inconspicuously when you want to use it. Function libraries may also be used with frames, by designating on particular frame as the "owner" of the library. All of the other frame documents have a global variable called funcs, which refers to that special frame from the perspective of the calling frame. Consequently, you can call a function named myfunction in the function library by using the reference funcs.myfunction from anywhere in any of the nested frames. You can also make a function library in a frameset itself. Finally, some future version of the Netscape browser will have the ability to load JavaScript files using the SRC attribute of the SCRIPT tag, much as images are loaded.
Chapter 20 provides a comprehensive example of using a function library with frames.
Note that the functions given here are meant to be introductory only. Since Javascript, at the moment, lacks many of the functions that have become standard in most programming languages, we have provided you with a library of some commonly used functions.
If you are in that document, clicking the tile image in that frame pops up a window with all of the function declarations and variable definitions.
We have used the construct "Javascript: xxx" as a replacement for a URL reference in linked text or images. We can also use this idea to create a one line scratchpad to execute code any time we want. First of all, you can attach just "javaScript:" to a link When this link is exercised a window pops up with two frames-a text widget on the bottom and a blank window on top. If you type some script into the text box, and then hit Return, the text is executed. You can even rewrite the document in the top frame.
Note. The "javascript:" construction does not work uniformly on all platforms. In addition, there are behavioral differences between Netscape version 2.0 and version 2.0.1. Use this technique with caution.
Chapter 8 describes the use of the "javascript: " construct in several places. See the "Links" and Anchors" sections in particular.
Rather than writing script code, there is an even easier way to get the same behavior. Just type "javascript:" in the location display and hit Return. Up pops the scratchpad! In fact, you can even type your code directly into the location text box and have it be executed. Figure 9.5 illustrates this simple JavaScript scratchpad.
Fig. 9.5 The Javascript one line scratchpad is created with the "javascript:" URL.
In HyperCard, one can write Hypertalk in a text field and then 'do' the field, which executes the contents of that field. Because of this your HyperCard scripts can write and execute code on-the-fly. You can do something similar in JavaScript using eval(). Although eval() sounds like it should be used only to evaluate mathematical equations this function can actually do much more. It can evaluate any string that is a Javascript statement. Try this experiment. Create an HTML button and attach the following statement as its onClick event handler:
eval('alert("I did it!")')
When you click the button the alert box pops up. In the same way, you can pass the contents of an HTML text field or textarea to a JavaScript event handler. This is illustrated by the function evaluate(). This function consists of a single line of code, eval(what). A single HTML button arranges to pass the contents of a textarea to this function, which promptly tries to execute the contents of that textarea as JavaScript code. The eval statement can be placed directly in the button handler, of course. By placing it in the separate (but trivial) evaluate function we make it easier to extend in the future. Figure 9.6 shows this simple textarea evaluator at work.
FIG. 9.6 The contents of a TEXTAREA can be evaluated as JavaScript code using eval.
Associative arrays were introduced in chapter 4 and have been used in several previous chapters in this book. You are already aware that an associative array is a one dimensional array of pairs. You access the left member of the pair with a numeric index. You access the right member of the pair with a string index equal to the value of the left member of the pair. For example, (left) myArray[1] = "red" but (right) myArray["red"]= "FF0000". Arrays must be explicitly created by a function that takes a generic this object and gives it a size. You can also create other properties for the array in this function. To actually create the array, you use the new operator together with our array creation function. For example the statement
myNewArray = new createArray(6,'')
creates an array called myNewArray with 6 elements and initializes all of the left members to ''. Note that this is not being done for you by JavaScript in some magical way. You have to write the creation function and then invoke it with the appropriate parameters.
An array is a primordial Javascript object. Its properties, which represent the array members, can be anything. You must set the special property known as size, however, in your creation function. This property gives the length of the array (and is occasionally referred to as length for that reason). The size property is usually put in the zeroth element of the array during initialization. This means that your arrays will actually have n+1 elements in total: n elements for data, and one extra element at index zero to hold the array size. You need to be sure that your array access functions do not overwrite the zero element. Listing 9.6 shows a general purpose createArray() function.
Listing 9.6 A General Purpose createArray Function function createArray(n, init) { this.size = n //This initialization is absolutely necessary for (i = 1 ; i <= n ; i++) { this[i] = init //Initialize all of the left hand elements } return this //Return the array object to the caller }
Notice that there is no initialization of the right members of the array. You can also arrange to do this in the createArray function, but only with some effort. This is because the left element must be unique, and we have initialized all the array elements to the same value, namely the value init. If you have two array members containing the same value, you are only able to get to the first one.
In addition to creating arrays, it is often desirable to be able to reset or clear an array so that it may be reused. A special procedure, known as a double replacement scheme, must be used to clear or reset an existing array. (You can always create a completely new array, of course.) This special approach is needed because you have no way of knowing what values are already stored in the array. In particular, you have no way of knowing that they are unique. The double replacement method uses the following loop to safely reset the array myArray:
myArray[i] = '@@@@@' myArray['@@@@@'] = '' myArray[i] = ''
This method uses a special dummy replacement value to manipulate both the left and right sides of the pair. In the preceding example, the string '@@@@@' is used. To avoid the problem of non-unique indices, this dummy value must be highly unusual so that itis extremely unlikely to actually appear in the array.
Do not try to initialize the right side of an array in the same loop in which you initialize the left side. Javascript mixes left and right values for you. If you need to initialize the right side then do it in a separate loop.
Associative arrays occur far more often than you might think. They even occur in everyday life, although most people do not think of them that way. The picture on the top of a TV dinner box is related to what is in the box. You choose your dinner by looking at the picture because the picture conjures up thoughts of what is in the box. You would not open every box in the freezer and examine its actual contents in order to decide which one to put in the microwave. Programmers tend to think of an association in somewhat less colorful terms such as a = b, x = 3y2, and so on. A Windows.ini file is an excellent example of the use of associative pairs. Every entry has a left element, such as *.doc, and a right element, such as c:\winword.winword.exe. In this case, the association is a relationship between a file suffix and the application that created it. Hypertext links are also associations; they are just ordered backward. The following HTML:
<A HREF='http://www.myfavoritelink.com'>My favorite link</A>
is actually an associative pair. If we were to place this into an array, most of us would place My favorite link on the left side, and 'http://www.myfavoritelink.com' on the right. We often reference complicated, large, and sometimes obtuse objects with less complicated words or nicknames.
A simple associative pair array may not be sufficient for your needs. Since an array is just an unstructured object, we can conveniently make it into a more complex object. For example, we can add a description property to an array. This is useful because the array may have a short name that is easy to use elsewhere in our code, but that name may not be very illustrative of the array's purpose.
Another property we might want to add is a property that reflects the "element of interest right this minute" or the "current" element. An example of this might be the strings in a listbox. The current property could refer to the currently selected item. This might be called the currentIndex property, or, more tersely, the nDx property. (Remember that Javascript is case-sensitive.) Finally, if we are using the list as some kind of a stack, or if we are keeping track of items that are constantly being added or deleted from the list, we might need to know where the next open slot is located. We will call this property the nextIndex property.
But where do we put these properties? Well, properties are just array elements, so the question is where in the element list they should be placed. If we put them at the beginning, the array elements proper do not start at index=1. If we put them at the end, the array elements are in the right place, but it is now more difficult to increase the size of the array. This is because there are referencing problems if you access the properties by their array index, rather than by their names. We will examine the advantages and drawbacks of both approaches.
While Netscape's description of arrays and their capabilities is glowing, there are some pitfalls in using arrays. Most of them stem from a lack of initialization or from incorrect initialization. Various array functions are given that present different approaches to the location of the enhanced array object properties, such as currentIndex and nextIndex, and how (or if), the array is initialized.
The page generated by this file has buttons to allow you to initialize various arrays, reset them, clear them to null, enter a single value, and fill the arrays. Each array is treated in the same fashion. After you have exercised one or more of these functions you can then look at the contents of each array to see the effect. The array creation functions are organized into two catgories: those that place the enhanced properties at theend , and those that place the enhanced properties at the beginning. The first four array creation methods place the enhanced properties at the end of the array and initialize all empty slots to "@". They differ in how they handle the enhanced properties.
The final three creation methods place the enhanced properties at the beginning of the array. This set of methods can be used to examine the consequences of not initializing the array, or initializing all of the elements (including those of the special properties) to the same thing, and of initializing only the empty elements. These functions are obviously not the only possible ways in which such array functions can be written. Listings 9.7 and 9.8 show two of these functions. Notice that it is not possible to create an uninitialized array and still place the extended properties at the bottom.
Listing 9.7 A Function to Create an Array with Special Properties Added at the End function createArray1_d(n,init) { var i = 0 this.length = n + 3 for (i = 1 ; i <= n ; i++) { this[i] = init } this.description = 'desc' this.nDx = 'nx' this.nextIndex = 'ni' return this }
Listing 9.8 An Array Creation Routine with Extended Properties at the Beginning function createArray3(n,init) { var i = 0 this.length = n + 3 this.description = 'dc' this.nDx = 'nx' this.nextIndex = 'ni' for (i = 4 ; i <= n + 4 ; i++) { this[i] = init } return this }
When you first load the c9-4.htm page all of its arrays have been created; those arrays which initialize themselves have done so. Initialization has been done only for the left elements. No initialization has been done for the right elements. The viewing routine has been set to look at the first unused element. This will be the element at index n + 1, which is not yet processed or initialized in any way.
For purposes of this discussion, we call those array elements that are not directly related to a property the empty elements. Those that are associated with properties are called the special elements. Each element consists of a left and right element. The left element of the zeroth element of the array has the array size in it. Note that the array size does not include the zeroth element itself, so that if the array size is 100, the array actually has 101 elements (the 101st being the size element). Said another way, the size includes elements 1...array.size. You should structure your for loops so that you iterate from element 1 up to and including element array.size.
Platform dependencies have been reported in the current release of Netscape Navigator. You may find array functionality to be different depending on the platform on which you are running the Navigator. Try the code in c9-4.htm and c9-4x.htm as test cases.
First, let's look at the those arrays that have the special elements at the end of the array. Click the 1a button. This creation routine initializes all of the empty elements to "@" and the special elements to "". Did you expect to see the property names in the special elements? They do not appear here. They are properties associated with the array object, not values within the array object. This creation scheme works as you would expect, except that you might expect the right element of anArray[0] to be undefined. It appears to be merely empty instead.
Before you breathe a sigh of relief, let's look at the next array creation method (associated with button 1b). Because we could not tell which element was which, we decided to initialize the left side of each special element with the name of the property associated with it. We did not do anything to the right side of the properties...well, we didn't, but Javascript did! Notice that the right element has also been filled in with the name of the property, as shown in figure 9.7. This should provide your first clue that Javascript arrays seem to have a mind of their own. Further, the right side of anArray[0] has the last special property assignment in it.
FIG. 9.7 JavaScript often fills associative array values autonomously.
Button 1c manipulates an array in which all of the special elements have been initialized to the same value as the empty elements. There are no surprises here, except for the "@" put into the anArray[0] right side element. The final creation method of the first series initializes the special elements to two unique characters for each related property. This creation routine is also the most well-behaved of all, as shown in figure 9.8. The presence of the last special element assignment on the right side of anArray[0] is no longer a surprise.
FIG. 9.8 Initializing a JavaScript array to unique values works best.
The three creation methods in the second series put the special elements at the beginning of the array. Using the first of these three methods it is possible to produce a completely uninitialized array. Still, nothing unusual happens; the empty elements are just null, as shown in figure 9.9. The second of these creation methods initializes the special elements, except that the initialization spans the whole array instead of just the empty elements. The very last creation method is designed and behaves properly, although, once again, anArray[0] is mysteriously filled in by JavaScript.
FIG. 9.9 Accessing a completely uninitialized array yields null values.
Instead of clearing or reinitializing the arrays at this point, click the rightmost button named Enter One Value/Array. This button does exactly that. It enters a "red","FF0000" pair into anArray[2] for those arrays with special properties at the end and into anArray[6] for those arrays with special properties at the beginning.
The array created by method 1a offers up no more surprises in this case. The value pair has been entered in the correct place. Notice that the array uses one-based indexing in terms of reference as well. The arrays created by the other methods with the special elements at the end (methods 2 through 4) are also well behaved. However, when we get to the first of the methods that puts the special elements at the beginning, trouble appears. The data pair is placed in anArray[6] but the "FF0000" is also placed in the left element of anArray[7], as shown in figure 9.10.
FIG. 9.10 Using an uninitialized array can overwrite array elements; once it is initialized all is well.
It is worthwhile to note that this array is the one that was not initialized. To see this effect, reload the page. If you initialize the array in any way, even to null, it becomes well behaved. This should be a strong indication that initialization is always a good idea. The next two methods behave well within the array proper; notice, however, that an "FF0000" has been placed in the left element of the first unused element.
To continue our array experimentation, let's reinitialize the arrays, preferably by reloading the file. Then click the Fill Arrays button. This fills the empty elements of the array with pairs in the form of name=month. Repeat your inspection process. Array 1a is filled properly but now an "FF0000" has appeared in the first unused element. The same is true for the other three arrays which have their special properties last. As might be expected, the uninitialized array with special properties first fails to perform as desired, as shown in figure 9.11. The other two arrays with properties first behave well, except for the standard quirks: the left element of the last array element ends up in the right element of anArray[0] and the left side of the first unused element is set to the right side of the now discarded color pair. Clearing the array solves all the problems associated with not being initialized, as shown at the bottom of figure 9.12.
FIG. 9.11 Entering data into an uninitialized array scrambles the values
Fig. 9.12 After an uninitialized array has been cleared all reference problems are resolved.
You can perform a variation on this experiment by reinitializing the arrays and then clearing them, using either method. Use the Initialize and Clear (or Clear to Null) buttons, and then add a single value to all of the arrays, or simply fill the arrays. The 0 element and first unused elements remain quirky, but the array elements themselves behave properly.
The companion page in the file c9-4x.htm provides a variation on the same theme. It is the same as the code in c9-4.htm, except that all its arrays have an extra uninitialized element, just after the original eight array elements. As before, a first unised element is displayed. If you go through the same procedures, you see that this extra element causes even more confusion.
Initialize your arrays before you use them! Uninitialized arrays cannot be trusted to provide correct results when referenced. They cannot even be trusted to provide consistently incorrect results, and can be a nightmare to debug.
Listing 9.9 is worth special attention. It provides a function named fillArrayFromLists() which will fill the associative pairs of an array from two parallel lists of delimited elements. It is a lot faster to write such lists than it is to specifically set each right and left element in an array. This function uses the library function word() which is described earlier in this chapter. You may recall that this function uses a character delimiter to separate the elements in the list. The separator can be any character; the most common one is a comma (,). HyperCard enthusiasts will recognize this as an item list.
Listing 9.9 A Function to Load an Associative Array from Two Delimited Lists function fillArrayFromLists(anArray,aaList,bbList,sep,s,f) { var lstr = '' // left hand array element var rstr = '' // right hand array element var i = 0 // iteration variable var counter = 1 for (i = s ; i <=f ; i++) { anArray[i] = word(sep,counter,aaList) anArray[anArray[i]] = word(sep,counter,bbList) counter++ } }
This function takes six parameters: the array anArray whose values are to be set, two delimited strings aaList and bbList which hold the right and left elements, the string delimiter sep, and the first s and last f indices in anArray which are to be set. The function executes a simple for loop over all the array indices from s to f inclusive. In each iteration it calls the word() function twice, to extra the right and left elements from aaList and bbList.
As a demonstration of the power of this function, let us invoke it using two short lists. In our case, we use the asterisk character (*) as the delimiter. The following code shows each person on the list aList being associated with a particular month, as given in the list bList:
var aList = 'Mona*Jane*Barbara*Sandra*Maxine' var bList = 'January*March*September*February*December' fillArrayFromLists(array1,aList,bList,'*',1,5)
Now that we have created our enhanced array object and dissected it at great length, how can we use it and which version should we adopt? Although most of the creation methods we have tested work if the array is properly initialized, we will use the final version of the methods which place their special properties first in our subsequent code. This is the one that did not overwrite our special properties. Most of the time only the left elements of the associative pair are used, but these lists can also hold an associated object (right element).
Tip. The enhanced array object may be used to implement a string list, object list, or a stack.
One array which arises frequently is JavaScript applications is an array of newly created child windows, since JavaScript does not provide such an array by default. Listing 9.10 shows the function winArrayAdd(), which can be used to add a window to such a window array. Note that this function uses global variables to keep track of the next available array slot (in nextWin) as well as the current window (in curWin).
Listing 9.10 A Function to Add a Window to a Window Tracking Array function winArrayAdd(aWinHdl) { // set next open slot in winArray to hold this window handle // and that windows creator aWinArray[nextWin] = aNewWin aWinArray[aWinArray[nextWin]] = aNewWin.creator curWin = nextWin // make this the current window nextWin++ // increment next available slot pointer }
Instead of using global variables we can, of course, use an array with special properties as well. The code in listing 9.11 illustrates this form of the window addition function. While listing 9.11 does not look much different from 9.10, the latter version is the preferred one. This is because it keeps essential window tracking information together. Other functions may therefore access all the relevant data by simply examining appropriate elements of the array, rather than having to look at the array and also consult some global variables.
Listing 9.11 A Better Version of the Window Add Function Using Special Properties function winArrayAdd(aWinHdl) { // set next open slot in winArray to hold this window handle // and that windows creator aWinArray[aWinArray.nextIndex] = aNewWin aWinArray[aWinArray[nextWin]] = aNewWin.creator aWinArray.ndx = aWinArray.nextIndex // make this the current window nextWin++ // increment next available slot pointer }
One of the biggest criticisms of associative arrays is that they are one dimensional. The argument can be made that the left and right sides constitute a second dimension, of size two, but this is not really a true multi-dimensional array. In fact, two-dimensional arrays are really just arrays of arrays. Because of the nature of associative arrays, one can develop such complex structures with relative ease.
Imagine, if you will, an array of colors. In a simple implementation of such an array, the left element contains the name of the color, such as "red", and the right member contains the hexadecimal value for the color. For red this value is 0xFF0000. There are many shades of red, however, and Netscape has even named a few specific ones. Of course, we could add all of these variant names to the array. It would be much nicer to be able to go to an array, find "red," and then access into a list of colors which were various shades of red. Let's now construct an example in which we can do that. As usual, the left element is initialized to "red". The right element, however, holds a handle to another array called moreReds. The array moreReds contains the typical colorname=hexvalue pair. Listing 9.12 shows a very simple-minded way of creating such a multi-dimensional array.
Listing 9.12 A First Approach to Creating a Multi-Dimensional Array colors = new createArray(9,'') moreRed = new createArray(20,'') moreYellow = new createArray(20,'') moreOrange = new createArray(20,'') moreBrown = new createArray(20,'') moreGreen = new createArray(20,'') moreBlue = new createArray(20,'') morePurple = new createArray(20,'') moreGray = new createArray(20,'') moreWhite = new createArray(20,'') ...
Not only is this tedious, but it's not even a complete solution. We have to put each of these secondary arrays into the right elements of the first array. There must be an easier way to do this. We need to develop an approach to getting the various values into the multi-dimensional array and getting them out again.
Listing 9.13 contains three functions. The createArray() function is very similar to the array constructor we have already seen. The fillcolorArray() function fills both the left and right elements of an associative array, which is passed in as a parameter. This means that if you execute the statement
anArray = fillColorArray(anArray,'green','gr')
the left elements are filled with green1, green2,..., and the right elements are filled with gr1, gr2,.... The third function, lotsOfColors(), is used to orchestrate the creation and initialization of this array. This function also uses our old friend the word() function.
One might think of using eval() to metamorphose a constructed string into an array handle, as in the following statement:
dstr = eval(dstr = new createArray(20,''))
In fact, this is not necessary. Here we see another tribute to the flexibility of JavaScript variables because the following statement works just fine:
dstr = new createArray(20,'')
Try this in C or Pascal! The resulting value of dstr is then passed to fillColorArray() to be stuffed with values. When this function returns, dstr is plugged into the right value of the current array pair.
Listing 9.13 Three Functions Used to Create an Associative Array of Associative Arrays function createArray(n,init) //n size of array //init what you want all values initialized to { var i = 0 this.length = n // set the size of the array for (i = 1 ; i < n ; i++) { this[i] = init // fill the array with "init" } return this // return the newly created array } function fillColorArray(anArray,init,init2) // anArray is the array to be filled // init holds the values for the left side // init2 holds the values for the right side { var i = 0 var astr = '' var bstr = '' var n = anArray.length // get array length for (i = 1 ; i <= n ; i++) // iterate over each element { astr = init + i // get left value bstr = init2 + i // get right value anArray[i] = astr // set left anArray[astr] = bstr // set right } return anArray // return modified array } function lotsOfColors() { var cstr // colors will be an array of 9 colors // colorstring will be the names of those colors, delimited by commas var colors = new createArray(9,'') var colorstring = 'red,yellow,orange,brown,green,blue,purple,gray,white' for (i = 1 ; i <= 9 ; i++ ) // iterate over array { cstr = word(',', i , colorstring) // extract the i-th element colors[i] = cstr // set left value dstr = 'more' + cstr // create and initialize right value in next two statements dstr = eval(dstr = new createArray(20,'')) dstr = fillColorArray(dstr,cstr,cstr.substring(0,2)) colors[colors[i]] = dstr // set right value // next three statements are for debugging // they display the values set in an alert astr = colors[cstr][3] bstr = colors[cstr][astr] alert( astr + '\n' + bstr) } }
The function lotsOfColors() warrants close attention. It starts off by creating the associative array, colors, as well as a comma delimited string that lists the colors it uses. It then cycles through a loop for each color to be processed. It first extracts the color we want from colorstring (as a string) and then sets the left element of the current associative pair to that string. Thus colors[1] yields "red". That's the easy part. How do we name the array handles that we place into the right member? We can construct an element named "moreRed" but the "moreRed" is a string. Fortunately, JavaScript permits a string to be enough of a chameleon that it can be turned into an array handle.
Arrays should be initialized and filled in different routines. If you attempt to perform these operations in the same routine; strange substitutions, which are symptomatic of an uninitialized array, will result.
So far, we have not tried to access the secondary members of this construction. Surprisingly enough, that is not much harder than accessing the usual one-dimensional associative array. In fact, the debugging code at the end of lotsOfColors() already shows how this is done.
astr = colors[cstr][3] bstr = colors[cstr][astr] alert( astr + '\n' + bstr)
The variable astr holds the left element, and bstr the right element. If you attempt to do this in one step it appears confusing. If you substitute the value of astr in the second expression, you get the following massive expression:
bstr = colors[cstr][colors[cstr][3]]
Note that we placed some convenient, but arbitrary, values into our subsidiary arrays. We could have had the fillColorArray() function generate successive, properly spaced hex values that were within the appropriate color range. In general, it is more likely that you will want to use these kinds of arrays to keep arbitrary data.
This section reviews some of the fundamental concepts of HTML objects in JavaScript, and then proceeds to the more complex Image and Text objects described at the beginning of this chapter. By this point you are well acquainted with the various HTML objects in JavaScript. The focus of this section is to explore some of their innovative uses. Many of the "tricks" of JavaScript revolve around its polymorphism, namely its capability to view a single thing in different ways.
JavaScript strings provide an excellent example of this polymorphism. Strings in JavaScript can be thought of as HTML objects, in addition to their usual meaning. This is because JavaScript provides methods for giving strings many of the formatted characteristics of HTML text, such as bold (<B>), italics (<I>), big (<BIG>), link (<A>), and so on. When you use a construction such as
mystring = 'This is some text.' mystring = mystring.bold,
mystring becomes '<B>This is some text.</B>'.We will use methods such as these in the Text object we create.
HTML String methods are discussed in great detail in the "String Object" section of Chapter 5.
By now you are intimately aware that objects are just arrays of properties. However, properties are accessed somewhat differently than array elements, and they usually only use the left side of the array. New properties can be added to an object at any time. This is possible because only the left element of the array is used. Trying to add new array elements on-the-fly when you are using both sides of the array element is fraught with disaster, as we have seen previously.
Objects can have properties and/or methods. Methods are simply functions that have been declared as properties. For example, you might have a color object. Colors are cited in terms of their red, green, and blue components (at least in browsers; there are other color mixing schemes). Many of Netscape's colors also have common names. To declare a color object, we must make a constructor/creator, as shown in listing 9.14. This is no different than the type of construction methods we have seen for arrays.
Listing 9.14 Creating a Simple Color Object function createColor(name,red,green,blue) { this.name = name this.red = red this.green = green this.blue = blue this.length = 4 return this } myGreenColorObject = new createColor('green','22','DD','22')
Object constructors really just reserve space for the property array elements. We can access the property values by their array indices, as well as by using the dot operator (.). This is sometimes extremely useful for storing objects. To convert the color object into a string delimiter with the '@' character the code in listing 9.15 might be used. This type of string is often useful for storing objects persistently, as we shall see in Chapter 20.
Listing 9.15 Storing an Object's Properties as a Delimited String function storeColor(aColorObject) { var k = aColorObject.length // number of elements in object var astr = '' // initialize to empty string var i = 0 for(i = 1 ; i <= k ; i++) // iterate over elements { astr += aColorObject[i] + '@' // append to astr. } return astr // return delimited string }
You can easily reverse this process, as listing 9.16 shows. The function getColor() undoes the work done by the function storeColor of listing 9.15. It tears apart the delimited string passed as its second argument astr, and sets the properties of aColorObject accordingly.
Listing 9.16 Retrieving an Object's Properties from a Delimited String function getColor(aColorObject,astr) { var lastloc = astr.lastIndexOf('@') // final delimiter var n =0 // number of delimiters found var f = 0 // location of current delimited var i = 1 // current array index while ( n >= 0 ) { // find next delimiter, store in local variable f f = astr.indexOf(astr,n) // set the i-th array element to the string between the last delimiter // ( at location n ) and the current delimiter ( at location f ) aColorObject[i] = astr.substring(n,f) // if we are at the very last delimiter ( at location lastloc ) if (f == lastloc) { // then take the substring from the current position up to the end // of the string and break out of the while loop aColorObject[i+1] = astr.substring(f+1,astr.length) break } // if we are not at the very last delimiter then repeat the process else n = f + 1 // update array index i++ } }
These examples are not the only ways to perform either of these operations. The storage function storeColor() of listing 9.15 could also have been implemented as shown in listing 9.17, for example. We could also use the word() function to retreive the properties of an object from a string in the getColor() function.. This would be slower than the approach shown in listing 9.16, however.
Listing 9.17 An Alternative Color Storage Function using the for..in Statement function storeColor(aColorObject) { var astr = "" for (var i in aColorObject) astr += aColorObject[i] + '@' return astr }
It would be nice if the color object knew how to do its own conversion to and from the delimited string representation. Since we have already written the essential conversion functions, it's easy to make them methods of the color object: we just assign them as properties of the object itself. If we use this approach our color object constructor is as shown in listing 9.18.
Listing 9.18 Creating a color Object with Methods And Properties function createBetterColor(name,red,green,blue) { this.name = name this.red = red this.green = green this.blue = blue this.toString = storeColor this.fromString = getColor this.length = 6 return this } myGreenColorObject = new createBetterColor('green','22','DD','22')
Note. Always set the length property of an object in your constructor. Some implementations of JavaScript will automatically create a length property in the element at index=0, and keep that length property updated as new elements are added, but this behavior is not guaranteed.
If you are going to convert a function into a method, you can (and should) alter it somewhat to exploit the fact that it is now a method function. If a function is not a method function of an object, then it may only access that object if the object is passed as a parameter to the function, or if the object has been declared globally. Once you make the function a method of the object, the function can reference the object that contains it via the keyword this. Consequently, we can rewrite the storeColor method to look like listing 9.19.
Listing 9.19 Storing an Object's Properties Using a Method Function function storeColor() { var k = this.length var astr = '' var i = 0 for(i = 1 ; i <= k ; i++) { astr += this[i] + '@' } return astr }
In order to change a function into a method function of an object, follow these three steps:
- Make the method a property of the object, as in aColor.toString = storeColor.
- Do not pass the object as a parameter to the function.
- Change all explicit references to the object to the keyword this; for example, l = aColorObject.length becomes l = this.length.
Drawing images is a complex affair. One of the most important things to remember about image manipulation in terms of JavaScript is that all images must be characterized by HEIGHT and WIDTH modifiers in their HTML tags. If you leave these modifiers out, JavaScript misbehaves or doesn't function at all. We will create an Image object that encapsulates all the important Image properties, including the height and width. The Image object will also know how to display itself in several contexts. The constructor for the Image object is shown in listing 9.20.
Listing 9.20 A Comprehensive Constructor for an Image Object ? function createImage(title, filename, height, width, vspace, hspace, border, bordercolor, frame, framecolor, href, notes) { this.title = title this.filename = filename this.height = height this.width = width this.vspace = vspace this.hspace = hspace this.border = border this.bordercolor = bordercolor this.frame = frame this.framecolor = framecolor this.href = href this.notes = notes this.draw = drawImage this.frame = frameImage this.reference = referenceImage this.popup = popImage return this }
If you examine the constructor for the Image object shown in listing 9.20, you see that it not only encapsulates the normal HTML qualifiers for an image, but it also has properties for a file spec, a title, an associated URL, and notes. It can present itself as a plain image, a framed image, or a linked image. It can also pop itself up in a window of its own. The image "draws" itself by presenting you with a string that you can send to a nascent document using document.write(). The code in listings 9.21, 9.22, and 9.23 shows the methods used to draw a plain image, a linked image, and a framed image.
Documents created on-the-fly, also known as nascent documents, are discussed thoroughly in the "Nascent Document" section of Chapter 8, "Dynamic HTML and Browser Objects."
Listing 9.21 The Image Object's Plain draw Method function drawImage(how,border) { var astr = '' // if the image does not use this modifier // the parameter can be empty or '^' astr = '<IMG SRC="' + this.filename + '"' if (how != '') astr += ' ALIGN=' + how if (this.height != '') astr += ' HEIGHT=' + this.height if (this.width != '' && this.width != '^') astr += ' WIDTH=' + this.width if (this.vspace != '' && this.vspace != '^') astr += ' VSPACE=' + this.vspace if (this.hspace != '' && this.hspace != '^') astr += ' HSPACE=' + this.hspace if (this.border != '' && this.border != '^') astr += ' BORDER=' + this.border astr +='>' if (this.border != '' && this.border != '^') astr = '<FONT COLOR=' + this.bordercolor + '>' + astr + '</FONT>' return astr }
Listing 9.22 The Image Object's Linked draw (Reference) Method function referenceImage(how,border,ref,atext) { if (ref == '') ref=this.href if (ref == '') ref = location.href if (atext == '') atext = 'Your text here!' var astr = '<A HREF=' + ref + '>' astr += '<IMG SRC="' + this.filename + '"' if (how != '') astr += ' ALIGN=' + how if (this.height != '') astr += ' HEIGHT=' + this.height if (this.width != '' && this.width != '^') astr += ' WIDTH=' + this.width if (this.vspace != '' && this.width != '^') astr += ' VSPACE=' + this.vspace if (this.hspace != '' && this.width != '^') astr += ' WIDTH=' + this.hspace if ('' + border != '') astr += ' BORDER=' + this.border astr +='>' astr += atext astr += '</A>' return astr }
Listing 9.23 The Image Object's Framed draw Method function frameImage(how,border,leading) { var astr = '<TABLE ' if (how != '') astr += ' ALIGN=' + how if ('' + border != '') astr += ' BORDER=' + border if ('' + leading != '') astr += ' CELLSPACING=' + leading astr += '><TR><TD ALIGN=CENTER>' var bstr = '</TD></TR></TABLE>' astr += this.draw('',2) astr += bstr return astr }
Notice that the frameImage method essentially draws the table structure, which is the "frame," and then calls the drawImage method to draw the image. In all cases, the image can have a border. In both the drawImage and frameImage methods, the border color is determined by the current font color. Since the Image object encapsulates the border color, it is as easy to include it as it is to include any other modifier. Referenced image borders, though, do not have such a choice; they will be the link color or vlink color specified for the page.
You can use the Image object to make a database of your images. A filename, notes (which can include a caption for the image), and a URL are included as part of the object, as we have seen. Listing 9.24 arbitrarily sets up a trivial database of six Image objects held in an array. It can present these images as plain, framed, or referenced. It can also pop up any image in its own window. Listing 9.24 also shows the method function that creates the pop-up.
Listing 9.24 The Pop-Up Method of the Image Object function popImage(title) { var w = 50 + parseInt(this.width) // image width as displayed var h = parseInt(this.height) + 50 // image height as displayed scrl = 'no' // do scrolling? if (w>640){w=640;scrl='yes'} // if width too big them scroll if (h>480){h=480;scrl='yes'} // if height too big then scroll // create HTML attributes for the image var whstr ='WIDTH=' + w + ',HEIGHT=' + h + 'RESIZABLE=yes,SCROLLBARS=' + scrl // open a new window aNewWin = self.open('',title,whstr) // if the new window could not be created... if (!aNewWin) { // notify the user that it failed, and try to indicate // possible causes var alertstr = "Could not open a new window." alertstr += " A window of named " + title alertstr += " may already be open." alertstr += " You may also be out of memory" alert(alertstr) } // if new window was created successfully... else { // create the HTML to display the image var astr = '<HTML><HEAD>' astr += '<BASE HREF="' + location.href + '">' astr += '<TITLE>' + this.title + '</TITLE>' astr += '</HEAD><BODY>' // write out that HTML aNewWin.document.write(astr) var bstr = '<CENTER>' + this.draw('CENTER') + '</CENTER>' aNewWin.document.write(bstr) aNewWin.document.write('</BODY></HTML>') // close the document aNewWin.document.close() } }
The resulting image display is shown in figure 9.13. The page generated Listing 9.24 is covered with pop-up windows containing its tiny database of six images. The images were placed in the pages arbitrarily; the page has no input interface. Any single image can be popped up by placing its number in the text box next to the Image button and clicking the button. Figures 9.14 and 9.15 show the plain and framed versions of the image displayed in a catalog format. The image catalog is nothing more than a scrolling display of all the images in the database.
FIG. 9.13 The Image object can be activated to pop up an image database.
FIG. 9.14 The image database may be displayed as a scrolling of plain images.
Fig. 9.15 The image database may be displayed as a scrolling catalog of framed images.
Most of the draw methods presented here accept a string terminator as a parameter, and also return a string. The terminator is usually the carriage return character, \n. This results in a string with linebreaks suitable for an alert. You can also use <BR> as the string terminator; this yields a string that can be nicely presented in a window. The utility function showInWindow in listing 9.25 pops up a window and tries to put anything you hand it into that window. We will use this function quite often in chapter 20.
Listing 9.25 A Function That Shows an Object in a Pop-up Window function showInWindow(tstr,title) { var w = 300 var h = 300 scrl = 'yes' if (w>640){w=640;scrl='yes'} if (h>480){h=480;scrl='yes'} var whstr ='SCROLLBARS=' + scrl + ',RESIZABLE=yes,WIDTH=' + w + ',HEIGHT=' + h aNewWin = self.open('',title,whstr) var astr = '<HTML><HEAD>' astr += '<BASE HREF="' + location.href + '">' astr += '<TITLE>' + title + '</TITLE>' astr += '</HEAD><BODY>' aNewWin.document.write(astr) aNewWin.document.write(tstr) aNewWin.document.write('</BODY></HTML>') aNewWin.document.close() }
The approach we employed for the Image object can be used for text manipulation as well. We will create a Text object that holds various important properties related to how the text is displayed. This Text object will be very similar to the Image object in terms of the type of draw methods which it supports. The constructor function for the Text object is shown in listing 9.26.
Listing 9.26 The Constructor Function for the Text Object function createText(type,title,text,size,color,bold,italic, supersub,frame,href,notes) { this.type =type this.title= title this.text = text this.size = size this.color = color this.bold = bold this.italic = italic this.supersub = supersub this.frame = frame this.href = href this.notes = notes this.draw = drawText this.frame = frameText this.reference = referenceText this.popup = popText this.lines = stringToList this.word = word return this }
Notice that this function is very similar to the constructor for the Image object. It encapsulates some of Netscape's own string properties. This object, too, knows how to present itself as plain, framed, or referenced text. It can also pop itself up in a window. Note that it has a type property; that is, the text can be a list, a paragraph, and so on. Its draw method accepts a parameter that allows you to draw the text according to a predefined type method or to draw the text using the properties you have set.
The Text object has some methods that can access the text as lines or words. Again, this is a crude emulation of HyperCard. These methods allow you to define the separator which separates one phrase from another. The stringToList method accepts a prefix and a suffix as well as the list separator. It separates the string into phrases and then adds the prefix and suffix given. Use the \n character as the separator to get logical lines. Our old friend, the word() function, is used to return the word at the position you specify. The implementation of the stringToList method is shown in listing 9.27.
Listing 9.27 A Function to Separate Delimited Strings into Phrases function stringToList(sep, pref,suf) // sep = separator (delimiter) // pref = prefix to prepend // suf = suffix to append { var n = 0 // location of current separator var f = 0 // location of next separator var astr = '' // string being built var nstr = '' // work string var finished = false while (f >= 0 ) { // get position of next separator f = this.indexOf(sep,n) // if this the last one? if (f == this.lastIndexOf(sep)) { // if so then get the end of the string f = this.length // and set the finished flag finished = true } // get the string, prepend the prefix and append the suffix nstr = pref + this.substring(n,f) + suf // add the current working string onto the final string astr += nstr // if all done then break out of the loop if (finished ) break // advance to the next separator n = f + 1 } return astr // return the completed string }
Note that this function uses an interesting trick to decide when it has come to the end of the string. It uses lastIndexOf() to get the character position of the last occurrence of the separator. As it walks the string, it checks to see if the character found equals the last instance of that character. If it does, it sets the phrase end to the end of the string, performs its usual string separation, and then quits.
Another useful routine, which can easily be modified for your own purpose, is the makeTitle method. MakeTitle goes through a string and eliminates spaces, but capitalizes the letter after the space. This is useful for catching two word window names, such as "Hi there," which a user has entered. If you hand the open() command more than one word, it simply does not open the window, nor does it tell you why. The makeTitle method converts such strings into more acceptable titles, such as "HiThere" in this case. The makeTitle function is shown in listing 9.28. Figure 9.16 shows the result of exercising the pop-up capabilities of the Text object.
.Listing 9.28 A Function to Create Window Title Strings function makeTitle(what) { var n = what.length // length of string "what" var i = 0 // iteration variable var cc = '' // current character var accstr='' // output string being built for (i = 0 ; i < n ; i++) // iterate over the whole string { // get character at position "i" cc = what.substring(i,i+ 1) // if that character is not a space, then add it to the output string if ( cc != ' ' ) { accstr += cc continue } else { // if that character is a space then skip it I++ // grab the first character after the space cc=what.substring(i,i + 1) // convert it to uppercase cc = cc.toUpperCase() // add that uppercase letter onto the output string accstr += cc } } return accstr // return the output string }
FIG. 9.16 The database of text objects can display itself in pop-up windows.
|