// "xbl.js" is a clone of the XBL framework for Mozilla, only 
// done up in Javascript. It is intended to match, to the extent
// possible, the XBL framework feature for feature, but not
// neceassarily "way of doing things" for "way of doing things", or
// bug for bug; when JS provides a better way we take it.

// This file is just the framework, suitable for distribution alone.

// Since I'm just putting this up for Slashdot, no warranties,
// etc. If you want to use it, use it under the terms of the Lesser
// GNU Public License, though I'd recommend contacting me
// (jerf@jerf.org) to see if there is a more up to date version as
// this is still in development.

// A comment in a JS file is not the place to describe XBL, so this
// will just document the correspondances:

// <binding>: A JS class, derived from Widget
// <content>:  This is the clumsiest part of the conversion
//   The content is represented as a list of text and objects,
//   which may be nested. objects will be turned into tags,
//   and strings will be added as text nodes. The objects
//   have the following special attributes (note you can only
//   have one of "tagName" or "widgetName"): 
//     "tagName": will be the type of the created tag, the other
//        elements not mentioned here pass through to the tag.
//        Created tags will have an attribute "widget" added to
//        the root tag of the widget containing a reference to
//        the Widget object.
//     "widgetType": A reference to the widget type function.
//        The rest of the attributes of the object not mentioned
//        here will be passed through. Widgets will recieve
//        a reference to the root widget (NOT their parent) as
//        the attribute ".rootWidget"; the root widget will
//        also have this, pointing to itself of course. This should
//        not be used for inherited DOM attributes, but for things
//        passed to the root widget as data relevant to your
//        application. (For instance, if you have a widget that is
//        binding to some piece of data and it has subwidgets, the 
//        subwidgets can learn about their binding from .rootWidget.)
//     "name": Along with being the HTML name, will be added
//        as an attribute of the JS object on construction, 
//        i.e., "name": "a" will have its DOM node available as
//        this.a
//     "children": A list of further children. Tags only for the 
//        moment though this will be fixed.
//     "appendTarget": A string that gives a name for a child
//        append target. The special name "default" indicates
//        the default target. The name is used as the second param
//        of the "appendChild" method on widgets, and if not
//        given, will go to the "default" append target.
//        If no append targets are given, appendChild will not
//        be available. 
//     "inherits": A comma-separated list of attributes to inherit. 
//        If there is a pipe in the name, what follows the pipe is
//        the default value which will be set unless a value is
//        specified. Example: "inherits": "cols|40" will set the
//        cols to 40 unless overridden. Note "inherit" runs through
//        the "setAttribute" protocol.
//   Example: The XML fragment
//       <body bgcolor="#FFFFFF"><p>hello</p></body>
//     converts into
//       {tagName: "body", bgcolor: "#FFFFFF",
//        children: [
//         {tagName: "p", children: ["hello"]}
//         ]}
//   Uglier, I know. Also note that there needs to be a top-level
//   DOM node.
//   This should be the .content member of the prototype.
// <implementation>: methods of course map directly to the JS object
//   methods. 
// <constructor>: Since JS makes inheriting constructors a pain,
//   and AFAIK there is no automatic way to do it, your constructor
//   must call the parent constructor manually. Sorry, I don't
//   like it either.
//   But we do get a win: I've seperated out the DOM construction 
//   part of the constructor from the data initialization. If you
//   don't understand why this is a win, it's because you never tried
//   to use DOM to embed new XBL objects in an XBL <constructor> 
//   call only to discover you can't access the XBL properties.
//   If that sounds like goobly-gook to you, well, trust me,
//   it's several hours of debugging and workaround time avoided.
//   .initDOM(atts) should initialize the dynamic DOM nodes, and
//   .initWidget(atts) should initialize the data. If you hold off
//   on calling the superclass .initDOM, you can manipulate the
//   .content before it is rendered.
//   Also, see deriveNewWidget definition below for the best way
//   to create a new Widget.
// <property>s: While properties map directly to JavaScript 
//   properties I'd generally discourage their use as at the
//   very least IE 6 does not support them. In fact it does not
//   support them so very thoroughly that it is a syntax error
//   in IE. But if you really want them, they can be written.
//   Instead, I suggest using .get("att") or .set("att", "value")
//   for properties, which can be easily overridden with methods
//   named "set_att" or "get_att" on the prototype, so it isn't
//   *too* big a loss. ".set" and ".get" are also synonyms for 
//   ".setAttribute" and ".getAttribute", so widgets are duck-typed 
//   as DOM nodes that way.
// <handlers>: Methods starting with "on" will be mapped to event
//   handlers on the top node. Event handlers followed by an 
//   underscore, then a name, will be attached to the node with
//   that name. (Example: "onkeypress_input" will map to the 
//   "keypress" event on the "input" DOM node.)

