Sean asks:
This is a pretty interesting idea. I was pretty mad at Sean for mailing this to me on Wednesday right when I had to fly! (In case folks are wondering why I was so silent recently, I flew up to CA on Wednesday and back home on Friday so I've been a bit remote.) As always, there are many ways we could attack this, but this is the route I chose.I have an existing application. Its for doctors and technologists who are applying for accreditation. There are a lot of terms that are used and the client wants the terms linked to a pop up that has a definition. Now, I can set up a CFC to grab the term's definition and return it to an ajax call from CF and link the term to it, that's no problem. The problem is, I don't want to go through each page, and then again when the content is changed and manually link any recognized words to the JS function that executes the remote call to the CFC. Do you know of a way, perhaps with jQuery or the built-in CF stuff, to, after each page loads, scan the text for any recognized words (this list can be retreived from another CFC call for all the words we "know of") and then link to it using the pop-up bubble?
- On load, use JavaScript to get all the text from one div. While Sean said he wanted to scan the 'page', I assume he really means the main page content, and not stuff like the header of footer.
- Convert that text into a unique list of words.
- Send that list to the server and figure out which words are 'hot', ie, terms that we have definitions for.
- When we get the list of terms back, update the content to create links to something that will display the definition.
Make sense? Ok, let's start. I began with a super simple HTML page:
<html>
<head>
</head>
<body>
<div id="content">
<p>
This is some <b>fun</b> text.
</p>
<p>
This is some <b>boring</b> text.
</p>
<blockquote>
<p>
For score and seven years ago...
</p>
</blockquote>
</div>
</body>
</html>
Notice I have one main div with the ID of content. You could imagine a system by where you use ColdFusion custom tags to wrap your content (see this blog entry for an example) and always count on the important page content being within one specific ID.
Our first task is to get the text from the div. Notice I said text and not html. Let's use jQuery:
var content = $("#content").text()
Now we need to split the content into words. I'll use a bit of regex for that:
var words = content.split(/\b/)
And now I'll store each word into a simple object. I'm using this as a trick to convert a list of words into a set of unique words. There is probably a nicer way to do this.
var uWords = new Object()
for(var i=0; i<words.length;i++) {
var word = words[i]
word = word.replace(/[\s\n]/g,'')
if(word != '' && uWords[word] == null) uWords[word] = ''
}
I then convert this generic object into an array:
var aWords = new Array()
for(var w in uWords) aWords.push(w)
And lastly, let's send this to ColdFusion:
if(aWords.length) $.post('term.cfc?method=filteredTermList&returnFormat=json',{termList:aWords.toString()},highlightWords,"json")
This code says: If I had any words, do a POST operation to the CFC named term.cfc, run the method filteredTermList, and pass an argument named termList. The aWords.toString() is a shortcut to convert the array into a list. Next I tell jQuery to run a method, highlightWords, when done. The last argument lets jQuery know the result will be in json. This let's jQuery automatically convert it to native JavaScript data.
All of of the above code was wrapped in a $(document).ready(function() package so as to run as soon as the page loads. (Don't worry, I'll post the complete code at the end.)
Ok, now let's go to the CFC side. It's a rather simple CFC so I'll post the entire file first:
<cfcomponent>
<!--- Hard coded list of terms we know. --->
<cfset variables.termList = ["boring","cool","hot","cold","nice","watch","score"]>
<cffunction name="filteredTermList" access="remote" returnType="array" output="false">
<cfargument name="termList" type="string" required="true" hint="List of words to search. Assumed to be unique.">
<cfset var term = "">
<cfset var result = []>
<cfloop index="term" list="#arguments.termList#">
<cfif arrayFind(variables.termList,term)>
<cfset arrayAppend(result, term)>
</cfif>
</cfloop>
<cfreturn result>
</cffunction>
<cffunction name="arrayFind" access="private" returnType="numeric" output="false" hint="Returns the position of an item in an array, or 0">
<cfargument name="arr" type="array" required="true">
<cfargument name="s" type="string" required="true">
<cfset var x = "">
<cfloop index="x" from="1" to="#arrayLen(arguments.arr)#">
<cfif arguments.arr[x] is arguments.s>
<cfreturn x>
</cfif>
</cfloop>
<cfreturn 0>
</cffunction>
</cfcomponent>
The CFC begins with a hard coded list of known terms. This would normally be dynamic. The important method, filteredTermList, is what I'll focus on. It accepts a list of words. For each word, it will check and see if it exists in our known list (notice the use of arrayFind), and if there is a match, the word gets appended to a result array. This result is returned at the end of the method.
Ok, so back to the front end. Remember we had told jQuery to run a function called highlightWords when done. Here is how I defined that method:
function highlightWords(data,textStatus) {
var currentContent = $("#content").html()
for(var x=0; x<data.length;x++) {
word = data[x]
//replace word with <a href="" click="return showTerm('term')">term</a>
var newstr = '<a href="" onclick="return showTerm(''+word+'')">'+word+'</a>'
//get current html
//replace word with the new html
var reg = new RegExp(word, "gi")
currentContent = currentContent.replace(reg,newstr)
}
//update it
$("#content").html(currentContent);
}
We begin by grabbing the HTML of our content div. We need the HTML this time as we are actually going to be modifying the markup a bit. I then loop over the data result. This is what was returned from the CFC so it will be an array of words. For each word, I do a replace where each word is exchanged with HTML that will run a function, showTerm.
Now lets look at showTerm. This is what will show the definition for our word.
function showTerm(term){
ColdFusion.Window.create('term','Definition of '+term,'term.cfc?method=definition&term='+escape(term)+'&returnformat=plain', {center:true,modal:true})
ColdFusion.Window.onHide('term',winClosed);
return false
}
function winClosed() {
ColdFusion.Window.destroy('term',true)
}
For showTerm I made use of cfwindow. (See the PS for why I did and my rant about jQuery UI.) Notice that I make sure of the CFC again, but set the returnFormat to plain. This lets me run a CFC method that will return a string and not have to worry about JSON or WDDX encoding. The onHide/winClosed stuff just helps me handle creating new window objects. The method I added to the CFC is pretty simple:
<cffunction name="definition" access="remote" returnType="string" output="false">
<cfargument name="term" type="string" required="true" hint="Word to look up.">
<cfreturn "The definition of " & arguments.term & " is, um, something.">
</cffunction>
As you can guess, this would normally be a database lookup. But that's it! We now add terms to our CFC and know that the front end will automatically pick up on the terms and automatically hot link them. I've included the code as a zip to the entry so feel free to download and play. (Although you will have to download jQuery yourself.)
Before I get into my little rant about jQueryUI, here are some things to consider:
- Why not simply load the known words on startup? I could have done that, especially with my short list of known words. But I figured that in a real production system your list of known words could be huge. That isn't something you probably want to add to each page load. I figured scanning for unique words and sending that to the server to get the known list back would mean less traffic. I don't know that of course, and this ties back to something I said at the NYCFUG earlier this month. Don't consider AJAX a magic bullet. Sending data back and forth with JavaScript isn't going to magically be 'better' just because it's JSON or XML or whatever.
- It would be cool to add tracing to which terms get looked up. If you notice that some words are almost always looked up, it may represent an opportunity for something you can educate yours users about. So for example, if you see a lot of traffic for terms related to throat diseases, maybe you could write additional content on that area of study. On the flip side, if no one ever looks up the definition of Foo, then you can probably remove Foo from the list of content to hot link.
Any other things people would mention? Anyone using a system like this in production?
I've uploaded my demo here: http://www.coldfusionjedi.com/demos/termdemo/test.cfm
p.s. Ok, my rant. For my term popup I wanted to use jQueryUI. I was happy to find that there was a simple Dialog control, but I found skinning it to be very difficult. The docs talk about themes and mention the ThemeRoller application, all of which look really cool. But I wasn't able to find a simple example of using the theme for the dialog. The theme I downloaded had one HTML file, but it was cluttered with every possible example control you could imagine. Maybe I was being lazy, but I just found it difficult and overwhelming as a new user. To be fair, I thought jQuery's main code was a bit overwhelming at first, so I probably just need to give it another try, but I was pretty ticked at first. That's why I 'punted' and switched to cfwindow. It just plain worked.
Archived Comments
Awesome Ray, thanks!
If you use this in production and can share the URL, please do. Share. The URL. Thanks. ;)
Ray,
I have a customer who is trying to this exact thing - he calls it his Glossary. We were considering just doing all of this on the ColdFusion side but maybe this technique will be more fun and visually appealing. I will point them to this script and see what they think.
Cool, let me know. If he likes it, he can use the code for ONE MILLION DOLLARS! (Or 0. Up to him!)
I've written a couple of these live 'terminology' routines for different clients (all back-end, not javascript though) and they had lots of hot words that were actually phrases or at least two words long. What I'm saying is that if you're only scanning an array of single words then you'll be skipping lots of hot phrases.
Sean said it's for an accreditation website, so I took a look at one and found these multi-words that may benefit from a terminology tooltip:
accreditation standards, WTO TBT, company reputation, inspection bodies, independent evaluation.
Each word by themselves mean one thing, but when together with another word they take on a different meaning.
I think it will work better if you compare the 'database' of words and phrases against the text on the page rather than the other way around.
Ray ... Why doesn't he just store the tooltip definitions in an XML file ... like the <a href="http://livedocs.adobe.com/c...">CFSelect ~ bindCFNS.CFC</a> example in the CF8 docs? Would that work or am I off base here ... ?
@EB - I don't think you need to store a full url in such an xml file - just some unique identifier since your client could construct a url. But to your suggestion in general, I talk about that towards the end of the blog entry. My worry was that your list of terms would be too large to load via ajax.
@GF - Good point there. However, I still think it would be bad to send the entire db of words to the client. Another option would be to maybe send the entire text to the server? Instead of unique words. Then you would be able to do what you want.
There would have to be a way to make the terms two dimensional IE ... category ... listing ... like the state > city relationship ... I have the bindCfns.CFC working as in the docs example ... The cities usually load in a matter of two - to three seconds ... FWIW there's around 40,000 listings in the XML file ..
Ray, wouldn't it be easier to add the links into the copy before sending the page to the browser? I dont understand the benefit of having jQuery do the work of finding words and then using another http request (which could be a very large request) to get the links.
@EB - 2-3 seconds isn't long ... until it is added to every single page. ;)
@anthony - Sean specifically said this wasn't an option. Now, if the content you wanted to hot link was 100% db driven, then it probably would be simpler. But if it wasn't, then there wouldn't be an easy way to automatically add links when new terms are added. I don't agree that the first post will be huge since it is just unique words. The flip side to that though is that we all know what happens when you assume. If this were in production I'd add some logging to the cfc method. I could look later on to see, on average, how many unique words were being sent.
http://scopecache.riaforge....
@Ray: Entire text to server == bingo!
I think that's the best way to do it.
You could do something like: send the text to the server, have the server wrap "hot" content with a span or href, and then use jQuery or whatnot to scan the returned text (adding a CSS class to the "hot" content makes this easy) and "widgit-ize" what needs to be.
FWIW: dojo has things that make all this pretty easy.
FWIW2: Seems sorta like this stuff could go in an a11y /i18ln deal...
Interesting implementation. :-)
I've been looking into something similar. However, in my variation I wanted to check all CAP WORDS against a list of known acronyms and if there is a match, replace it with an acronym tag with the definition in the title.
It seems like you can do most of this work on the server side.
Ray,
For the pop-up, I recommend jqModal.
http://dev.iceburg.net/jque...
Example:
http://coldfusion-ria.com/t... (FF works best).
@EB - ScopeCache is for caching slow processes. It has nothing to do with network traffic. That is my concern.
@denny - That could work as well. I know jQuery makes it easy to say, 'take items with class x and do y to them', so if the server added the links and used a class, that could also be an option. As I said - many ways to solve this. :)
Is there a jQuery tooltip plugin, or was the requirement specific to a window/dialog?
Well I think Sean already had that part done, so the only 'requirement' here is what I wanted to work. ;)
Rey Bango pointed me to the jQuery UI groups so I'm going to give them a try sometime later today.
Maybe I can write something up and the jQuery folks could use it?
FWIW, I'd agree with Kumar -- jqModal worked well for me in my first adventure into AJAX+modal stuff.
Well, I'm digging into the UI Dialog stuff some more, this time outside of the stuff above. Maybe if I focus just on the dialog I'll be able to make it work. Going to blog my results later.
Another take on this, using a database in this case:
http://www.coldfusion-ria.c...
Cool, Kumar. I posted a comment to your entry.
Hi Ray. I've implemented this without JQuery. You do the tagging on the server when the page text is being added in the admin page for the site. The tagged text block is saved in the database record.
There is another table with just terms and their definitions. This is loaded into the server memory and the text block being submitted as part of the page text is compared word for word with the terms. When a word match is found, the word is rewritten with a style and some JS. Then the when all is done, the record is saved in the DB ready to be sent out to the browser. The JS function for the popup is in the header for the site. All I am doing is loading another CF Page passing the unique ID of the definition and querying for the definition. Not very hi-tech I know, but it works great. (see terms underlined in orange.)
Gary F. Has a good point and this is where the feature needs work. Context is important for a term and that is for version 2.0.