<!--- For demonstration, set ID cookie for Admin
as if it came from an external application
and give this window an Admin role in the url --->
<cfparam name="url.id" default="">
<cfif not len(trim(url.id))>
This page acts like a page in your Admin application.
For demonstration, you must follow the URL with
?ID={external Admin userid}
<cfabort>
</cfif>
<cfcookie name="curruser" value="#url.id#">
I'm ready to provide ...
<a href="tu.cfm?&TalkRole=1" target="_blank">Human Help</a>
Here's a page that does the same thing for users, except that TalkRole is not used; call it userapp.cfm. In both cases, all that's happening here is that a user or administrator can click on a link that opens a new window to tu.cfm. The application sets a user or admin environment that will be sensed by human help.
<!--- For demonstration, set ID cookie for user
as if it came from an external application --->
<cfparam name="url.id" default="">
<cfif not len(trim(url.id))>
This page acts like a page in your user application.
For demonstration, you must follow the URL with
?ID={external user's userid}
<cfabort>
</cfif>
<cfcookie name="currentvendor" value="#url.id#">
I'm ready to receive ...
<a href="tu.cfm" target="_blank">Human Help</a>
If a cookie for an administrator is present and TalkRole is defined and not left empty in the URL, then set an ID for use in the human help application to the value of the cookie, set the variable AdminURL to "&url.TalkRole=1", and set a value in pixels that will be used to control the height of the control frame in a human help frameset. The unusual value for AdminURL helps it be concatenated to a query string later.
If this combination of conditions is not true but a vendor cookie is present, do the almost the same thing. Differences: make AdminRole empty in the URL, and set a smaller control height than you did for an administrator.
<!--- Set "my" ID and parameters
from calling URL and external application's cookie --->
<cfset MyID=0>
<cfif isDefined("url.TalkRole")
and len(trim(url.TalkRole))
and isDefined("cookie.curruser")>
<cfset MyID=val(cookie.curruser)>
<cfset AdminURL="&TalkRole=1">
<cfset ControlHeight=170>
<cfelseif isDefined("cookie.currentvendor")>
<cfset MyID=val(cookie.currentvendor)>
<cfset AdminURL="">
<cfset ControlHeight=120>
</cfif>
<cfif not MyID>
Can't identify you; can't continue.
<cfabort>
</cfif>
This approach to working with what amounts to two external applications assumes that the ID for an administrator won't be the same as the ID for a vendor. If you can't guarantee that IDs won't overlap in the applications, you can keep them from overlapping in human help by modifying the code to add a arbitrary value to the vendor ID (or the admin ID) and by modifying the code that uses IDs to check external tables.
Include tuid.cfm to get the ID, role, and (as previously mentioned), the desired height of the control frame. However, if the URL mode says "bye", include tutalk.cfm to respond "bye", include tudrop.cfm to drop the help session - more about this later - and stop without opening the frameset. Therefore, using this URL to call tuid.cfm shuts down the help session.
<!--- Get my ID, Role, and parameters ---> <cfinclude template="tuid.cfm"> <!--- If URL mode says "bye", stop ---> <cfparam name="url.Mode" default=""> <cfif url.Mode is "bye"> <cfset Statement="bye"> <cfinclude template="tutalk.cfm"> <cfset DropID=MyID> <cfinclude template="tudrop.cfm"> This session has ended; please close this window. <cfabort> </cfif>
To make it easy for administrators to specify which user they're directing their replies to, each help session is identified by a letter of the alphabet. Therefore, if the URL doesn't say "bye", the next step is to determine that letter. If the letter isn't known, include tustart.cfm to assign a new one. If the letter is known but isn't in tblTalker, include tustart.cfm to assign a new one. At the end of this process, url.MyLtr will contain that letter.
<!--- Get or start help session;
confirm or provide my letter --->
<cfparam name="url.MyLtr" default="">
<cfif not len(trim(url.MyLtr))>
<cfinclude template="tustart.cfm">
<cfelse>
<cfquery name="talkerGet" datasource="context">
select * from tblTalker
where Userid = #val(MyID)#
</cfquery>
<cfif not talkerGet.recordcount>
<cfinclude template="tustart.cfm">
</cfif>
</cfif>
Now it's time to display the frames. Set their query strings so that MyLtr is the appropriate letter and so that AdminURL is "&TalkRole=1" for admin or empty for other users. Set the height of the control frame as specified by tuid.cfm earlier. To keep the driver frame from being seen, set its height to 1 pixel, and prevent it from scrolling and from being resized. (The driver frame refreshes periodically. If it can't be seen, the refresh won't bother the user.)
<!--- Set internal tracking ---> <cfset Extra="MyLtr=#url.MyLtr##AdminURL#"> <html> <head> <title>Human Help</title> <!--- Place frameset BETWEEN head and body ---> </head> <cfoutput> <frameset rows="#ControlHeight#,*,1" frameborder="0" border="0"> <frame name="control" src="tucontrol.cfm?#Extra#"> <frame name="conversation" src="tushow.cfm?#Extra#"> <frame name="driver" src="tudrive.cfm?#Extra#" noresize scrolling="no"> </frameset> </cfoutput> <body> <noframes> Please use a browser that understands frames. </noframes> </body> </html>
The SQL syntax of the delete statement for a production database such as Oracle or MS-SQL is different from the syntax for Microsoft Access. This code uses the Microsoft Access syntax if it's being run in a test environment (127.0.0.1 or "futureec") instead of in a production environment. Because the letter identifying this user for human help wasn't in the URL, retire any statements consisting of "bye" going from or going to this ID in tblTalker. (These statements are left over from the previous session for reasons that will be discussed later.)
<!--- Get external data based on ID and role ---> <cfinclude template="tuexdata.cfm"> <!--- Set my "letter" ---> <cfquery name="talkerDrop" datasource="context"> <cfif cgi.server_name contains "127" or cgi.server_name contains "futureec.com"> delete * from tblTalker <cfelse> delete from tblTalker </cfif> where Userid = #val(MyID)# </cfquery> <cfquery name="talkDrop" datasource="context"> update tblTalk set Retired = 1 where Statement = 'bye' and (FromID = #val(MyID)# or ToID = #val(MyID)#) </cfquery>
If this is not an administrator, look for a letter from B through Z (chr 66 through 90) that is NOT used in a query of tblTalker. (The letter A is reserved for administrators; so, it's not considered here.) When an unassigned letter is found, break from the loop and assign it. If all letters from B to Z are used, then complain.
If this is an administrator, simply use the letter A (chr 65) without a search.
When done, set url.MyLtr to the letter you have selected.
<cfif not len(trim(AdminURL))>
<cfquery name="talkerList" datasource="context">
select TalkLtr from tblTalker
</cfquery>
<cfset LtrList="">
<cfloop query="talkerList">
<cfset LtrList=listAppend(LtrList,TalkLtr)>
</cfloop>
<cfset OK=0>
<!--- (Loop from B to Z) --->
<cfloop from="66" to="90" index="LtrCode">
<cfif not listContains(LtrList, chr(LtrCode))>
<cfset OK=1>
<cfbreak>
</cfif>
</cfloop>
<cfif not OK>
Try again later; too many need help now.
<cfabort>
</cfif>
<cfelse>
<cfset LtrCode=65>
</cfif>
<cfset url.MyLtr=chr(LtrCode)>
Now add this user to tblTalker. The Name and Company data come from the user side of the external application or the admin side of that application as appropriate. Finally, include tutalk.cfm to add the special statement "...paging" to tuTalk where an administrator can see it.
<!--- Add me to list of talkers --->
<cfquery name="talkerAdd" datasource="context">
insert into tblTalker
(TalkLtr, Userid,
SentDt, Company,
Name)
values
('#url.MyLtr#', #val(MyID)#,
#createODBCDateTime(now())#, '#Company#',
'#Name#')
</cfquery>
<!--- Add initial statement --->
<cfset form.Statement="paging...">
<cfinclude template="tutalk.cfm">
<!--- Get external data based on ID and role --->
<cfif len(trim(AdminURL))>
<cfquery name="adminGet" datasource="context">
select * from tblAdmin
where AdminID = #val(MyID)#
</cfquery>
<cfif not AdminGet.recordcount>
Admin not in application.
<cfabort>
</cfif>
<cfset Company="Admin">
<cfset Name=adminGet.Name>
<cfelse>
<cfquery name="userGet" datasource="context">
select VendorID, CompanyName,
FirstName from tblVendor
where VendorID = #val(MyID)#
</cfquery>
<cfif not userGet.recordcount>
User not in application.
<cfabort>
</cfif>
<cfset sp5=" ">
<cfset Company=trim(left(
userGet.CompanyName&sp5&sp5&sp5&sp5,40))>
<cfset Name=trim(left(
userGet.FirstName&sp5&sp5&sp5,15))>
</cfif>
<!--- Set when this talker last sent something --->
<cfquery name="talkerUpd" datasource="context">
update tblTalker
set SentDt = #createODBCDate(now())#
where Userid = #val(MyID)#
</cfquery>
<!--- Add the statement --->
<cfparam name="ToLtr" default="A">
<cfparam name="ToID" default=0>
<cfparam name="Company" default="">
<cfparam name="Name" default="">
<cfquery name="sendStatement" datasource="context">
insert into tblTalk
(FromLtr, FromID,
FromCompany, FromName,
ToLtr, ToID,
SentDt, Statement,
Retired)
values
('#url.MyLtr#', #val(MyID)#,
'#Company#', '#Name#',
'#ToLtr#', '#ToID#',
#createODBCDateTime(now())#, '#Statement#',
0)
</cfquery>
If an adminstrator says "bye" to a user, and the user's ID is known, then include tudrop.cfm to drop the user. Once this is done, use javascript to reload the control frame so that the user won't be displayed in its list of active sessions. Because this is an administrator, its letter is "A" and its TalkRole is 1.
<!--- If Admin, drop a user to whom I say "bye",
and refresh my control frame --->
<cfif len(trim(AdminURL))
and (Statement is "bye")
and val(ToID)>
<cfset Extra="MyLtr=A&TalkRole=1">
<cfset DropID=val(ToID)>
<cfinclude template="tudrop.cfm">
<cfoutput>
<body onload=
"javascript:parent.control.location.href='tucontrol.cfm?#Extra#'">
</cfoutput>
</cfif>
If the letter isn't known, complain and stop. Include tuid.cfm to get the user and role. Set the query string. Include tuexdata.cfm to get external data. Set defaults.
The control frame submits form variables to itself. For administrators, it contains a select box named Pick. For the selected item, the value of Pick is two elements separated by an underscore: a list with the underscore as its delimiter. Element 1 is the letter of the session, and element 2 is the ID of the user to whom the administrator's statement is directed. (For other users, form.Pick contains only spaces separated by an underscore.) If Statement is not empty, and the trimmed value of Pick has data (not just the bare underscore), then address the statement in tblTalk to the letter and ID you've selected with Pick. (Only administrators have a pick list; all others take a default with spaces in the place of useful data.) If Statement is not empty, and the pick list does not have data, then address the statement in tblTalk to letter "A" (all administrators) and an ID of zero. Either way, if Statement is not empty, include tutalk.cfm to send it to the destination you've just set.
<!--- If url.MyLtr isn't available, stop --->
<cfif not (isDefined("url.MyLtr")
and len(trim(url.MyLtr)))>
Talk letter missing; can't continue.
<cfabort>
</cfif>
<!--- Get my ID, AdminURL, and parameters --->
<cfinclude template="tuid.cfm">
<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">
<!--- Get external data based on ID and role --->
<cfinclude template="tuexdata.cfm">
<!--- Set defaults --->
<cfparam name="form.Statement" default="">
<cfparam name="form.Pick" default=" _ ">
<!--- If statement isn't empty, send it --->
<cfif len(trim(form.Statement))>
<cfif trim(form.Pick) is not "_">
<cfset ToLtr=listGetAt(form.Pick,1,"_")>
<cfset ToID=listGetAt(form.Pick,2,"_")>
<cfelse>
<cfset ToLtr="A">
<cfset ToID=0>
</cfif>
<cfinclude template="tutalk.cfm">
</cfif>
Both administrators and other users need a form in the control frame. Have the form post back to this page, but add a query string (Extra) to the form action so the control frame will have the proper environment. Display the identity. If this is an administrator, show a pick list and force the page to focus on the pick list. Either way, the frame will have a statement field, a send button, and a close link.
Displayed identity consists of Company (or "Admin") and the individual's first name. For clarity during demonstrations, the ID is also displayed (in square brackets). For a finished application, you will want to change this so the ID is not displayed.
<!--- Accept input ---> <form name="Control" method="post" action=<cfoutput>"tucontrol.cfm?#Extra#"</cfoutput>> <!--- Display my identity ---> <cfoutput>#Company#: #Name# [#MyID#]</cfoutput><br>
If this is an administrator - AdminURL isn't empty - query tblTalker for all help sessions (sequenced by help letter). The pick list will show three entries at a time but can hold a many as necessary. The value for each entry is expressed as {letter}_{ID}; the display for each entry is expressed as {letter} - {company}: {name}. If the ID for the entry is the current userid, then mark the entry "selected".
The statement text field is given a short size: 30 characters. This lets three help windows easily fit on the screen at once during demonstrations and lets administrators use just a small portion of their screens for human help. The field will hold 200 characters (by scrolling). So, the short field size doesn't limit conversation.
<!--- (Show pick list if admin) --->
<cfif len(trim(AdminURL))>
<cfquery name="talkerList" datasource="context">
select * from tblTalker
order by TalkLtr
</cfquery>
<select name="Pick" size="3">
<cfoutput query="talkerList">
<cfif listGetAt(form.Pick,2,"_") is #Userid#>
<cfset chosen="selected">
<cfelse>
<cfset chosen="">
</cfif>
<option value="#TalkLtr#_#Userid#" #chosen#>
#TalkLtr# - #Company#: #Name#
</cfoutput>
</select>
<br>
</cfif>
<input type="input" name="Statement" value=""
size="30" maxlength="200"><br>
<input type="submit" name="Send" value="Send">
(up to 200 characters)
The "Close" link uses a target of _top to break out of the frame with tu.cfm. Its query string, Extra, identifies the help session letter MyLtr (and for administrators, TalkRole=1). If this is an administrator, javascript shifts the browser's focus to the pick field. This allows administrators to type to users quickly. If the IE browser is used, the administrator can enter the letter of the user to receive the statement, tab once to the statement field, enter a statement, press {Enter}, and immediately repeat the cycle. If the Netscape browser is used, the cycle is almost as quick except that the administrator must use the cursor key instead of a session letter to pick the user to send a statement to. The javascript follows the classic pattern of hiding the actual script inside comments; although, without javascript, other aspects of this application won't work. You may wish to remove the comments, and you may wish to come up with javascript that makes things easier for administrators who use Netscape. It's useful for Netscape now; however, additional javascript could make it even better.
<cfoutput> <a href="tu.cfm?Mode=byeExtra#" target="_top">Close</a> </cfoutput> </form> <!--- If Admin, focus on pick ---> <cfif len(trim(AdminURL))> <script language="javascript"> <!-- document.Control.Pick.focus(); // --> </script> </cfif>
If MyLtr isn't already available - it should always be present - complain and stop. Otherwise, include tuid.cfm for identification, build a query string in the variable Extra, and list current conversations. For regular users, this list is filtered to include just text from and to the user and only includes statements that have not been retired. (Statements are retired when the conversation is over.)
<!--- If url.MyLtr isn't available, stop --->
<cfif not (isDefined("url.MyLtr")
and len(trim(url.MyLtr)))>
Talk letter missing; can't continue.
<cfabort>
</cfif>
<!--- Get my ID, Role, and parameters --->
<cfinclude template="tuid.cfm">
<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">
<!--- Get recent conversation, most recent first --->
<cfquery name="getRecent" datasource="context">
select * from tblTalk
<cfif not len(trim(AdminURL))>
where Retired = 0
and (ToID = #MyID# or FromID = #MyID#)
</cfif>
order by StatementKey desc
</cfquery>
If a user says "bye", include tudrop.cfm to drop the user from tblTalker and associated conversation from tuTalk. Then, use javascript to break out of the frame with tu.cfm, adding "bye" to the query string as if the user had clicked on the "Close" link. Tu.cfm will then finish the process of ending the help session.
<!--- If not Admin, drop myself when I say "bye", and remove my frames ---> <cfif not len(trim(AdminURL)) and (trim(getRecent.Statement) is "bye")> <cfset DropID=val(MyID)> <cfinclude template="tudrop.cfm"> <cfoutput> <body onload= "javascript:parent.location.href='tu.cfm?mode=byeExtra#'"> </cfoutput> </cfif>
If an administrator sees "bye" from someone else, include tudrop.cfm to drop that user. Then, reload the control frame so it won't display this user in the list of available help sessions.
<!--- If Admin, and most recent statement is "bye" from someone else, drop that individual and refresh the control frame ---> <cfif len(trim(AdminURL)) and (trim(getRecent.Statement) is "bye") and (val(getRecent.FromID) is not MyID)> <cfset DropID=val(getRecent.FromID)> <cfinclude template="tudrop.cfm"> <cfoutput> <body onload= "javascript:parent.control.location.href='tucontrol.cfm?#Extra#'"> </cfoutput> </cfif>
If the most recent statement in an administrator's conversation frame is "paging...", change the background color to something attention-getting, and reload the control frame so its pick list will display the new request for help. The next statement that isn't "paging..." will shift the color back to normal. Non-admin users always have a normal color for the background; they won't see this effect.
You may wish to change the code to use a background sound instead of shifting the background color to get adminstrators' attention, but note first that most browsers are not configured properly to handle sound. Therefore, sound is not recommended for demonstrations of the human help technique. It takes time to get browsers to handle sound properly.
<!--- If Admin, and most recent statement is "paging...", change the background color and refresh the control frame ---> <cfif len(trim(AdminURL)) and (trim(getRecent.Statement) is "paging...")> <cfoutput> <body bgcolor="BCBCBC" onload= "javascript:parent.control.location.href='tucontrol.cfm?#Extra#'"> </cfoutput> <cfelse> <body bgcolor="FFFFFF"> </cfif>
Show statements starting with the most recent. The list is filtered as mentioned above. For regular users, each statement is preceded with "Admin" or with the user's name, either followed by a colon. For administrators, each statement is preceded with "{letter}-{name} to {letter}:" so it's clear who said what to whom. Statements from users usually include their names, and the names are also in the pick list. So, the fact that statements to users only show the letter to whom the statement is addressed isn't a handicap, just useful shorthand.
<!--- Show recent conversation --->
<cfoutput query="getRecent">
<cfif not len(trim(AdminURL))>
<cfif trim(getRecent.FromLtr) is "A">
Admin:
<cfelse>
#FromName#:
</cfif>
<cfelse>
#trim(getRecent.FromLtr)#-#trim(getRecent.FromName)#
to #trim(getRecent.ToLtr)#:
</cfif>
#Statement#<br>
</cfoutput>
If MyLtr isn't available - it should be - complain and stop. Include tuid.cfm to identify the user and role. Set Extra with MyLtr and AdminURL as usual. Read tblTalk to get the last statement key that's relevant to this user. If tblTalk is empty - it will be, initially - then add an empty row to the query and set the key value in that row to zero, just to have a starting point. "Last" in the URL usually contains the last relevant key seen by this frame. If the key from the table is greater than the key in the URL, then there are more statements to display; set More to 1. (If not, set More to zero.)
<!--- If url.MyLtr isn't available, stop --->
<cfif not ( isDefined("url.MyLtr")
and len(trim(url.MyLtr)) )>
Talk letter missing; can't continue.
</cfif>
<!--- Get my ID, Role, and parameters --->
<cfinclude template="tuid.cfm">
<!--- Set internal tracking --->
<cfset Extra="MyLtr=#url.MyLtr##AdminURL#">
<!--- Note last relevant statement key
and whether there are more than we've seen --->
<cfquery name="getLast" datasource="context">
select max(StatementKey) as Last from tblTalk
<cfif not len(trim(AdminURL))>
where ToID = #val(MyID)#
or FromID = #val(MyID)#
</cfif>
</cfquery>
<cfif not getLast.recordcount>
<cfset dummy=queryAppendRow(getLast)>
</cfif>
<cfparam name="url.Last" default="0">
<cfif val(getLast.Last) gt url.Last>
<cfset More=1>
<cfelse>
<cfset More=0>
</cfif>
If your environment is like mine, you may develop on a non-secure server and publish on a secure server. A cfif statement can pick the right protocol automatically. If cgi.server_port isn't 443, then use http (unsecure) instead of https (secure). (cgi.server_protocol is a false lead; it's not the variable you want to track.)
Once you know the protocol, you can let the script location determine the rest of the information you need to write the URL for the driver frame. (You could hard-code the information, but then you'd have to maintain it manually.)
Add the query string. It consists of MyLtr, then the AdminURL, and then a Pulse that flip-flops between 1 and -1. Pulse is handy for demonstrations; because, you can change the code to enlarge the driver frame a little and then set its background color based on Pulse to dramatize its operation. For production, you probably don't need it.
Use meta refresh to load the resulting URL (with its query string) every six seconds. (You can play with this figure if you're happy with less-frequent updates.)
Once you've set up the refresh - something you have to do before entering the body of the frame - then if there's More data to display, use javascript to refresh the conversation frame. Open the body. Send trivial text (a break, for example). Before this break is a good place to put debug information while you're on the margin between test and production.
<!--- Before the body tag, set this URL to refresh --->
<cfif cgi.server_port does not contain "443">
<cfset Protocol="http">
<cfelse>
<cfset Protocol="https">
</cfif>
<cfset Myself="#Protocol#://">
<cfset Myself="#Myself##cgi.server_name##cgi.script_name#?">
<cfset Myself="#Myself#&Last=#val(getLast.Last)#">
<cfset Myself="#Myself#&MyLtr=#url.MyLtr##AdminURL#">
<cfif isDefined("url.Pulse") and (url.Pulse gt 0)>
<cfset Pulse=-1>
<cfelse>
<cfset Pulse=1>
</cfif>
<cfset Myself="#Myself#&Pulse=#Pulse#">
<cfoutput>
<meta http-equiv="refresh" content="6; URL=#Myself#">
<!--- Reload conversation if more --->
<cfif More>
<cfset ConversationSrc="tushow.cfm?MyLtr=#url.MyLtr##AdminURL#">
<body
onload="javascript:parent.conversation.location.href='#ConversationSrc#'">
<cfelse>
<body>
</cfif>
</cfoutput>
<br>
</body>
Look for the letter for this ID's help session in tblTalker. If found, then (with one exception) retire the statements to or from this ID. Statements consisting of the word "bye" are left active so that multiple windows will see them and get a chance to drop the talker from the active list.
<!--- Drop user and conversation --->
<cftransaction>
<cfquery name="talkerGet" datasource="context">
select TalkLtr from tblTalker
where Userid = #DropID#
</cfquery>
<cfif talkerGet.recordcount>
<cfquery name="talkerDrop" datasource="context">
<cfif cgi.server_name contains "127">
delete * from tblTalker
<cfelse>
delete from tblTalker
</cfif>
where Userid = #val(DropID)#
</cfquery>
<cfquery name="talkDrop" datasource="context">
update tblTalk
set Retired = 1
where (FromID = #val(DropID)# or ToID = #val(DropID)#)
and Statement <> 'bye'
</cfquery>
</cfif>
</cftransaction>
Notice the background change in the admin window when any user window opens - even the admin window itself. Have a conversation with yourself. Click the "Close" link for a regular user; watch the admin window change and the regular user's frameset become a simple page. As admin, tell the other user "bye" and watch its session shut down.
Right-click and copy this link - adminapp.cfm?ID=5 - into a browser from the "other" family - the one you aren't already using as an administrator - and click the link it displays to get a second admin window open. Notice the how the existing admin window shows the new admin in the list. (Both are labelled "A" but display separate names). Browse userapp.cfm?ID=120 in a second window in either browser. Watch both admin windows change. Notice that both admin windows show all text. Notice that adminstrators can tell which admin is making which statements to the user but that the user only knows the statements come from "admin".
To make sure you've set things up right, highlight some of the conversation and watch it for a while. The highlight should not vanish after 6 seconds but should remain until the list of statements actually changes. Do the same for the control frame. If the highlight remains intact when nothing useful is happening, you've proven that the frame is only refreshed as necessary. (The driver frame refreshes periodically, but only the blur in the status line is visible.)
If you don't see a blur at all, there is probably an error in your driver frame. To see it, change tu.cfm to make the driver frame larger (or try to right-click on the last pixel of the frameset and view source).
This tip explores several advanced techniques to provide immediate interaction without a single line of Active X or Java. In the HTML realm, it uses targets in URLs, and it uses a pair of values (delimited by an underscore) instead of a single value for the data returned by a select box. It also manages a frameset, changes background color, and refreshes a frame. In the javascript realm, it sets field focus and lets one frame reload other frames. In the ColdFusion realm, it reminds us that there's a difference between putting a variable in ColdFusion's URL scope (in a cfset statement) and writing an actual URL to be seen by the Web server. It provides practice in acting on differences in the user environment. It sets cookies, works with databases, and modifies URLs. In short, this tip not only provides a valuable capability. Understanding it is a good springboard for other projects. =Marty=
[Instructions in the "Try It" section have been modified to make sure the user gets multiple windows open and to correct the ID=5 link.]