// Additional special methods or capabilities:
// The framework provides a coherency-maintenence service in the form
// of "vars", which are matched to the DOM state automatically. Vars
// are created via the "declareVar(varName, DOMnodeName, varType)" method,
// and accessed via ".getAttribute" and ".setAttribute" notation. The
// "varType" is the type of the correspondance; several common types
// are provided. When the user manipulates the DOM nodes, the value of
// the var is automatically updated, and when you .set the var, the
// dom  node is automatically updated appropriately.
//
// Widgets use "setAttribute" & "getAttribute", just like DOM
// nodes. setAttribute and getAttribute look for the following
// things, in the following order:
// * A function name "get_[name]" or "set_[name]", which will
//   be called.
// * A variable by that name, which will call the update for that var.
// * Attributes on "this".
// See the Label widget for an example of a useful HTML widget that
// creates a label that can be updated via ".setAttribute('value',
// 'a')".
// Also, since I find this rather verbose, .get() and .set() are
// synonyms for getAttribute and setAttribute respectively.
//
// Hints:
// * If your <constructor> assigned constant attributes like
//   "this.inited = 0", you can put them in the prototype of the
//   class instead; I found some of my XBL constructors just went away.
// * See widgetsBasic.js for some additional widgets that may
//   help you move away from XBL.

// This function takes a DOM node or a Widget, and returns the DOM
// node; an adapter.

function DOM(node) {
  if (node instanceof Widget) {
    return node.domNode;
  } 
  return node;
}

// A superclass for coherent variables.
function Var() {
}

Var.prototype.get = function () {};
Var.prototype.set = function () {};

// The most common kind of var: Tied to the value of a input widget
// So common, it is considered the default argument to "declareVar"
function ValueVar (widget, nodeName) {
  this.input = DOM(widget[nodeName]);
}

ValueVar.prototype = new Var();

ValueVar.prototype.get = function () {
  return this.input.value;
}

ValueVar.prototype.set = function (value) {
  this.input.value = value;
}

// This manages the inherits list, managing propogating attributes 
// and registering the inherits
function AttributeInheritanceManager () {
}

// Pass in the thing to be inherited and the node inheriting it,
// and this will store the attachment. "as" is the actual attribute 
// of the node that will be changed, defaulting to the given att.
// Think of it "attribute LENGTH on the XBL-like widget will be
// inherited 'as' 'size'" on the component widget
AttributeInheritanceManager.prototype.register = function (att, node, as) {
  if (!(att in this)) {
    this[att] = [];
  }
  if (as == undefined) as = att;
  this[att].push([node, as]);
}

// Propogates the attribute setting on the widget to the registered
// nodes
AttributeInheritanceManager.prototype.propogate = function (att, value) {
  if (att in this) {
    for (var idx in this[att]) {
      var node = this[att][idx][0];
      var attributeToSet = this[att][idx][1];
      node.setAttribute(attributeToSet, value);
    }
  }
}

// This gives the widgets an ID so the actual event handlers created
// for the browser can map back to the Widget handlers correctly
maxId = 0;
Widgets = {};

function Widget(atts, prototypeOnly) {
  if (!prototypeOnly) {
    this.initDOM(atts);
    this.initWidget(atts);
  }
}

// An "empty" tag, suitable for placeholding
Widget.prototype.content = {tagName: "span"}

