widgetJavascriptConsole.js

Summary

No overview generated for 'widgetJavascriptConsole.js'


// Javascript Console widget
// Ported from http://www.squarefree.com/shell/shell.html

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.

// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

// Contact and support is available through the Sourceforge project
// "xblinjs", at https://sourceforge.net/projects/xblinjs/ .

/**
  Nice Javascript console, embeddable into your page.

  @summary This is derived from <a
  href="http://www.squarefree.com/shell/shell.html">squarefree.com</a>
  and adapted to the XBLinJS framework.

  <p><b>Differences From Source</b>:</p>

  <ul>
    <li>I dropped the ability to run in the opener's context, as I
        don't think this will be used that way. Use the bookmarklet
        provided by squarefree.com.</li>
    <li>Copied the code to force the selection into textbox, but made
        it optional as that would make it hard to debug other widgets
        on the screen.</li>
    <li>If a history entry is the same as the last one, it is not
        added to the history.</li>
    <li>I'm not sure what the _win.location.href stuff is, so I
        skipped it. Might break something (though whatever that
        something is, it's not Mozilla or IE...).</li>
    <li>Made it easier to add your own "shell commands" in a
        subclass, with better support for help and showing keyboard
        commands on the screen.</li>
    <li>I dropped the feature where making an entry, then pushing down 
        enters it into history and clears the input... I don't get it,
        if someone can explain why they want that I can put it back
        in.</li>
    </ul>

  <p><b>Relevant Parameters</b>:</p>

  <ul>
    <li><b>keepfocus</b>: Whether the widget should try to hold onto
        the focus. Defaults to false, and only works in browsers
        that support .addEventListener.</li>
    <li><b>clear_input</b>: Whether the widget should clear the 
        input when executed. Defaults to <tt>true</tt> to match the
        source's performance, but I sometimes like <tt>false</tt>;
        depends on the nature of the debugging task.</li>
    </ul>

  <p><b>Implementing your own shell commands</b>:</p>

  <p>The JavascriptConsole includes the idea of adding in "shell
  commands", basically just functions put into the scope of 
  execution, which you can provide for the user to use.</p>

  <p>The shell commands are collected in the following manner:</p>

  <ul><li>Anything in the object starting with the word
      "shellCommands" will be scanned for command objects,
      as described below. If false, they will be skipped.
      The order chosen is random, as Javascript iterates
      the object, so do not multiply-define things.</li>
      <li>After that, the object is scanned for anything starting 
      with "shell_", which will be added to the list of shell
      commands. Anything false will be skipped. This can be used
      to override the above; to mask "help" out, create a 
      member called .shell_help and set it to false, and the
      help message will go away.</li>
    </ul>

  <p>Shell commands are sorted by key name, then presented to the user
  at the top of the console.</p>

  <p>Shell commands may be one of two things. They may simply be a
  function, in which case they will be shown by key name on the 
  shell command line. They may also be Javascript objects, which 
  can contain the entries shown below. Regardless, the shell commands
  will recieve as their first parameter a reference to their
  JavascriptConsole object, followed by whatever parameters the
  user may have given.</p>

  <ul>
    <li><b>func</b>: This is the actual function that will be
        run.</li>
    <li><b>hide</b>: A boolean; if true, the command will not
        be shown. Use this if you want a "hidden" command, like
        "pr" in the original shell. Use expressions to dynamically
        select what shell commands are available.</li>
    <li><b>accessKey</b>: A link will be attached to the function
        name in the shell display, and given this accessKey, allowing 
        the user to execute it with "alt-[accessKey]". The shortcut
        will be displayed to the user.</li>
    <li><b>defaultExe</b>: The default thing executed when the shell
        command is clicked on. This should be a string, which
        will be passed to the widget to execute as if it were
        typed in.</li>
    <li><b>helpText</b>: If given, a question mark will be added next
        to the display of the name of the function, which, when
        clicked, will print out this text.<br /><br />
        For advanced uses, helpText can also be a function
        recieving the widget as the first parameter (so it
        can do advanced printing, most likely).</li>
    <li><b>displayedName</b>: The name that will be displayed,
        including if desired any argument info. This string
        will be displayed <i>exactly</i>. Default is
        the key name plus "(object(ans))", indicating the "ans"
        parameter will be used if you click on it by default.</li>
    </ul>

  <p>Only <tt>func</tt> is necessary, but if that is all you have,
  you might as well leave it just as a function.</p>
*/
deriveNewWidget("JavascriptConsole", Widget);

