﻿/*
* jQuery live links plugin
* http://github.com/nje
*/
(function ($) {
   
   var oldattr = $.attr,
    oldval = $.fn.val,
    olddata = $.data,
    oldcleandata = $.cleanData,
    linkId = 0,
    liveLinks = {};

   function attr(obj, name, value, pass) {
      // an attr that supports plain objects (won't look for .style, nodeType, etc)
      return $.isPlainObject(obj)
        ? (value === undefined ? obj[name] : (obj[name] = value))
        : oldattr(obj, name, value, pass);
   }

   function raiseEvents(type, context, change, setValue) {
      // todo: peek if there are any listeners to avoid extra work
      var ret,
        event = $.Event(type + "Changing"),
        isArray = type === "array";
      event.newValue = isArray ? change.arguments : change.newValue;
      $.event.trigger(event, [change], context);
      if (!event.isDefaultPrevented()) {
         var newvalue = isArray ? change.arguments : event.newValue;
         ret = setValue(newvalue);
         var oldvalue = change.oldValue;
         if (isArray || typeof oldvalue === "undefined" || newvalue !== oldvalue) {
            isArray ? (change.arguments = newvalue) : (change.newValue = newvalue);
            $.event.trigger(type + "Change", [change], context);
         }
      }
      return ret;
   }

   $.attr = function (elem, name, value, pass) {
      var ret = this;
      if (value === undefined)
         ret = attr(elem, name, value, pass);
      else {
         raiseEvents("attr", elem, { attrName: name, oldValue: attr(elem, name), newValue: value },
            function (newvalue) {
               attr(elem, name, value, pass);
            });
      }
      return ret;
   }

   $.fn.val = function (value) {
      var ret = this;
      if (value === undefined)
         ret = oldval.call(this);
      else {
         this.each(function () {
            var self = $(this);
            raiseEvents("attr", this, { attrName: "val", oldValue: oldval.call(self), newValue: value },
                function (newvalue) {
                   oldval.call(self, newvalue);
                });
         });
      }
      return ret;
   }

   $.data = function (elem, name, data) {
      var ret;
      if (typeof data === "undefined") {
         ret = olddata(elem, name);
      }
      else {
         var oldvalue,
            newvalue = data,
            attrName;
         if (typeof name === "object") {
            attrName = "data:!";
            newvalue = name;
         }
         else {
            attrName = "data:" + name;
            oldvalue = olddata(elem, name);
         }
         ret = raiseEvents("attr", elem, { attrName: attrName, oldValue: oldvalue, newValue: newvalue },
            function (newvalue) {
               return olddata(elem, name, newvalue);
            });
      }
      return ret;
   }

   $.cleanData = function (elems) {
      for (var i = 0, elem; (elem = elems[i]) != null; i++) {
         if ($.data(elem, "links")) {
            // remove any links that target this element
            // as the source may not be being cleaned up
            $.unlink({ target: elem });
         }
      }
      oldcleandata(elems);
   }

   $.each("pop push reverse shift sort splice unshift".split(" "), function (i, name) {
      $[name] = function (arr) {
         var args = $.makeArray(arguments);
         args.splice(0, 1);
         return raiseEvents("array", arr, { change: name, arguments: args }, function (arguments) {
            arr[name].apply(arr, arguments);
         });
      }
   });

   var special = $.event.special,
    formElems = /^(textarea|input|select)$/i;

   $.each("attrChanging attrChange arrayChanging arrayChange".split(' '), function (i, name) {
      var isattr = i < 2;

      $.fn[name] = function (filter, fn) {
         if (arguments.length === 1) {
            fn = filter;
            filter = null;
         }
         return fn ? this.bind(name, filter, fn) : this.trigger(name);
      }

      special[name] = {
         add: function (handleObj) {
            var old_handler = handleObj.handler;
            handleObj.handler = function (event, change) {
               var data = handleObj.data,
                    attrName = change ? (isattr ? change.attrName : change.change) : null;
               if (!change || !data || data === attrName || $.inArray(attrName, data) > -1) {
                  $.extend(event, change);
                  // todo: support extra parameters passed to trigger as
                  // trigger('attrChange', [<change>, extra1, extra2]).
                  old_handler.call(this, event);
               }
            }
         }
      }
   });

   special.attrChange.teardown = function () { };

   $(document).bind("change.attrChange", function (ev) {
      var self = $(ev.target);
      self.trigger("attrChange", { attrName: "val", newValue: self.val() });
   });

   var setter_lookup = {
      val: "val",
      html: "html",
      text: "text"
   }

   function processFromTo(settings, context, fn) {
      var from = settings.from,
        to = settings.to,
        sources = from.sources || from.targets,
        sourceAttrs = from.attr,
        converts = from.convert,
        targets = to.targets || to.sources,
        targetAttrs = to.attr,
        updates = to.update;
      sources = $.isArray(sources) ? sources : [sources];
      targets = $.isArray(targets) ? targets : [targets];
      updates = typeof updates === "undefined" ? null : ($.isArray(updates) ? updates : [updates]),
    sourceAttrs = typeof sourceAttrs === "undefined" ? null : ($.isArray(sourceAttrs) ? sourceAttrs : [sourceAttrs]),
    targetAttrs = typeof targetAttrs === "undefined" ? null : ($.isArray(targetAttrs) ? targetAttrs : [targetAttrs]),
    converts = $.isArray(converts) ? converts : [converts];
      $.each(sources, function (i, source) {
         var target = targets[Math.min(targets.length - 1, i)],
            sourceAttr = sourceAttrs ? sourceAttrs[Math.min(sourceAttrs.length - 1, i)] : null,
            targetAttr = targetAttrs ? targetAttrs[Math.min(targetAttrs.length - 1, i)] : null,
            convert = converts[Math.min(converts.length - 1, i)],
            update = updates ? updates[Math.min(updates.length - 1, i)] : null;
         fn({
            source: source,
            target: target,
            sourceAttr: sourceAttr,
            targetAttr: targetAttr,
            convert: convert,
            update: update
         }, context);
      });
      if (settings.twoWay) {
         var update = to.update;
         delete to.update;
         fn({ from: to, to: from }, context);
         to.update = update;
      }
   }

   function getValue(elem, attr) {
      var value = (attr && attr.indexOf("data:") === 0)
        ? elem.data(attr.substr(5))
        : (attr === "val" ? elem.val() : elem.attr(attr));
      return value;
   }

   function setValue(sourceElem, targetElem, attr, value) {
      var defaultAttr = sourceElem.attr("itemprop");
      if (!defaultAttr) {
         var e = sourceElem.get(0);
         defaultAttr = e.name || e.id;
      }
      targetElem.each(function () {
         var a = attr;
         if (!attr) {
            if (this.nodeType) {
               // val for inputs, text for everything else
               var nodeName = this.nodeName;
               a = formElems.test(nodeName) ? "val" : "text";
            }
            else {
               a = defaultAttr;
            }
         }
         var fn = setter_lookup[a];
         if (fn) {
            $(this)[fn](value);
         }
         else if (a.indexOf("data:") === 0) {
            $.data(this, a.substr(5), value);
         }
         else {
            $(this).attr(a, value);
         }
      });
   }

   $.linkLive = function (settings, context) {
      if (settings.from && settings.to) {
         processFromTo(settings, context, $.linkLive);
         return;
      }
      var source = settings.source,
        target = settings.target,
        sourceAttr = settings.sourceAttr || "",
        targetAttr = settings.targetAttr || "",
        convert = settings.convert;

      // wrap arrays in another array because $([]) is treatment of
      // the contents, not the array itself
      source = $.isArray(source) ? [source] : source;
      target = $.isArray(target) ? [target] : target;

      var handler = function (ev, forceSource) {
         var source = ev ? ev.target : forceSource;
         var attr = sourceAttr;
         if (!attr) {
            // val for inputs, text for everything else
            if (source.nodeType) {
               var nodeName = source.nodeName;
               attr = formElems.test(nodeName) ? "val" : "text";
            }
         }
         var _source = $(source),
            newValue = ev ? ev.newValue : getValue(_source, attr),
            cv = convert ? ($.convertFn[convert] || convert) : null;
         if (cv) {
            newValue = cv(newValue, settings);
         }
         if (typeof newValue !== "undefined") {
            setValue(_source, $(target, context), targetAttr, newValue);
         }
      }

      $(source, context).live("attrChange", handler);

      var list = liveLinks[source] || (liveLinks[source] = []);
      list.push({
         source: source,
         sourceAttr: sourceAttr,
         target: target,
         targetAttr: targetAttr,
         context: context,
         handler: handler
      });

      if (settings.update) {
         $(source).each(function () {
            handler(null, this);
         });

      }
   }

   $.unlinkLive = function (settings, context) {
      if (settings.from && settings.to) {
         processFromTo(settings, context, $.unlinkLive);
         return;
      }
      var source = settings.source,
        target = settings.target,
        sourceAttr = settings.sourceAttr || "",
        targetAttr = settings.targetAttr || "";
      var links = liveLinks[source];
      if (links) {
         $.each(links, function (i, link) {
            if (link.context === context && link.sourceAttr === sourceAttr && link.target === target && link.targetAttr === targetAttr) {
               $(link.source, context).die("attrChange", link.handler);
               delete links[link.source];
               link.source = null;
               link.target = null;
               link.handler = null;
               link.context = null;
               return false;
            }
         });
      }
   }

   $.link = function (settings, context) {
      if (settings.from && settings.to) {
         processFromTo(settings, context, $.link);
         return;
      }
      var source = settings.source,
        target = settings.target,
        sourceAttr = settings.sourceAttr || "",
        targetAttr = settings.targetAttr || "",
        convert = settings.convert;

      // wrap arrays in another array because $([]) is treatment of
      // the contents, not the array itself
      source = $($.isArray(source) ? [source] : source, context);
      target = $($.isArray(target) ? [target] : target, context);

      target.each(function (i, target) {
         var _target = $(target);
         source.each(function (i, source) {
            var attr = sourceAttr;
            if (!attr) {
               // val for inputs, text for everything else
               if (this.nodeType) {
                  var nodeName = this.nodeName;
                  attr = formElems.test(nodeName) ? "val" : "text";
               }
            }
            var handler = function (ev) {
               if (!ev || source === ev.target) {
                  var _source = $(ev ? ev.target : source),
                        newValue = ev ? ev.newValue : getValue(_source, attr),
                  // re-evaluate convert each occurrance to pick up dynamic changes to convertFn
                        cv = convert ? ($.convertFn[convert] || convert) : null;
                  if (cv) {
                     newValue = cv(newValue, settings);
                  }
                  if (typeof newValue !== "undefined") {
                     setValue(_source, _target, targetAttr, newValue);
                  }
               }
            };
            var id = linkId++,
                link = {
                   source: source,
                   sourceAttr: sourceAttr,
                   target: target,
                   targetAttr: targetAttr,
                   handler: handler,
                   id: id
                };
            // register this link with the target
            var data = $.data(target),
                links = data.links || (data.links = { targets: {}, sources: {} }),
                index = links.targets;
            index[id] = link;
            // register this link with the source
            data = $.data(source);
            links = data.links || (data.links = { targets: {}, sources: {} });
            index = links.sources;
            index[id] = link;
            // listen to changes on the source
            $(source).attrChange(attr ? attr.split(' ') : '', handler);
            // update if specified
            if (settings.update) {
               handler();
            }
         });
      });
   }

   /*
   Links are individually remembered via data() in order to facilitate
   removing them individually. For example, you might all at once link
   a single source to several targets, then remove the linking to one of
   the targets.

   Each link object looks like this:

   link = { source: source, sourceAttr: sourceAttr,
   target: target, targetAttr: targetAttr,
   handler: handler }

   And they are indexed per element like this:
         
   elem.data.links = {
   // index of links that have this element as the source
   sources: {
   linkId1: link1,
   linkId2: link2,
   ...
   },
   // index of links that have this element as the target
   targets: {
   linkId1: link1,
   linkId2: link2,
   ...
   }
   }

   The links are stored by a unique link id rather than array
   in order to make removing them upon unlinking faster.

   'Live' links are not associated with any dom elements for obvious reasons.
   A private dictionary is kept for them, indexed by the source selector.
   var liveLinks = {
   source1: [link1, link2, ...],
   source2: [link1, link2, ...],
   ...
   }
   */

   function getLinksFor(obj, attr, isSource) {
      var links = $.data(obj, "links"),
        index = links ? (links[isSource ? "sources" : "targets"]) : null;
      if (!index) {
         return {};
      }
      else if (attr) {
         var matched = {};
         $.each(index, function (linkId, link) {
            if (link[isSource ? "sourceAttr" : "targetAttr"] === attr) {
               matched[linkId] = link;
            }
         });
         index = matched;
      }
      return index;
   }
   function filterBy(links, obj, objAttr, isSource) {
      var matched = {},
        objField = isSource ? "source" : "target",
        objAttrField = isSource ? "sourceAttr" : "targetAttr";
      $.each(links, function (linkId, link) {
         if (link[objField] === obj && (!objAttr || link[objAttrField] === objAttr)) {
            matched[linkId] = link;
         }
      });
      return matched;
   }

   function getLinks(source, sourceAttr, target, targetAttr) {
      var matched;
      if (source) {
         matched = getLinksFor(source, sourceAttr, true);
         if (target) {
            // filter by target
            matched = filterBy(matched, target, targetAttr);
         }
      }
      else if (target) {
         matched = getLinksFor(target, targetAttr);
         if (source) {
            // filter by source
            matched = filterBy(matched, source, sourceAttr, true);
         }
      }
      else {
         matched = {};
      }
      return matched;
   }

   function unlink(link) {
      // unbind handler
      $(link.source).unbind("attrChange", link.handler);
      // remove link from sources and targets list in each side's data cache
      var links = $.data(link.source, "links");
      delete links.sources[link.id];
      links = $.data(link.target, "links");
      delete links.targets[link.id];
      // remove references to help ensure no circular references
      link.source = null;
      link.target = null;
      link.handler = null;
   }

   $.unlink = function (settings, context) {
      if (settings.from && settings.to) {
         processFromTo(settings, context, $.unlink);
         return;
      }
      var source = settings.source,
        target = settings.target,
        sourceAttr = settings.sourceAttr || "",
        targetAttr = settings.targetAttr || "";
      source = source ? $($.isArray(source) ? [source] : source, context) : null;
      target = target ? $($.isArray(target) ? [target] : target, context) : null;
      function remove(source, target) {
         var links = getLinks(source, sourceAttr, target, targetAttr);
         $.each(links, function (linkId, link) {
            unlink(link);
         });
      }
      if (source && target) {
         source.each(function (i, source) {
            target.each(function (i, target) {
               remove(source, target);
            });
         });
      }
      else if (source) {
         source.each(function (i, source) {
            remove(source);
         });
      }
      else if (target) {
         target.each(function (i, target) {
            remove(null, target);
         });
      }
   }

   $.convertFn = {
      "!": function (value) {
         return !value;
      }
   };

})(jQuery);