// This initializes the data storage and DOM nodes of the widget. 
// This is generally only overridden if you want to affect the DOM
// before setting values and stuff, which is unusual.
Widget.prototype.initDOM = function (atts) {
  // Give ourselves an id, so we can be found by number
  this.widgetId = maxId++;
  Widgets[this.widgetId] = this;

  // Store the insertion points
  this.appendTargets = {};

  // store which attributes are being "inherited", so we can manage
  // that
  this.inherits = new AttributeInheritanceManager;

  // place to store the event chain
  this.events = {}

  // Create the content
  var content = this.content;
  this.domNode = DOM(this.createObject(content));

  // register methods starting with "on" automatically as event
  // handlers
  for (var propName in this) {
    if (propName.substr(0, 2) == "on") {
      if (propName.indexOf("_") == -1) {
        
        this.registerEventHandler(propName.substr(2),
                                  false, propName);
      } else {
        var eventType = propName.substr(2, propName.indexOf("_")-2);
        var target = propName.substr(propName.indexOf("_") + 1);
        this.registerEventHandler(eventType, target, propName);
      }
    }
  }

  // place to store vars; consider this private, except you may
  // (varname in this.vars) acceptably, as long as you still use
  // .get(varname) and .set(varname, value) normally
  this.vars = {}

  // declare the vars in this.variables
  for (var idx in this.variables) {
    var decl = this.variables[idx];
    this.declareVar.apply(this, decl);
  }
}

// This runs through an event handler chain, and if any of them
// return false, it stops and returns false. Otherwise, this
// returns true.
Widget.prototype.processEvent = function (eventType, target, event) {
  var handlers = this.events[eventType];
  if (handlers == undefined) {
    // how'd this happen? Oh well...
    return true;
  }

  for (var idx in handlers) {
    var handler = handlers[idx];
    var method = handler[0];
    var eventTarget = handler[1];
    var context = handler[2];
    if (target == eventTarget) {
      var thisContext = context == undefined ? this : Widgets[context];
      var result;
      result = thisContext[method].call(thisContext, event);
      if (result == false) {
        return false;
      }
    }
  }

  return true;
}

// This registers an event handler by name, all these things are
// strings. "Context" is the id of the widget you'd like this event
// to run in the context of (i.e., foreign widgets can ask to recieve 
// events from this widget)
Widget.prototype.registerEventHandler = function (eventType, target,
                                                  handleMethod, context) {
  var origTarget = target;
  if (target) {
    target = this[target];
  } else {
    target = this;
  }

  // If the target is a widget itself, we need to tap into that
   // widget's event handling even though we want this context
   if (this != target && target instanceof Widget) {
    if (context == undefined) context = this.widgetId;
    return target.registerEventHandler(eventType, false, handleMethod,
                                       context);
  }

  var escOrigTarget = origTarget;
  if (escOrigTarget != false) {
    escOrigTarget = "'" + escOrigTarget + "'";
  }

  // "return Widget[0].processEvent('keypress', 3, event)"
  var widget = "Widgets[" + this.widgetId + "]";
  var handler = ("return " + widget + ".processEvent"
                 + "('" + eventType + "', " + escOrigTarget + ", event);");
  DOM(target).setAttribute("on" + eventType, handler);

  if (this.events[eventType] == undefined) {
    this.events[eventType] = [];
  }
  this.events[eventType].unshift([handleMethod, origTarget, context]);
}

// This initializes the widget by actually calling setAttribute(name,
// val) on the provided attributes.
Widget.prototype.initWidget = function (atts) {
  // copy over the provided attributes, if any
  if (atts) {
    for (var name in atts) {
      this.setAttribute(name, atts[name]);
    }
  }
}