/** @private */
JavascriptConsole.prototype.content = {
  tagName: "div", className: "Console", children: [
    {tagName: "h3", name: "features", children: [
      "JavaScript Shell (XBLinJS), based on ",
       {tagName: "a", 
        href: "http://www.squarefree.com/shell/shell.html",
        children: ["JavaScript Shell 1.1"]}
      ]},
    {tagName: "div", className: "ConsoleShellCommands", name: "shellComDiv"},
    {tagName: "div", className: "ConsoleOutput", name: "output"},
    {widgetType: "DOMWrap", domNode: {tagName: "input", 
                                      className: "ConsoleInputBox"},
     name: "input"}
  ]
}

/**
  @private
*/
JavascriptConsole.prototype.defaults = {
  keepfocus: false,
  clear_input: true,
  tooManyMatches: null
}

/**
  Initializes the shell command display.

  @private
*/
JavascriptConsole.prototype.initDOM = function (atts) {
  Widget.prototype.initDOM.apply(this, arguments);

  /** 
    Add history to the text widget.
    @private 
  */
  this.historyWidget = new HistoryAddin({target: this.input});

  // now, let's collect the shell commands.
  /**
    An object containing the shell commands, as callable functions.
  */
  this.shellFuncs = {};

  /**
    The current scope the widget is borrowing, if any.
  */
  this.scope = {};

  /**
    Stores the special "answer" value.

    @private
  */
  this.answer = {ans: undefined};

  /**
    An object containing the help messages.

    @private
  */
  this.helpMessages = {};

  // things bundled in shellCommand objects
  var commandCollections = [];
  // things found as .shell_*
  var commands = {};
  for (var name in this) {
    if (name.substr(0, 12) == "shellCommand") {
      commandCollections.push(this[name]);
    }
    if (name.substr(0, 6) == "shell_") {
      commands[name.substr(6)] = this[name];
    }
  }
  commandCollections.push(commands);

  // complete the collection process
  var commands = {};
  for (var idx in commandCollections) {
    var commandList = commandCollections[idx];
    for (var commandName in commandList) {
      commands[commandName] = commandList[commandName];
    }
  }
  var commandKeyList = getKeys(commands);
  commandKeyList.sort();
  
  // process the command list
  var shownCommand = false;
  for (var idx in commandKeyList) {
    var commandName = commandKeyList[idx];

    var command = commands[commandName];

    // skip falses
    if (!command) continue;

    // normalize it
    if (typeof command == FUNCTION_TYPE) {
      command = {func: command};
    }

    if (command.hide) continue;

    // actually stick it in the context
    this.shellFuncs[commandName] = closureMaker({jc: this, 
                                                 shellFunc: command.func},
      function (args, params) {
         args = arrayCopy(args);
         args.unshift(params.jc);
         params.shellFunc.apply(params.shellFunc, args);
      });


    // generate the arguments...
    var func = command.func;
    var accessKey = command.accessKey;
    var defaultExe = command.defaultExe;
    // if there is an accessKey but no given command, provide one
    if (accessKey && !defaultExe) {
      defaultExe = commandName + "(ans)";
    }
    var helpText = command.helpText;
    var displayedName = command.displayedName;
    if (!displayedName) displayedName = commandName + "(ans)";

    // now show it
    var node = this.create("span", {className: "ConsoleShellCommand"});
    var text;
    // link?
    if (defaultExe) {
      text = this.create("a", {href: "javascript:WidgetGlobals.Widgets[" +
                               this.widgetId + "].go(unescape('"
                               + escape(defaultExe) + "'))", 
                               accesskey: accessKey}, displayedName);
    } else {
      text = textNode(displayedName);
    }
    node.appendChild(text);
    // that handles the name and the execution, now, help?
    if (helpText) {
      var help = this.create("a", {href: "javascript: void(0)", 
                                   className: "ConsoleHelp"},
                             "?");
      this.helpMessages[commandName] = helpText;
      if (typeof helpText == FUNCTION_TYPE) {
        help.onclick = closureMaker({jc: this,
                                     helpFunc: helpText},
          function (args, params) {
            args = arrayCopy(args);
            args.unshift(params.jc);
            params.helpFunc.apply(params.helpText, args);
            params.jc.refocus();
          });
      } else {
        // must be a string
        help.onclick = closureMaker({jc: this, key: commandName},
          function (args, params) {
            with (params) {
              jc.printWithRunin("Help for " + key, jc.helpMessages[key],
                                "ConsoleHelpDisplay");
              jc.refocus();
            }
          });
      }
      node.appendChild(help);
    }

    // And finally, keyboard shortcut?
    if (accessKey) {
      var keyHelp = this.create("span", {className: "ConsoleAccessKey"},
                                " (ALT-" + accessKey.toUpperCase() + ")");
      node.appendChild(keyHelp);
    }

    // if this isn't the first, add a comma
    if (!shownCommand) {
      this.shellComDiv.appendChild(textNode("Shell Commands: "));
      shownCommand = true;
    } else {
      this.shellComDiv.appendChild(textNode(", "));
    }
    // append the node
    this.shellComDiv.appendChild(node);

    // HACK: Without this, the accesskeys won't work on IE6, as
    // setAttribute("accesskey") doesn't work. Note 
    if (this.isIE) {
      node.outerHTML = node.outerHTML;
    }
  }
}

