• Build a Wikipedia Search Widget in Under an Hour

    Kent Brewster
    Technology Evangelist,
    Yahoo! Developer Network

    http://developer.yahoo.com

    http://kentbrewster.com

  • System Requirements

    A computer, with working Internet connection.
    Check this now, please, by visiting the URL at the bottom of your screen. You ought to pop straight into this presentation, which will help enormously if you want to come back later and review.
    A grade-A Web browser.
    ... such as Firefox, Safari, Opera, or IE6+.
    If you're running Firefox, also get Firebug. (If you're not running Firefox .. um ... please run Firefox.) Firebug is the best JavaScript development environment I've found yet.
    If you don't already know about firebug, check out http://getfirebug.com.
    Any text editor. WordPad works fine; so do these:
    • For OSX: BBEdit, available from Bare Bones Software at http://www.barebones.com.
    • For Windows: Textpad, available at http://textpad.com.
    Please don't try this with Dreamweaver or any other WYSIWIG editor.
    It might work fine ... but past experience has taught me that you'll be very frustrated when your editor tries to Do What You Mean™.
  • Aims of the Class

    Gain Confidence and Have Fun
    While what we're about to do cannot possibly be considered easy or trivial to understand, it's actually pretty simple to implement. When you're done, you'll have a better understanding of many concerns facing the profession.
    Learn about HTML, CSS, and JavaScript
    We'll be cautiously skimming some heavy-duty issues, such as standards compliance, user experience design, object-oriented programming, and best practices ... if you're not careful, you might learn something. :)
    But Most of All: Build Something Useful!
    In this case, it's a portable, standards-compliant Web application that searches for and displays information found on every student's best friend, Wikipedia. You'll be able to include it in your blot or the page of your choice, or run it straight from your desktop whenever you want without the need for a Web server of your own.
  • Logistics and Demonstration

    Feeling Lost, Stuck, or Frustrated?
    The URL at the bottom of the screen contains a pointer to the current state of the code. If you get lost, or your carpal tunnel starts to act up, please feel free to go there, grab code, and paste.
    During the session we'll be doing a fair amount of coding and editing.
    Existing code will be show in red, like this: doStuffWith();
    Changes to be made will be shown in green, like this: doStuffWith(thatThing);
    To complete this change, you'd insert thatThing into the existing line of code. We will make very few deletions; when we do, I'll let you know what needs to come out.
    Please Do Me A Huge Favor
    If anyone else comes in during the class and sits next to you, let them know about this, by pointing them to the current state of the code. Thanks!
    And Now, the Demonstration
    If you're playing along at home, enter something in the blank. Mouse around in the results. Try bookmerking something via del.icio.us. Don't forget to poke at the W logo. Close it by clearing the blank.
  • On the Importance of Separate Layers

    Throughout this presentation you'll detect a theme: the importance of keeping one's layers separate and distinct. Please do your best to maintain separate structure, presentation, and behavior layers.
    Structure: your HTML.
    Presentation: your CSS.
    Behavior: your JavaScript
    To separate these three layers, we will call the CSS from a link in the document <head> and the JavaScript from a link just before the closing </body> tag. As has been repeatedly proven by Mr. Steve Souders--our performance guru at Yahoo!, who literally wrote the book on the subject--this is the method that works best for the greatest number of Web browsers.
    HUGELY IMPORTANT CAREER-SAVING NOTE: in the event that you apply for a job as a front-end engineer at any reputable shop, the very first thing your employer will do is open up the source of one of your sites and check that you are keeping your structure, presentation, and behavior layers separate.
  • Let's Get Started

    Create Three Empty Text Files
    Name them structure.html, presentation.css, and behavior.js.
    Add the following to structure.html:
    <html>
     <head>
      <title>Wikipedia Search</title>
      <link rel="stylesheet" type="text/css" href="presentation.css" />
     </head>
     <body>
      <h1>Hello, World!</h1>
      <script type="text/javascript" src="behavior.js"></script>
     </body>
    </html>
    Yes, you'll see some indentation; while it is not strictly necessary, it will help you to see blocks of structure and code.
    Remember to Cheat If Necessary
    The URL at the bottom of this page will drop you right into the current state of the code. Copy and paste if you need to!
  • Add Presentation and Behavior

    Here's presentation.css, short and sweet:
    body { background-color:yellow; }
    Here's behavior.js:
    var WIKISEARCH = function() {
       return {
          init : function() {
               WIKISEARCH.doStuff();
          },
          doStuff : function() {
               alert('Hello, world!');
          }
       };
    }();
    
    window.onload = function() {
       WIKISEARCH.init();
    };
    If the function we just wrote doesn't make sense, it's time for a quick peek at object-oriented programming.

    Diversion the First: Object Oriented Programming

    Object-oriented programming has yet to be documented in a way that makes sense to me, and what you're about to read is, sadly, no different.

    For this presentation I'm going to briefly show what you need to know to have the example make sense. Please keep in mind that this is the tip of a very large iceberg:

    • Apologies In Advance for the Sucky Documentation
    • So, What's an Object?
    • Objects Can Also Contain Functions
    • Securing Things With Singletons.
    • A Little Help From Private Variables.
    • And finally: Jump-Starting our Function.

    I am still refining this, and working on other diversions around topics like scope. Comments would be welcome!

    About the Sad, Sucky State of Documentation

    Consider the following sentences:

    "Having the one-off function call return the class constructor forms a closure that can hold private static members and assign privileged static methods as properties of the returned constructor."

    ... and then, there's this:

    "When the tweedle beetles battle with their paddles in a bottle full of water on a noodle-eating poodle, this is what we call a tweedle beetle noodle poodle water bottle paddle battle."

    The second sentence is from a children's book by Dr. Seuss. The first comes straight from the maintainer of comp.lang.javascript FAQ, and is representative of the general level of language you're going to find around this topic. To someone approaching object-oriented programming with an empty mind and a clean slate, the semantic value of these two sentences is equivalent: zero!

    There is one and only one good book on JavaScript, David Flanagan's JavaScript: The Definitive Guide, now out in its fifth edition from O'Reilly Books. You'll know it by the rhino on the cover. I also strongly recommend the ydn-javascript discussion group at groups.yahoo.com, and the YUI blog, at yuiblog.com.

    Up next: getting our hands dirty with objects.

    So, What's an Object?

    Objects are collections of values, which can be null, undefined, booleans, numbers, and strings, plus other objects, which include arrays of any of the above.

    Objects can contain named values:

       var a = { "name" : "Bob Dobbs" };
       alert(a.name);

    Objects can contain arrays:

       var a = { "id" : [6, 19, 465] };
       alert(a.id[1]);

    Objects can contain other objects:

       var a = { "people" : [
          { "name":"Bob Dobbs", "email":"bob@dobbs.com" },
          { "name":"Ted Knight", "email":"ted@knight.com" },
          { "name":"Carol Merrill", "email":"carol@merrill.com" },
          { "name":"Alice Springs", "email":"alice@springs.com" }
       ]};
    
       alert(a.people.length);
       alert(a.people[2].email);

    But that's not all. Objects can also contain functions.

    Objects can also contain functions:

       var a = {
          doStuff : function() {
             alert(a.people[3].name);
          },
          people : [
             { "name":"Bob Dobbs", "email":"bob@dobbs.com" },
             { "name":"Ted Knight", "email":"ted@knight.com" },
             { "name":"Carol Merrill", "email":"carol@merrill.com" },
             { "name":"Alice Springs", "email":"alice@springs.com" }
          ]
       };
       a.doStuff();

    Here we see a mixed object a, which contains function doStuff and array people, which in turn contains separate name and email objects for each person. When doStuff fires, we should see an alert for person 3's name.

    Very important: while all arrays are objects, not all objects are arrays! So the order in which the parts of a are listed is not important. Reading this suggests that doStuff should error out, since the people object hasn't yet been seen by the interpreter. Not true, since a.doStuff(); happens outside the object a.

    Next up: security concerns.

    Security with Singletons

    A lot can go wrong on a Web page, especially one that might be hosting third-party advertising. Here's how to shield our application from outside meddling, and vice versa:

       var a = function() {
          return {
             doStuff : function() {
                alert(a.people[3].name);
             },
             people : [
                { "name":"Bob Dobbs", "email":"bob@dobbs.com" },
                { "name":"Ted Knight", "email":"ted@knight.com" },
                { "name":"Carol Merrill", "email":"carol@merrill.com" },
                { "name":"Alice Springs", "email":"alice@springs.com" }
             ]
          };
       }();

    Additions are in green; we've wrapped our functions in an anonymous return, and added the empty parentheses at the end. This invokes closure, which runs the function immediately and keeps its results in memory for future access.

    There are, of course, more complications. Now that we've shielded our function from the outside world, we're going to have a hard time passing data back and forth between our subfunctions. For that we'll be using private variables.

    Private Variables

    Now that we have an impenetrable shield around our function, we need to be able to pass data back and forth between its parts. We'll do that with a private variable:

       var a = function() {
          var myName = 'Earl';
          return {
             doStuff : function() {
                alert(myName);
             },
             people : [
                { "name":"Bob Dobbs", "email":"bob@dobbs.com" },
                { "name":"Ted Knight", "email":"ted@knight.com" },
                { "name":"Carol Merrill", "email":"carol@merrill.com" },
                { "name":"Alice Springs", "email":"alice@springs.com" }
             ]
          };
       }();

    If we put myName outside this function, it would be available for any other script on the page to monkey around with. Because it's outside our anonymous return, it acts like a global variable available only to us.

    So. We've got this bullet-proof set of functions with stable data inside. How do we start it up? It's not going to fire off automatically any more.

    Jump-Starting the Anonymous Function

    Although there are several ways of doing this, grabbing the window.onload event and firing off the anonymous function seems to be the most solid method.

       window.onload = function() {
          a.doStuff();
       };

    If we need to pass a variable in later, here's how:

       window.onload = function() {
          a.doStuff('withMyStuff');
       };

    If you don't have access to window.onload, you can try dropping it in inline:

       a.doStuff();

    This is fraught with peril, especially if your function relies on the page already being fully rendered, and definitely not recommended for production environments.

    That's it for our brief side trip into object-oriented JavaScript. My best advice: keep reading, keep practicing, try to pick up patterns in structure, and let it sort of wash over you and soak in. It'll come, eventually.

    Close This

  • Drag structure.html Into Your Web Browser

    If all goes well:
    You should be looking at an ugly yellow page, with an even uglier JavaScript alert saying "Hello, world!" If it worked, look up, make eye contact with me, smile and show me the "OK" sign. (If it didn't, scowl at your screen and shake your head.)
    What just happened?
    You've created a modern, standards-compliant Web page, with semantically-valid structure, object-oriented programming, and good separation of structure, presentation, and behaviors.
    Do I really need three files? Can't I just smash this all together on one page?
    Yes, you can, and (after we're done) you should play with this, just to prove to yourself that it really does work. Also, if you're going to run this as a stand-alone desktop app, having only one file to worry about will be much better.
    And one more time: please cheat if you need to!
    Examples are right there; you can copy and paste, save-as, or even run the thing right from the page, one step at a time.
    Next up: creating the input form.
  • Creating the Input Form

    Add the green parts to behavior.js, so it looks like this:
    var WIKISEARCH = function() {
       var $ = {};
       return {
          init : function(el) {
             $.badge = document.getElementById(el);
             $.q = document.createElement('INPUT');
             $.q.onchange = function() {
                WIKISEARCH.runSearch(this.value);
             }
             $.badge.appendChild($.q);
          },
          runSearch : function(query) {
               alert(query);
          }
       };
    }();
    
    window.onload = function() {
       WIKISEARCH.init('wikiSearch');
    };
    The wikiSearch division is where our search box is going to live. We'll be using the same ID in all three layers, to keep things straight.
  • Adding the Division where the Widget Will Live

    Now all we need to do is add a place for the query blank to appear, in structure.html. Remove this:
    <h1>Hello, World!</h1>
    ... and substitute this:
    <div id="wikiSearch"></div>
    Save both files and reload what's in your Web browser. (No, presentation.css doesn't change during this step.)
    If you see an entry blank, type something and click outside the box. (Enter also works, for some browsers.) You ought to see an alert box with whatever it is you typed.
    What Just Happened?
    When your page loads, the script looks for a node with ID wikiSearch. If it finds it, it creates an input box and sets up a function that listens for changes to the box's value. If the value changes, you'll see it in an alert box.
    Next: hooking up the data.
  • Creating the Callback Function

    We'll need an array to store a bunch of new functions. Just above the closing bracket of your init function, add what's in green:
       $.badge.appendChild($.q);
       WIKISEARCH.ping = [];
    In your runSearch function, remove the line that says alert(query); and add this:
    runSearch : function(query) {
       var n = WIKISEARCH.ping.length;
       WIKISEARCH.ping[n] = function(result) {
          delete WIKISEARCH.ping[n];
          WIKISEARCH.removeScript('WIKISEARCH.ping[' + n + ']');
          if (result.ResultSet.totalResultsAvailable) {
             alert('Results found: ' + result.ResultSet.totalResultsAvailable);
          } else {
             alert('Nothing found, sorry!');
          }
       };
       var callback = 'WIKISEARCH.ping[' + n + ']';
    This creates a new function, in the next available element of our array WIKISEARCH.ping. If that's not clear, it's time for another side trip, this time into creating functions.
    When any instance of WIKISEARCH.ping runs, it deletes itself, runs removeScript to remove the script node that returned the data--we haven't written this yet, so don't panic!--and then displays search results.

    Diversion the Second: Dynamic Functions

    One of JavaScript's many hidden powers is the ability to modify itself while it's running, by adding, changing, and deleting functions. Consider the following:

       var dynaFunk = [];
       var n = dynaFunk.length;
       alert(n);
       dynaFunk[n] = function(myStuff) {
          alert(myStuff);
       }
       alert(dynaFunk.length);
       dynaFunk[0]('Ping!');

    What we've created above is functionally identical to this:

       dynaFunk[0] = function(myStuff) {
          alert(myStuff);
       }
       dynaFunk[0]('Pong!');

    In either case, if we try to run a function we haven't defined yet, we'll get into trouble:

       dynaFunk[6]('Woot!');

    Next up: Using the Function Array Index

    Using the Function Array Index

    A lovely extra bonus in creating dynamic arrays of functions is this: when called, each function will know its own array index.

       var dynaFunk = [];
       for (var i = 0; i < 4; i++){
          dynaFunk[i] = function(myNumber) {
             alert(myNumber * i);
          }
       }
       dynaFunk[3](9);
       dynaFunk[1](97);
       dynaFunk[0](6);
       dynaFunk[2](3);

    In this case we're taking the number we're passing in and multiplying it by the function's array index, proving that it knows its own index.

    This is very important when dealing with APIs that return data in JSON format. Chances are excellent your data provider won't want to send you back arbitrary text in addition to their nicely-sanitized data; that way lies extreme insecurity. In Yahoo!'s case, they compromise by allowing you to request callbacks with square brackets.

    Most dynamic functions run once. In order to keep from eating up available memory, we need to know how to delete them when we're done.

    Deleting Functions

    We delete functions the same way we delete any other array member, with the delete function. Here's our function again:

       var dynaFunk = [];
       for (var i = 0; i < 4; i++){
          dynaFunk[i] = function(myNumber) {
             alert(myNumber * i);
             delete dynaFunk[i];
          }
       }
       dynaFunk[2](12);
       dynaFunk[2](12);

    What just happened?

    We made four dynamic functions, dynaFunk[0] through dynaFunk[3]. When we ran dynaFunk[2](12), it ran, alerted its array index times its parameter, and then deleted itself. When we tried to run it again, it wasn't there.

    We're deleting the function at the end of its own body for reasons of clarity here; source order does not matter, however, and it's much easier to read if the delete function is up top, so that's where it's going to go for our production widget.

    That's it for our quick trip through dynamic functions; when you're ready, close this and we'll get back to work.

  • Creating the API Call

    Just under the last line you just added, enter this:
       var callback = 'WIKISEARCH.ping[' + n + ']';
       var url = 'http://search.yahooapis.com/WebSearchService/V1/webSearch?';
       url += 'appid=WikiSearch';
       url += '&results=20';
       url += '&site=en.wikipedia.org';
       url += '&output=json';
       url += '&query=' + $.q.value;
       url += '&callback=' + callback;
       WIKISEARCH.runScript(url, callback);
    Here we're creating the URL that will be called on Yahoo!'s server. For more information, please see developer.yahoo.com.
    We're asking for version 1 of the webSearch call on the WebSearchService service, specifying our application ID, the number of results and language we'd like, our query string, the site we'd like our search restricted to, and our output format and callback. Our callback is the function we just made, WIKISEARCH.ping[0], or one of its siblings.
    On the Importance of JSON and Callbacks
    Receiving our results in JSON format wrapped in the callback of our choice makes proxy-free applications possible. For more information on JSON, see JSON.org.
    We're still not ready to try this. We've created dynamic functions to handle the data and specified what data we want, but we still need some way to make the API call.
  • Requesting and Receiving the Data

    Still in behavior.js, add two new utility functions, right under runSearch. Be sure to put the comma after the closing bracket around runSearch, and not to put in an unnecessary one after removeScript:
       WIKISEARCH.runScript(url, callback);
    },
    runScript : function(url, id) {
       var s = document.createElement('script');
       s.id = id;
       s.type ='text/javascript';
       s.src = url;
       document.getElementsByTagName('body')[0].appendChild(s);
    },
    removeScript : function(id) {
       if (document.getElementById(id)) {
          var s = document.getElementById(id);
          s.parentNode.removeChild(s);
       }
    }
    We'll be collecting our data by creating and destroying additional JavaScript nodes as we need them, just as we created and destroyed functions inside runSearch. Our new function runScript runs dynamic script nodes; removeScript, unsurprisingly, removes them. If this isn't making sense, let's take another side trip into dynamic script nodes.
    Run this, and you should see an alert box showing how many results the API found.

    Diversion the Third: Dynamic Script Nodes

    Consider the following:

       <script type="text/javascript" src="http://foo.com/bar.js">

    When the page loads, bar.js runs. Now, consider this:

       var s = document.createElement('script');
       s.type ='text/javascript';
       s.src = 'http://foo.com/bar.js';
       document.getElementsByTagName('body')[0].appendChild(s);

    These two examples are functionally identical. In both cases, http://foo.com/bar.js loads and executes.

    The first example works in cases where bar.js is dynamically generated at the server end. This is how most ad servers do their heavy lifting.

    The second example is much more useful in cases where you don't know the exact URL you're going to load before the page renders.

    There's no reason why we can't just make an infinite stream of script nodes, but deleting them when we're done is a nice, neighborly thing to do.

    Deleting Script Nodes

    There are many ways to delete a DOM node, but we need to be very careful when deleting script nodes. Different browsers crash for different reasons; so far, the most reliable method I've found looks like this:

       var s = '';
       if (s = document.getElementById('myScript')) {
          s.parentNode.removeChild(s);
       }

    Here we're creating empty object s and attempting to assign to it the script node myScript. If this works, we look one step up in the document for its parent node and remove it from there.

    This won't work for us unless the script node we want to remove has an ID, and we know what it is. (In this case it's myScript, which we've hard-coded in ... but it could be anything.)

    Because we're using an API that returns results wrapped in a callback function, however, we can sneakily use that same callback function as our script node id, and remove the function and the script node in one swoop. If we watch the demo with Firebug's HTML tab, we'll see script nodes with IDs like WIKISEARCH.ping[0] being created and destroyed.

    Close This

  • Result Structure

    In order to display search results, we'll need some additional structure. First we'll add a new HTML element, a definition list, to contain our results.
    Definition lists are ideal for this, because they consist of matched pairs of items, a term followed by a definition. We'll put the result's URL and title in the term, and its description in the definition.
    Up in init, right after $.badge.appendChild($q), add this:
       $.badge.appendChild($.q);
       $.r = document.createElement('DL');
       $.badge.appendChild($.r);
       WIKISEARCH.ping = [];
    Next, head down to runSearch and remove this:
       if (result.ResultSet.totalResultsAvailable) {
          alert('Results found: ' + result.ResultSet.totalResultsAvailable);
       } else {
          alert('Nothing found, sorry!');
       }
    ... and let me know when you're ready to go. This next bit is fairly complicated, so we'll take it slowly.
  • Formatting Results

    Still in runSearch, substitute what's in green for what you just removed:
       WIKISEARCH.removeScript('WIKISEARCH.ping[' + n + ']');
       $.r.innerHTML = '';
       if (result.ResultSet.Result.length) {
          for (var i = 0; i < result.ResultSet.Result.length; i++) {
             var dt = document.createElement('DT');
             var a = document.createElement('A');
             a.rel = result.ResultSet.Result[i].Title;
             a.innerHTML = result.ResultSet.Result[i].Title.split(' - ')[0];
             a.href = result.ResultSet.Result[i].Url
             dt.appendChild(a);
             $.r.appendChild(dt);
             var dd = document.createElement('DD');
             dd.innerHTML = result.ResultSet.Result[i].Summary;
             $.r.appendChild(dd);
          }
       } else {
          var dt = document.createElement('DT');
          dt.innerHTML = 'Nothing found, sorry!';
          $.r.appendChild(dt);
       }
    
    The rel attribute will come into play later, when we start adding bookmarks.
    Run this, and you ought to see full search results.
  • Let's Get Small

    We want a widget, not a full page. Open up presentation.css, take out what's there, and substitute this:
    #wikiSearch * {
     color:#000; font-family:arial; font-size:13px;
    }
    #wikiSearch {
     position:relative; background-color:#888; border:1px solid #000; width:180px;
    }
    #wikiSearch dl{
     background-color:#fff; margin:0 5px 5px 5px; position:relative; width:170px;
    }
    #wikiSearch dl dd {
     background:#ffd; border:1px solid #000; display:none; margin:0 5px; overflow:hidden;
     position:absolute; width:160px;
    }
    #wikiSearch dl dt {
     position:relative; text-indent:15px; overflow:hidden; white-space:nowrap;
     width:170px;
    }
    #wikiSearch input {
     border:0; margin:5px; height:16px; width:170px;
    }
    Don't run this yet; we have a bit more work to make our descriptions pop in and out on mouseover.
  • Revealing Descriptions

    To get those descriptions to pop up, we'll need to take another look at behavior.js and modify runSearch:
    a.href = result.ResultSet.Result[i].Url;
    a.target = '_blank';
    a.onmouseover = function() {
       this.parentNode.nextSibling.style.display = 'block';
    }
    a.onmouseout = function() {
       this.parentNode.nextSibling.style.display = 'none';
    };
    dt.appendChild(a);
    $.r.appendChild(dt);
    var dd = document.createElement('DD');
    dd.innerHTML = result.ResultSet.Result[i].Summary;
    dd.style.zIndex = i + 100;
    $.r.appendChild(dd);
    Here we're telling the widget to open search results in new pages, show the summaries on mouseover, hide them on mouseout, and increase their Z index by 100, to make sure they show up on top of all of the other entries that might be dynamically generated afterwards. (This is also known as the "be nice to Opera" rule.)
    Save this, run it, and you ought to see happy little pop-ups with your descriptions.
  • Some Cosmetic Fixes

    Before we go any further, we're going to fix some annoying display bugs. First, pop open behavior.js and add this in init:
    $.r = document.createElement('DL');
    $.r.style.display = 'none';
    In runSearch, we need to remember to undo what we did above and show the results block, when we have results to show:
    $.r.innerHTML = '';
    $.r.style.display = 'block';
    A bit further down, we're going to add striping and white space:
    dt.appendChild(a);
    if (i % 2) { dt.className = 'odd'; }
    $.r.appendChild(dt);
    var dd = document.createElement('DD');
    var p = document.createElement('P');
    p.innerHTML = result.ResultSet.Result[i].Summary;
    dd.appendChild(p);
    dd.style.zIndex = i + 100;
    (Here we're adding CSS class odd to all odd-numbered rows, and creating a paragraph inside the DD, to hold the summary. Don't run it yet; we still need to fix the CSS.)
  • Adding Stripes, Removing Link Underlines, Padding Descriptions

    Open up presentation.css and add these lines. It doesn't matter where they go; personally, I tend to alphabetize my CSS, just to keep things consistent:
    #wikiSearch a {
      cursor:pointer; text-decoration:none;
    }
    
    #wikiSearch dl dt.odd {
      background-color:#eee;
    }
    
    #wikiSearch dl dd p {
      margin:5px; font-size:87%;
    }
    You may be wondering why we're not just adding some padding inside those DDs instead of putting in an entire new paragraph. Two reasons: when possible, I prefer to add margin and not padding, since there are major browser incompatiblities with padding. And later on we might want to go back and add headers or footers inside the description blocks; it's easier if the contents already has its own block-level element.
    Run it again; things should look much nicer. There should be stripes, white space in the description, and no underlined links anywhere.
  • Adding Bookmark Links

    Here how we're going to format presentation.css, to add our bookmark links. (That's a lower-case L in the URL, not the number 1. Sorry about that.):
    #wikiSearch dl dt a.bookmark {
      background:transparent
      url('http://l.yimg.com/us.yimg.com/i/ypicks/icons/delicious12.gif')
      0 50% no-repeat;
      left:2px; height:1.2em; position:absolute; top:0; width:12px;
    }
    Add 15px of text-indent to each line, so the link will show:
    #wikiSearch dl dt { position:relative; overflow:hidden;
      text-indent:15px; white-space:nowrap; width:170px; }
    In behavior.js, add a new link to each <dt>, before the existing one that shows the title:
    var dt = document.createElement('DT');
    var a = document.createElement('A');
    a.className = 'bookmark';
    a.title = 'save to del.icio.us';
    a.onmouseup = function() {
       WIKISEARCH.saveBookmark(this);
    }
    dt.appendChild(a);
    Don't run it yet; we still need to add the part that opens the bookmark page.
  • Saving Bookmarks

    When someone clicks one of our happy bookmark links, the following function (which you should add to behavior.js, just above runScript) will run:
    saveBookmark : function(el) {
       var ns = el.nextSibling;
       var url = 'http://del.icio.us/post/?';
       url += 'url=' + escape(ns.href);
       url += '&title=' + escape(ns.rel);
       url += '&jump=no';
       window.open(url,'popup','width=720px,height=420px',0);
    },
    runScript : function(url, id) {
    
    Bonus question for the audience: why are we using the rel attribute and not the title attribute to send down the title?
    Save this, run it, and search for something. When results come up, click the little del.icio.us dingbat to save a bookmark. You ought to see the title and URL filled in already; you'll need to tag it yourself.
    If you're not signed in to del.icio.us, you'll get their login prompt. If you don't have a login, you can run through their entire sign-in process in the same window and save your bookmark. I strongly recommend del.icio.us as an excellent information-sharing tool.
  • Status Indicator and Home Page Link

    It's nice to have a visual indicator that the widget is working, and we need a link back to the search page while it's not. Let's open up behavior.js and add the link, in the init function, right before we insert the input box:
    $.badge = document.getElementById(el);
    $.h = document.createElement('A');
    $.h.className = 'home';
    $.h.target = '_blank';
    $.badge.appendChild($.h);
    $.q = document.createElement('INPUT');
    In runSearch, change the class name and add the URL whenever it's is running:
    var n = WIKISEARCH.ping.length;
    $.h.className = 'loading';
    WIKISEARCH.ping[n] = function(result) {
       delete WIKISEARCH.ping[n];
       WIKISEARCH.ping[n] = function(result) {
          $.h.href = 'http://en.wikipedia.org/wiki/' + $.q.value;
    Still in runSearch, quit showing the "loading" image after results come back:
          $.r.appendChild(dt);
       }
       $.h.className = 'home';
    };
  • Status Indicator and Home Page CSS

    Two new classes and one margin change, in presentation.css. (That's a lower-case L in the image URL; sorry about that.)
    #wikiSearch a.loading, #wikiSearch a.home {
      display:block;
      background:transparent
        url('http://l.yimg.com/us.yimg.com/i/us/my/mw/anim_loading_sm.gif')
        50% 50% no-repeat;
      position:absolute;
      top:5px;
      left:5px;
      height:16px;
      width:16px;
    }
    
    #wikiSearch a.home {
      background-image:url('http://en.wikipedia.org/favicon.ico');
    }
    The margin and width change leaves space for the progress indicator/home-page link:
    #wikiSearch input {
      border:0; margin:5px 5px 5px 26px; height:16px; width:149px;
    }
    Save it and run it. We're almost through ... there's just one more thing left.
  • Last Trick: Make It Run As Soon As The User Starts Typing

    What's in green is our final modification to runSearch, a conditional block that only runs if we're making a new query:
    if ($.q.value) {
       if ($.q.value != $.lastQuery) {
          $.lastQuery = $.q.value;
          var n = WIKISEARCH.ping.length;
    At the bottom of runSearch, close your new conditional block and tell it what to do if the user blanks out the query box after running a query:
          WIKISEARCH.runScript(url, callback);
       }
    } else {
       if ($.lastQuery) {
          $.r.style.display = 'none';
          $.r.innerHTML = '';
          $.h.href = 'http://en.wikipedia.org';
       }
    }
    We're only going to run a search if there's something in the query box ($.q.value) which is different from the last query we ran, which we'll store in $.lastQuery.
    Don't run this yet; we need to re-rig the query box.
  • Make It Listen for Input

    In init, remove this:
    $.q.onchange = function() {
       WIKISEARCH.runSearch(this.value);
    }
    ... and make this change:
       WIKISEARCH.ping = [];
       setInterval(WIKISEARCH.runSearch, 500);
    },
    Instead of waiting for the onchange event to fire, we're going to check whether we need to run a search, every 500 milliseconds.
    By removing the search button and displaying results inline, we relieve the user of the responsibility of saying when he (or she) is done typing. This will greatly increase the user's perception of speed, even though the widget isn't really running any faster than a conventional search page.
    Run it. If it works, we're done!
  • More URLs To Worry About

    Questions? Comments?
    You can reach me via my blog:
    http://kentbrewster.com/wiki-search-widget
    APIs and documentation?
    Yahoo! Developer Network:
    http://developer.yahoo.com
    Looking for Work?
    http://careers.yahoo.com
    Keep an Eye Out for Hack Day!
    http://hackday.org