// Recursively creates DOM nodes as described in the documentation.
// child is true on recursive calls and is used for determining which
// DOM node gets the .widget attribute.
Widget.prototype.createObject = function (objDef, child) {
  var object;
  // constants but I don't want them in the global namespace.
  var ELEMENT = 1;
  var WIDGET = 2;
  var objType;
  if (objDef.tagName) {
    object = document.createElement(objDef.tagName);
    for (var attName in objDef) {
      if ((attName) == "children") continue;
      if ((attName) == "tagName") continue;
      if ((attName) == "inherits") continue;

      object.setAttribute(attName, objDef[attName]);
    }
    if (!child) {
      object.widget = this;
    }
    objType = ELEMENT;
  } else if (objDef.widgetType) {
    // set up the atts
    var atts = {};
    for (var attName in objDef) {
      if (attName == "widgetType") continue;
      if (attName == "inherits") continue;
      if (attName == "name") continue;
      atts[attName] = objDef[attName];
    }
    object = new objDef.widgetType(atts);
    object.rootWidget = this;
    objType = WIDGET;
  } else {
    alert("Illegal object def.");
    return undef;
  }

  if (objDef.appendTarget) {
    this.appendTargets[objDef.appendTarget] = object;
  }

  if (objDef.name) {
    this[objDef.name] = object;
  }

  if (objDef.inherits) {
    // FIXME: Redirected inheritances
    var inheritances = objDef.inherits.split(/\s*,\s*/);
    for (var idx in inheritances) {
      var field = inheritances[idx];
      // split off default and set it (will be overridden later
      // if it was set)
      if (field.indexOf("|") != -1) {
        var def = field.substr(field.indexOf("|") + 1);
        field = field.substr(0, field.indexOf("|"));
        object.setAttribute(field, def);
      }
      this.inherits.register(inheritances[idx], object);
    }
  }

  // create the children, if any
  if (objType == ELEMENT && objDef.children) {
    var children = objDef.children;
    for (var idx in children) {
      var child = children[idx];
      if (typeof child == "string") {
        var node = document.createTextNode(child);
        object.appendChild(node);
      } else {
        var node = DOM(this.createObject(child, 1));
        object.appendChild(node);
      }
    }
  }

  return object;
}

Widget.prototype.declareVar = function (varName, varType,
                                        value, extra) { 
  if (varType == undefined) varType = ValueVar;
  this.vars[varName] = new varType(this, extra);
  if (value) {
    this.setAttribute(varName, value);
  }
}

// Appends this widget to some DOM node
Widget.prototype.appendTo = function (parent) {
  DOM(parent).appendChild(DOM(this));
}

// Appends a DOM node, text fragment, or widget to the specified
// insertion point
Widget.prototype.appendChild = function (element, target) {
  if (target == undefined) {
    target = "default";
  }

  var appendTarget = this.appendTargets[target];
  if (!appendTarget) {
    throw "Append target '" + target + "' does not exist.";
  }

  if (typeof element == "string") {
    appendTarget.appendChild(textNode(element));
  } else {
    appendTarget.appendChild(DOM(element));
  }
}

// Support for .setAttribute and .getAttribute.
Widget.prototype.getAttribute = function (name) {
  if (typeof(this["get_" + name]) == "function") {
    return this["get_" + name]();
  }
  if (this.vars[name]) {
    return this.vars[name].get();
  }
  return this[name];
}

Widget.prototype.get = function () {
  return this.getAttribute.apply(this, arguments);
}

Widget.prototype.setAttribute = function (name, value) {
  if (typeof(this["set_" + name]) == "function") {
    this["set_" + name](value);
  } else if (this.vars[name]) {
    this.vars[name].set(value);
  } else {
    this[name] = value;
  }
  this.inherits.propogate(name, value);
}

Widget.prototype.set = function () {
  return this.setAttribute.apply(this, arguments);
}

// Creating a widget correctly is a pain and I'm so used to dynamic
// languages like Perl and Python where OnceAndOnlyOnce isn't a mere
// pipe dream that this ugly Magic Invocation to derive a widget
// is pissing me off. This at least simplifies the Magic Invocation
// from everything you see below to "eval(deriveNewWidget(newName,
// base, extraInit))"; this also contains this boilerplate as
// it has changed a couple of times.

function deriveNewWidget (newName, baseWidget, extraInit) {
  if (!extraInit) extraInit = "";
  return "function " + newName + " (atts, prototypeOnly) { \
  if (!prototypeOnly) { \
    this.initDOM(atts); this.initWidget(atts); " + extraInit + "\
  } \
} \
" + newName + ".prototype = new " + baseWidget + "(0, 1); \
";
}

// convenience for creating nodes
function create(tagName, attDict, childText) {
  element = document.createElement(tagName);
  for (var key in attDict) {
    element.setAttribute(key, attDict[key]);
  }
  if (childText) {
    element.appendChild(textNode(childText));
  }
  return element;
}

function textNode(text) {
  return document.createTextNode(text);
}

// copying an object is sometimes used in prototypes for deriving
// from other .contents; this is a one-level deep copy
function objCopy(obj) {
  var newObj = {};
  for (var att in obj) {
    newObj[att] = obj[att];
  }
  return newObj;
}