/** @private */
JavascriptConsole.prototype.initWidget = function (atts) {
  Widget.prototype.initWidget.apply(this, arguments);
 
  var js = this;

  this.getGlobals().onloadFunctions.push(function () {js.refocus()});
}

/**
  Prints a line on the console.

  @param s The string to print.
  @param type The CSS class to assign to the printed line. (See
  widgets.css for the .Console* lines.)
*/
JavascriptConsole.prototype.println = function (s, type) {
  if (s = String(s)) {
    var newDiv = this.create("div", {className: type}, s);
    this.output.appendChild(newDiv);
    return newDiv;
  }
}

/**
  Prints a line on the console, with a "runin".

  <p>A "runin", apparently, is a bolded text with a colon appended;
  so, for example, you could show help with:
  <tt>this.printWithRunin("Help", "Some help.",
  "ConsoleHelpDisplay")</tt>.</p>

  @param h The header to display.
  @param s The string to display.
  @param type The CSS class to assign to the text. (See widgets.css
  for the .Console* lines.)
*/
JavascriptConsole.prototype.printWithRunin = function (h, s, type) {
  var div = this.println(s, type);
  var head = this.create("strong", {}, h + ": ");
  div.insertBefore(head, div.firstChild);
}

/** @private */
JavascriptConsole.prototype.printQuestion = function (q) {
  this.println(q, "ConsoleInput");
}

/** 
  Note this also stores the answer in the ans slot; not sure this
  is great design, but I guess this is private so it's OK...

  @private 
*/
JavascriptConsole.prototype.printAnswer = function (a) {
  if (a !== undefined) {
    this.println(a, "ConsoleNormalOutput");
    // FIXME: Use right thingy
    if (this.shellCommands) {
      this.answer.ans = a;
    }
  } 
}

/**
  Print an exception. Handles IE vs. Mozilla differences.

  @param er The exception to print.
*/
JavascriptConsole.prototype.printError = function (er) {
  if (er.name) {
    // Because IE doesn't have error.toString.
    this.println(er.name + ": " + er.message, "ConsoleError");
  } else {
    // Because security errors in Moz /only/ have toString.
    this.println(er, "ConsoleError");
  }
}

/** @private */
JavascriptConsole.prototype.set_keepfocus = function (val) {
  val = !!val;

  // this is checked by the listener.
  this.keepfocus = val;
  var jc = this;

  if (val && !this.registeredKeepFocusListener &&
      window.getSelection && document.addEventListener) {
    var listener = function (e) {
      // abort if focus theft was turned off
      if (!jc.keepfocus) return;

      var targetNode = e.target;
      while (!targetNode.tagName)
        targetNode = targetNode.parentNode;
      var tagName = targetNode.tagName.toUpperCase();
      if (tagName != "A" && tagName != "INPUT" &&
          !String(window.getSelection()))
        jc.refocus();
    }
    document.addEventListener("click", listener, false);

    this.registeredKeepFocusListener = true;
  }
}

/**
  @private
*/
JavascriptConsole.prototype.onkeydown_input = function (event) {
  var jc = this;
  if (event.keyCode == 9) {
    this.tabComplete();
    setTimeout(function () {jc.refocus();}, 0);
    return false;
  }
}

/**
  When enter is pressed, execute the command.

  @private
*/
JavascriptConsole.prototype.onkeypress_input = function (event) {
  if (event.keyCode == 13) {
    this.go();
  }
}

/**
  Execute the given string as input.

  @param s The string to execute.
*/
JavascriptConsole.prototype.go = function (_s_) {
  // every variable in this function leaks out into the context
  // the user is evaluating their code in, which explains the
  // _name_s in this function
  var _question_ = _s_ ? _s_ : this.input.getAttribute("value");

  // special case: if it's "help", print the help
  if (_question_.toLowerCase() == "help") {
    _question_ = "help()";
  }

  if (_question_ == "") return;

  this.historyWidget.addToHistory(_question_);

  // Evaluate it.
  var answer;
  this.printQuestion(_question_);
  try {
      with (this.shellFuncs)
        with (this.scope)
          with (this.answer)
            this.printAnswer(eval(_question_));
  } catch (e) {
    this.printError(e);
  }
  if (this.clear_input) {
    setAttributeOn(this.input, "value", "");
  }
  this.refocus();
}

/**
  Perform tab completion.

  @private
*/
JavascriptConsole.prototype.tabComplete = function () {
  var jc = this;
  /*
   * Working backwards from s[from], find the spot
   * where this expression starts.  It will scan
   * until it hits a mismatched ( or a space,
   * but it skips over quoted strings.
   * If stopAtDot is true, stop at a '.'
   */
  function findbeginning(s, from, stopAtDot)
  {
    /*
     *  Complicated function.
     *
     *  Return true if s[i] == q BUT ONLY IF
     *  s[i-1] is not a backslash.
     */
    function equalButNotEscaped(s,i,q)
    {
      if(s.charAt(i) != q) // not equal go no further
        return false;

      if(i==0) // beginning of string
        return true;

      if(s.charAt(i-1) == '\\') // escaped?
        return false;

      return true;
    }

    var nparens = 0;
    var i;
    for(i=from; i>=0; i--)
    {
      if(s.charAt(i) == ' ')
        break;

      if(stopAtDot && s.charAt(i) == '.')
        break;
        
      if(s.charAt(i) == ')')
        nparens++;
      else if(s.charAt(i) == '(')
        nparens--;

      if(nparens < 0)
        break;

      // skip quoted strings
      if(s.charAt(i) == '\'' || s.charAt(i) == '\"') //"
      {
        var quot = s.charAt(i);
        i--;
        while(i >= 0 && !equalButNotEscaped(s,i,quot)) {
          //dump(s.charAt(i));
          i--;
        }
        //dump("\n");
      }
    }
    return i;
  }

  function getcaretpos(inp)
  {
    if(inp.selectionEnd)
      return inp.selectionEnd;

    if(inp.createTextRange)
    {
      //dump('using createTextRange\n');
      var docrange = this.window.document.selection.createRange();
      var inprange = inp.createTextRange();
      inprange.setEndPoint('EndToStart', docrange);
      return inprange.text.length;
    }

    return inp.value.length; // sucks, punt
  }

  function setselectionto(inp,pos)
  {
    if(inp.selectionStart) {
      inp.selectionStart = inp.selectionEnd = pos;
    }
    else if(inp.createTextRange) {
      var docrange = this.window.document.selection.createRange();
      var inprange = inp.createTextRange();
      inprange.move('character',pos);
      inprange.select();
    }
    else { // err...
    /*
      inp.select();
      if(this.window.document.getSelection())
        this.window.document.getSelection() = "";
        */
    }
  }
    // get position of cursor within the input box
    var caret = getcaretpos(DOM(this.input));

    if(caret) {
      //dump("----\n");
      var dotpos, spacepos, complete, obj;
      //dump("caret pos: " + caret + "\n");
      // see if there's a dot before here
      dotpos = findbeginning(this.input.getAttribute("value"), caret-1, true);
      //dump("dot pos: " + dotpos + "\n");
      if(dotpos == -1 || this.input.getAttribute("value").charAt(dotpos) != '.') {
        dotpos = caret;
	//dump("changed dot pos: " + dotpos + "\n");
      }

      // look backwards for a non-variable-name character
      spacepos = findbeginning(this.input.getAttribute("value"), dotpos-1, false);
      //dump("space pos: " + spacepos + "\n");
      // get the object we're trying to complete on
      if(spacepos == dotpos || spacepos+1 == dotpos || dotpos == caret)
      {
        // try completing function args
        if(this.input.getAttribute("value").charAt(dotpos) == '(' ||
	 (this.input.getAttribute("value").charAt(spacepos) == '(' && (spacepos+1) == dotpos))
        {
          var fn,fname;
	  var from = (this.input.getAttribute("value").charAt(dotpos) == '(') ? dotpos : spacepos;
          spacepos = findbeginning(this.input.getAttribute("value"), from-1, false);

          fname = this.input.getAttribute("value").substr(spacepos+1,from-(spacepos+1));
	  //dump("fname: " + fname + "\n");
          try {
              with(this.window)
                //FIXME
                //with(Shell.shellCommands)
                  fn = eval(fname);
          }
          catch(er) {
            //dump('fn is not a valid object\n');
            return;
          }
          if(fn == undefined) {
             //dump('fn is undefined');
             return;
          }
          if(fn instanceof Function)
          {
            // Print function definition, including argument names, but not function body
            if(!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
              this.println(fn.toString().match(/function .+?\(.*?\)/), "ConsoleTabComplete");
          }

          return;
        }
        else
          obj = this.window;
      }
      else
      {
        var objname = this.input.getAttribute("value").substr(spacepos+1,dotpos-(spacepos+1));
        //dump("objname: |" + objname + "|\n");
        try {
            with(this.window)
                obj = eval(objname);
        }
        catch(er) {
          this.printError(er); 
          return;
        }
        if(obj == undefined) {
          // sometimes this is tabcomplete's fault, so don't print it :(
          // e.g. completing from "print(document.getElements"
          // println("Can't complete from null or undefined expression " + objname, "error");
          return;
        }
      }
      //dump("obj: " + obj + "\n");
      // get the thing we're trying to complete
      if(dotpos == caret)
      {
        if(spacepos+1 == dotpos || spacepos == dotpos)
        {
          // nothing to complete
          //dump("nothing to complete\n");
          return;
        }

        complete = this.input.getAttribute("value").substr(spacepos+1,dotpos-(spacepos+1));
      }
      else {
        complete = this.input.getAttribute("value").substr(dotpos+1,caret-(dotpos+1));
      }
      //dump("complete: " + complete + "\n");
      // ok, now look at all the props/methods of this obj
      // and find ones starting with 'complete'
      var matches = [];
      var bestmatch = null;
      for(var a in obj)
      {
        //a = a.toString();
        //XXX: making it lowercase could help some cases,
        // but screws up my general logic.
        if(a.substr(0,complete.length) == complete) {
          matches.push(a);
          ////dump("match: " + a + "\n");
          // if no best match, this is the best match
          if(bestmatch == null)
          {
            bestmatch = a;
          }
          else {
            // the best match is the longest common string
            function min(a,b){ return ((a<b)?a:b); }
            var i;
            for(i=0; i< min(bestmatch.length, a.length); i++)
            {
              if(bestmatch.charAt(i) != a.charAt(i))
                break;
            }
            bestmatch = bestmatch.substr(0,i);
            ////dump("bestmatch len: " + i + "\n");
          }
          ////dump("bestmatch: " + bestmatch + "\n");
        }
      }
      bestmatch = (bestmatch || "");
      ////dump("matches: " + matches + "\n");
      var objAndComplete = (objname || obj) + "." + bestmatch;
      //dump("matches.length: " + matches.length + ", this.tooManyMatches: " + this.tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
      if(matches.length > 1 && (this.tooManyMatches == objAndComplete || matches.length <= 10)) {

        this.printWithRunin("Matches", matches.join(', '), "ConsoleTabComplete");
        this.tooManyMatches = null;
      }
      else if(matches.length > 10)
      {
        this.println(matches.length + " matches.  Press tab again to see them all", "ConsoleTabComplete");
        this.tooManyMatches = objAndComplete;
      }
      else {
        this.tooManyMatches = null;
      }
      if(bestmatch != "")
      {
        var sstart;
        if(dotpos == caret) {
          sstart = spacepos+1;
        }
        else {
          sstart = dotpos+1;
        }
        setAttributeOn(this.input, "value",
                  getAttributeFrom(this.input, "value").substr(0, sstart)
                  + bestmatch
                  + getAttributeFrom(this.input, "value").substr(caret));
        setselectionto(DOM(this.input),caret + (bestmatch.length - complete.length));
      }
    }
}

/**
  Focus on the input box; use this to ensure the screen has been
  scrolled to the box after user input.

  <p>Copied from source, which comments the "blur" is needed for mozilla
  to scroll correctly.</p>
*/
JavascriptConsole.prototype.refocus = function () {
  DOM(this.input).blur();
  DOM(this.input).focus();
}

/**
  This is the list of shell commands provided by the authors of 
  the original Javascript Shell.

  <p>In the context passed to these shell commands is the variable
  "jc", for "JavascriptConsole", which can be used to access the 
  JavascriptConsole methods like "print".</p>

  @private
*/
JavascriptConsole.prototype.shellCommands = {
  load: {
    /** @private */
    func: function load(jc, url)
    {
      /** 
        @private 
      */
      var s;
      s = jc.window.document.createElement("script");
      s.type = "text/javascript";
      s.src = url;
      jc.window.document.getElementsByTagName("head")[0].appendChild(s);
      jc.println("Loading " + url + "...", "message");
    },
    displayedName: "load(scriptURL)",
    helpText: "Loads the script at scriptURL into the current window."
  },

  print: {
    /** @private */
    func: function print(jc, s) { jc.println(s, "print"); },
    displayedName: "print(string)",
    helpText: "Print the given string out, converting to string via new String()"
  },

  // from source:
  // the normal function, "print", shouldn't return a value
  // (suggested by brendan; later noticed it was a problem when showing others)
  pr: {
    /** @private */
    func: function pr(jc, s) { 
      jc.shellCommands.print(s);
      return s;
    },
    displayedName: "pr(string)",
    helpText: "Print out the given string and also return it. This"+
              " is useful in the middle of an expression: x = "+
              " pr(2 + 2) * 5;"
  },

  props: {
    /** @private */
    func: function props(jc, e) {
      var ns = ["Methods", "Fields", "Unreachables"];
      var as = [[], [], []]; // array of (empty) arrays of arrays!

      var protoLevels = 0;

      for (var p = e; p; p = p.__proto__)
      {
        for (var i=0; i<ns.length; ++i)
          as[i][protoLevels] = [];
        ++protoLevels;
      }

      for(var a in e)
      {
        // Shortcoming: doesn't check that VALUES are the same in object and prototype.

        var protoLevel = -1;
        try
        {
          for (var p = e; p && (a in p); p = p.__proto__)
            ++protoLevel;
        }
        catch(er) { protoLevel = 0; } // "in" operator throws when param to props() is a string

        var type = 1;
        try
        {
          if ((typeof e[a]) == "function")
            type = 0;
        }
        catch (er) { type = 2; }

        as[type][protoLevel].push(a);
      }

      function times(s, n) { return n ? s + times(s, n-1) : ""; }

      for (var j=0; j<protoLevels; ++j)
        for (var i=0;i<ns.length;++i)
          if (as[i][j].length) 
            jc.printWithRunin(ns[i] + times(" of prototype", j), as[i][j].join(", "), "ConsolePropList");
    },
    helpText: ("Display the methods and fields of an object, " +
               "broken down by how they are inherited"),
    displayedName: "props(object)",
    accessKey: "p"
    
  },

  blink: {
    /** @private */
    func: function blink(jc, e) {
      if (!e)                         throw("blink: e is null or undefined.");
      e = DOM(e);
      if (!e.TEXT_NODE)               throw("blink: e must be a node.");
      if (e.nodeType == e.TEXT_NODE)  throw("blink: e must not be a text node");
      if (e.documentElement)          throw("blink: e must not be the document object");

      function setOutline(e,o) { e.style.MozOutline = o; }
      function focusIt(a) { a.focus(); }
   
      if (e.ownerDocument)
        setTimeout(focusIt, 0, e.ownerDocument.defaultView.top);
 
      for(var i=1;i<7;++i) 
        setTimeout(setOutline, i*100, e, (i%2)?'3px solid red':'none');
    
      setTimeout(focusIt, 800, window);
    },
    // Show only for Mozilla
    hide: (!navigator || !navigator.userAgent || 
           (navigator.userAgent.toLowerCase().indexOf("gecko") == -1)),
    helpText: "Blink the given non-text DOM node or Widget (try 'blink(this.text)')",
    accessKey: "b",
    displayedName: "blink(node)"
  },

  scope: {
    /** @private */
    func: function scope(jc, sc) {
      if (!sc) sc = {};
      jc.scope = sc;
      jc.println("Scope is now " + sc + ".  If a variable is not found in this scope, window will also be searched.  New variables will still go on window.", "ConsoleMessage");
    },
    helpText: ("Changes the scope the Widget searches when executing"+
               " commands. Example: The 'math' command works by "+
               "executing 'scope(Math)', so all Math methods and "+
               "constants are available without prefix."),
    displayedName: "scope(object)"
  },

  math: {
    /** @private */
    func: function (jc) {
      jc.shellFuncs.scope(Math);
      jc.printWithRunin("Math constants", "E, LN2, LN10, LOG2E, LOG10E, PI, SQRT1_2, SQRT2", "propList");
      jc.printWithRunin("Math methods", "abs, acos, asin, atan, atan2, ceil, cos, exp, floor, log, max, min, pow, random, round, sin, sqrt, tan", "propList");
    },
    helpText: "Turns this console into a simple calculator by "+
              "changing the scope to the Math object",
    accessKey: "m",
    defaultExe: "math()",
    displayedName: "math()"
  },

  help: {
    /** @private */
    func: function (jc) {
      jc.println("This is a Javascript Console, implemented in XBLinJS,"+
               " as derived from Javascript Shell 1.1.",
               "ConsoleMessage");
      jc.println("Use it as you would Javascript, which will not be "+
               "explained here.", "ConsoleMessage");
      jc.println("Note that the last result from the command line "+
               "is available as 'ans', and that the 'Features' "+
               "are additional functions provided for your use; "+
               "see documentation for JavascriptConsoleWidget " +
               "for information about customization.",
               "ConsoleMessage");
      jc.println("Use history with up/down arrows, use TAB for "+
                 "completion.", "ConsoleMessage");
    },
    accessKey: "h",
    displayedName: "help",
    defaultExe: "help()"
  },

  toggleKeepFocus: {
    /** @private */
    func: function (jc) {
      jc.set("keepfocus", !jc.get("keepfocus"));
      var focus = jc.get("keepfocus");
      if (focus) {
        jc.println("Console will now try to hold onto the focus.");
      } else {
        jc.println("Console will not hold focus.");
      }
    },
    hide: !(window.getSelection && document.addEventListener),
    displayedName: "toggleKeepFocus()",
    accessKey: "f",
    defaultExe: "toggleKeepFocus()",
    helpText: function (jc) {
      var focused = jc.get("keepfocus");
      var help = ("Toggles whether the widget tries to retain the focus,"+
              " even when other parts of the document are clicked."+
              " Some people like this. Currently, this setting is ")
      help += focused ? "on." : "off.";
      jc.printWithRunin("Help for toggleKeepFocus", help, "ConsoleHelpDisplay");
    }
  },

  clear: {
    /** @private */
    func: function (jc) {
      emptyNode(jc.output);
    },
    displayedName: "clear()",
    accessKey: "c",
    defaultExe: "clear()",
    helpText: "Clears the console's output history."
  }
};

/**
  History addition widget - adds history to a widget, like "readline"

  @summary The History Addition widget is a mix-in Widget that adds
  history capabilities to any Widget that uses the "value" property to
  indicate the value of the widget, though it is likely to only be
  useful to text nodes. (It only works on Widgets since it assumes
  XBLinJS event handling in order to safely tie into the target 
  safely.)

  <p>This widget must be able to capture UP and DOWN keypresses with 
  the XBLinJS event handling system, and something must call the 
  <tt>.addToHistory</tt> methods when something occurs that needs to
  be added to the history.</p>

  <p>This widget has no DOM nodes, but is a Widget because it taps
  into the Widget event handling.</p>

  <p>Note that while I can't imagine a particular use, that it is
  not technically necessary for the "value" in question to be a
  string; this object should continue to work even if .getAttribute
  returns an object or something.</p>

  <p><b>Relevant Parameters</b>:</p>

  <ul>
    <li><b>target</b>: The target node or widget to bind to.</li>
    </ul>

  <p><b>Example</b>:</p>

  <p>If <tt>text</tt> is a text input widget, the following will
  activate it for history processing (along with someone calling
  <tt>.addToHistory</tt> sometime):</p>

  <p><tt>history = new HistoryAddin({target: text});</tt></p>

  <p>Generally you will do this inside an <tt>.initDOM()</tt>.</p>
*/
deriveNewWidget("HistoryAddin", Widget);

/** @private */
HistoryAddin.prototype.content = false;

/** 
  Initializes the handlers and such.
*/
HistoryAddin.prototype.initWidget = function (atts) {
  this.processAtt(atts, "target");

  /**
    The array of entries into the history list. You may read this,
    but it is private for writing.
  */
  this.histList = [];

  /**
    The current position in the history list. "undefined" is used 
    to indicate we are at the end.
  */
  this.histPos = undefined;

  /**
    A "temp" field that is used to store the original entry while
    the user is examining history. If .addToHistory is called, this
    will be eliminated, otherwise if the user cursors down far
    enough, this will be returned to the target widget.
  */
  this.tempHist = undefined;
  // parenthetically to any who might read this and notice the
  // difference between this implementation and the original, 
  // the way the original sort of implied this field was driving
  // me nuts.

  // register the foreign event handlers
  this.target.registerEventHandler("keydown", "handle_onkeydown",
                                   false, this);
}

/**
  addToHistory - add the given value to the history

  <p>This also resets the position of the history list.</p>

  @param s The string to add. If the string is false (or the empty
  string), the history feature will reset everything as normal,
  but will not add the string to the history. This will also decline
  to add an entry that is == to the previous one.
*/
HistoryAddin.prototype.addToHistory = function (s) {
  if (!(s == "" || !s || s == this.histList[this.histList.length - 1]))
    this.histList.push(s);
  this.tempHist = undefined;
  this.reset();
}

/**
  reset - resets the position of the history to the end

  <p>This resets the history position tracker. It does <i>not</i>
  change the value of the associated widget.</p>
*/
HistoryAddin.prototype.reset = function () {
  this.histPos = undefined;
}

/**
  onkeydown handler for history

  <p>From source: Use onkeydown because IE doesn't support onkeypress
  for arrow keys.</p>
*/
HistoryAddin.prototype.handle_onkeydown = function (event) {
  var keyCode = event.keyCode;
  var histList = this.histList;
  var historyAddin = this;

  // "up" key, 57373 is opera, apparently
  if (keyCode == 38 || keyCode == 57373) {
    var setTo;
    // Cases: 
    // 1. This is a fresh history request
    // 2. We're already in history
    // 3. We're already at the top
    if (this.histPos == undefined) {
      this.histPos = this.histList.length - 1;
      setTo = this.histList[this.histPos];
      this.tempHist = getAttributeFrom(historyAddin.target, "value");
    } else if (this.histPos > 0) {
      this.histPos--;
      setTo = this.histList[this.histPos];
    } else {
      // trying to cursor off the top
      return false;
    }
    // from source
    // Use a timeout to prevent up from moving cursor within new text
    // Set to nothing first for the same reason
    setTimeout(function () {
      setAttributeOn(historyAddin.target, "value", "");
      setAttributeOn(historyAddin.target, "value", setTo);
    }, 0);

    return false;
  }

  // "down" key, 57373 is opera, apparently
  if (keyCode == 40 || keyCode == 57373) {
    // three cases:
    // 1. We're at the bottom, so ignore
    // 2. We're right at the end and trying to come back, so
    //    put in the temp entry
    // 3. We're cursoring down so replace the value
    if (this.histPos == undefined) {
      return false; // eat it, but do nothing
    } else if (this.histPos == this.histList.length - 1) {
      this.histPos = undefined;
      setAttributeOn(this.target, "value", this.tempHist);
    } else {
      this.histPos++;
      setAttributeOn(this.target, "value", this.histList[this.histPos]);
    }
    return false;
  }

  // didn't eat the keycode, pass it on 
  return true;
}



Documentation generated by JSDoc on Tue May 3 17:16:26 2005