From 00cc9eb32a9387040d0175fcfd21cf9dcab6514f Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Thu, 10 Feb 2011 11:20:49 -0800
Subject: rewrite of JQuery lite implementation, which now better supports
 selected sets
---
 src/Angular.js           |  32 +---
 src/Compiler.js          |   5 +-
 src/jqLite.js            | 461 ++++++++++++++++++++++++++++++-----------------
 src/scenario/Scenario.js |   6 +
 4 files changed, 309 insertions(+), 195 deletions(-)
(limited to 'src')
diff --git a/src/Angular.js b/src/Angular.js
index 9eaeb093..99a4528d 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -268,24 +268,6 @@ function extensionMap(angular, name, transform) {
   });
 }
 
-function jqLiteWrap(element) {
-  // for some reasons the parentNode of an orphan looks like _null but its typeof is object.
-  if (element) {
-    if (isString(element)) {
-      var div = document.createElement('div');
-      // Read about the NoScope elements here:
-      // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx
-      div.innerHTML = '
 
' + element; // IE insanity to make NoScope elements work!
-      div.removeChild(div.firstChild); // remove the superfluous div
-      element = new JQLite(div.childNodes);
-    } else if (!(element instanceof JQLite)) {
-      element =  new JQLite(element);
-    }
-  }
-  return element;
-}
-
-
 /**
  * @workInProgress
  * @ngdoc function
@@ -422,7 +404,9 @@ function isBoolean(value) { return typeof value == $boolean;}
 function isTextNode(node) { return nodeName_(node) == '#text'; }
 function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; }
 function isElement(node) {
-  return node && (node.nodeName || node instanceof JQLite || (jQuery && node instanceof jQuery));
+  return node &&
+    (node.nodeName  // we are a direct element
+    || (node.bind && node.find));  // we have a bind and find method part of jQuery API
 }
 
 /**
@@ -1057,11 +1041,11 @@ function bindJQuery(){
   // bind to jQuery if present;
   jQuery = window.jQuery;
   // reset to jQuery or default to us.
-  if (window.jQuery) {
-    jqLite = window.jQuery;
-    extend(jqLite.fn, {
-      scope: JQLite.prototype.scope,
-      cloneNode: cloneNode
+  if (jQuery) {
+    jqLite = jQuery;
+    extend(jQuery.fn, {
+      scope: JQLitePrototype.scope,
+      cloneNode: JQLitePrototype.cloneNode
     });
   } else {
     jqLite = jqLiteWrap;
diff --git a/src/Compiler.js b/src/Compiler.js
index 890f2510..77c83846 100644
--- a/src/Compiler.js
+++ b/src/Compiler.js
@@ -26,7 +26,6 @@ Template.prototype = {
     if (!queue) {
       inits[this.priority] = queue = [];
     }
-    element = jqLite(element);
     if (this.newScope) {
       childScope = createScope(scope);
       scope.$onEval(childScope.$eval);
@@ -45,7 +44,7 @@ Template.prototype = {
         paths = this.paths,
         length = paths.length;
     for (i = 0; i < length; i++) {
-      children[i].collectInits(childNodes[paths[i]], inits, childScope);
+      children[i].collectInits(jqLite(childNodes[paths[i]]), inits, childScope);
     }
   },
 
@@ -98,7 +97,7 @@ Compiler.prototype = {
       scope = scope || createScope();
       element = element === true
         ? templateElement.cloneNode()
-        : (jqLite(element) || templateElement);
+        : (element ? jqLite(element) : templateElement);
       element.data($$scope, scope);
       template.attach(element, scope);
       scope.$element = element;
diff --git a/src/jqLite.js b/src/jqLite.js
index 01a563b6..40994161 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -14,20 +14,6 @@ var jqCache = {},
 
 function jqNextId() { return (jqId++); }
 
-function jqClearData(element) {
-  var cacheId = element[jqName],
-      cache = jqCache[cacheId];
-  if (cache) {
-    forEach(cache.bind || {}, function(fn, type){
-      removeEventListenerFn(element, type, fn);
-    });
-    delete jqCache[cacheId];
-    if (msie)
-      element[jqName] = ''; // ie does not allow deletion of attributes on elements.
-    else
-      delete element[jqName];
-  }
-}
 
 function getStyle(element) {
   var current = {}, style = element[0].style, value, name, i;
@@ -46,56 +32,120 @@ function getStyle(element) {
   return current;
 }
 
-function JQLite(element) {
-  if (!isElement(element) && isDefined(element.length) && element.item && !isWindow(element)) {
-    for(var i=0; i < element.length; i++) {
-      this[i] = element[i];
+if (msie) {
+  extend(JQLite.prototype, {
+    text: function(value) {
+      var e = this[0];
+      // NodeType == 3 is text node
+      if (e.nodeType == 3) {
+        if (isDefined(value)) e.nodeValue = value;
+        return e.nodeValue;
+      } else {
+        if (isDefined(value)) e.innerText = value;
+        return e.innerText;
+      }
     }
-    this.length = element.length;
+  });
+}
+
+/////////////////////////////////////////////
+function jqLiteWrap(element) {
+  if (isString(element) && element.charAt(0) != '<') {
+    throw new Error('selectors not implemented');
+  }
+  return new JQLite(element);
+}
+
+function JQLite(element) {
+  if (element instanceof JQLite) {
+    return element;
+  } else if (isString(element)) {
+    var div = document.createElement('div');
+    // Read about the NoScope elements here:
+    // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx
+    div.innerHTML = ' 
' + element; // IE insanity to make NoScope elements work!
+    div.removeChild(div.firstChild); // remove the superfluous div
+    JQLiteAddNodes(this, div.childNodes);
+    this.remove(); // detach the elements form the temporary DOM div.
   } else {
-    this[0] = element;
-    this.length = 1;
+    JQLiteAddNodes(this, element);
   }
 }
 
-JQLite.prototype = {
-  data: function(key, value) {
-    var element = this[0],
-        cacheId = element[jqName],
-        cache = jqCache[cacheId || -1];
-    if (isDefined(value)) {
-      if (!cache) {
-        element[jqName] = cacheId = jqNextId();
-        cache = jqCache[cacheId] = {};
-      }
-      cache[key] = value;
-    } else {
-      return cache ? cache[key] : _null;
-    }
-  },
+function JQLiteCloneNode(element) {
+  return element.cloneNode(true);
+}
 
-  removeData: function(){
-    jqClearData(this[0]);
-  },
+function JQLiteDealoc(element){
+  JQLiteRemoveData(element);
+  for ( var i = 0, children = element.childNodes || []; i < children.length; i++) {
+    JQLiteDealoc(children[i]);
+  }
+}
 
-  dealoc: function(){
-    (function dealoc(element){
-      jqClearData(element);
-      for ( var i = 0, children = element.childNodes || []; i < children.length; i++) {
-        dealoc(children[i]);
-      }
-    })(this[0]);
-  },
+function JQLiteRemoveData(element) {
+  var cacheId = element[jqName],
+  cache = jqCache[cacheId];
+  if (cache) {
+    forEach(cache.bind || {}, function(fn, type){
+      removeEventListenerFn(element, type, fn);
+    });
+    delete jqCache[cacheId];
+    element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
+  }
+}
 
-  scope: function() {
-    var scope, element = this;
-    while (element && element.length && !(scope = element.data($$scope))) {
-      element = element.parent();
+function JQLiteData(element, key, value) {
+  var cacheId = element[jqName],
+      cache = jqCache[cacheId || -1];
+  if (isDefined(value)) {
+    if (!cache) {
+      element[jqName] = cacheId = jqNextId();
+      cache = jqCache[cacheId] = {};
     }
-    return scope;
-  },
+    cache[key] = value;
+  } else {
+    return cache ? cache[key] : _null;
+  }
+}
+
+function JQLiteHasClass(element, selector, _) {
+  // the argument '_' is important, since it makes the function have 3 arguments, which
+  // is neede for delegate function to realize the this is a getter.
+  var className = " " + selector + " ";
+  return ((" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1);
+}
+
+function JQLiteRemoveClass(element, selector) {
+  element.className = trim(
+      (" " + element.className + " ")
+      .replace(/[\n\t]/g, " ")
+      .replace(" " + selector + " ", "")
+  );
+}
 
+function JQLiteAddClass(element, selector ) {
+  if (!JQLiteHasClass(element, selector)) {
+    element.className = trim(element.className + ' ' + selector);
+  }
+}
+
+function JQLiteAddNodes(root, elements) {
+  if (elements) {
+    elements = (!elements.nodeName && isDefined(elements.length))
+      ? elements
+      : [ elements ];
+    for(var i=0; i < elements.length; i++) {
+      if (elements[i].nodeType != 11)
+        root.push(elements[i]);
+    }
+  }
+}
 
+//////////////////////////////////////////
+// Functions which are declared directly.
+//////////////////////////////////////////
+var JQLitePrototype = JQLite.prototype = extend([], {
   ready: function(fn) {
     var fired = false;
 
@@ -107,15 +157,137 @@ JQLite.prototype = {
 
     this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9
     // we can not use jqLite since we are not done loading and jQuery could be loaded later.
-    new JQLite(window).bind('load', trigger); // fallback to window.onload for others
+    jqLiteWrap(window).bind('load', trigger); // fallback to window.onload for others
+  },
+  toString: function(){
+    var value = [];
+    forEach(this, function(e){ value.push('' + e);});
+    return '[' + value.join(', ') + ']';
+  }
+});
+
+//////////////////////////////////////////
+// Functions iterating getter/setters.
+// these functions return self on setter and
+// value on get.
+//////////////////////////////////////////
+forEach({
+  data: JQLiteData,
+
+  scope: function(element) {
+    var scope;
+    while (element && !(scope = jqLite(element).data($$scope))) {
+      element = element.parentNode;
+    }
+    return scope;
   },
 
-  bind: function(type, fn){
-    var self = this,
-        element = self[0],
-        bind = self.data('bind'),
+  removeAttr: function(element,name) {
+    element.removeAttribute(name);
+  },
+
+  hasClass: JQLiteHasClass,
+
+  css: function(element, name, value) {
+    if (isDefined(value)) {
+      element.style[name] = value;
+    } else {
+      return element.style[name];
+    }
+  },
+
+  attr: function(element, name, value){
+    if (isDefined(value)) {
+      element.setAttribute(name, value);
+    } else if (element.getAttribute) {
+      // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
+      // some elements (e.g. Document) don't have get attribute, so return undefined
+      return element.getAttribute(name, 2);
+    }
+  },
+
+  text: extend(msie
+      ? function(element, value) {
+        // NodeType == 3 is text node
+        if (element.nodeType == 3) {
+          if (isUndefined(value))
+            return element.nodeValue;
+          element.nodeValue = value;
+        } else {
+          if (isUndefined(value))
+            return element.innerText;
+          element.innerText = value;
+        }
+      }
+      : function(element, value) {
+        if (isUndefined(value)) {
+          return element.textContent;
+        }
+        element.textContent = value;
+      }, {$dv:''}),
+
+  val: function(element, value) {
+    if (isUndefined(value)) {
+      return element.value;
+    }
+    element.value = value;
+  },
+
+  html: function(element, value) {
+    if (isUndefined(value)) {
+      return element.innerHTML;
+    }
+    for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
+      JQLiteDealoc(childNodes[i]);
+    }
+    element.innerHTML = value;
+  }
+}, function(fn, name){
+  /**
+   * Properties: writes return selection, reads return first value
+   */
+  JQLite.prototype[name] = function(arg1, arg2) {
+    if ((fn.length == 2 ? arg1 : arg2) === undefined) {
+      if (isObject(arg1)) {
+        // we are a write, but the object properties are the key/values
+        for(var i=0; i < this.length; i++) {
+          for ( var key in arg1) {
+            fn(this[i], key, arg1[key]);
+          }
+        }
+        // return self for chaining
+        return this;
+      } else {
+        // we are a read, so read the first child.
+        if (this.length)
+          return fn(this[0], arg1, arg2);
+      }
+    } else {
+      // we are a write, so apply to all children
+      for(var i=0; i < this.length; i++) {
+        fn(this[i], arg1, arg2);
+      }
+      // return self for chaining
+      return this;
+    }
+    return fn.$dv;
+  };
+});
+
+//////////////////////////////////////////
+// Functions iterating traversal.
+// These functions chain results into a single
+// selector.
+//////////////////////////////////////////
+forEach({
+  removeData: JQLiteRemoveData,
+
+  dealoc: JQLiteDealoc,
+
+  bind: function(element, type, fn){
+    var bind = JQLiteData(element, 'bind'),
         eventHandler;
-    if (!bind) this.data('bind', bind = {});
+    if (!bind) JQLiteData(element, 'bind', bind = {});
     forEach(type.split(' '), function(type){
       eventHandler = bind[type];
       if (!eventHandler) {
@@ -131,7 +303,7 @@ JQLite.prototype = {
             };
           }
           forEach(eventHandler.fns, function(fn){
-            fn.call(self, event);
+            fn.call(element, event);
           });
         };
         eventHandler.fns = [];
@@ -141,133 +313,86 @@ JQLite.prototype = {
     });
   },
 
-  replaceWith: function(replaceNode) {
-    this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]);
-  },
-
-  children: function() {
-    return new JQLite(this[0].childNodes);
-  },
-
-  append: function(node) {
-    var self = this[0];
-    node = jqLite(node);
-    forEach(node, function(child){
-      self.appendChild(child);
+  replaceWith: function(element, replaceNode) {
+    var index, parent = element.parentNode;
+    JQLiteDealoc(element);
+    forEach(new JQLite(replaceNode), function(node){
+      if (index) {
+        parent.insertBefore(node, index.nextSibling);
+      } else {
+        parent.replaceChild(node, element);
+      }
+      index = node;
     });
   },
 
-  remove: function() {
-    this.dealoc();
-    var parentNode = this[0].parentNode;
-    if (parentNode) parentNode.removeChild(this[0]);
-  },
-
-  removeAttr: function(name) {
-    this[0].removeAttribute(name);
-  },
-
-  after: function(element) {
-    this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling);
-  },
-
-  hasClass: function(selector) {
-    var className = " " + selector + " ";
-    if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) {
-      return true;
-    }
-    return false;
-  },
-
-  removeClass: function(selector) {
-    this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", ""));
+  children: function(element) {
+    var children = [];
+    forEach(element.childNodes, function(element){
+      if (element.nodeName != '#text')
+        children.push(element);
+    });
+    return children;
   },
 
-  toggleClass: function(selector, condition) {
-   var self = this;
-   (condition ? self.addClass : self.removeClass).call(self, selector);
+  append: function(element, node) {
+    forEach(new JQLite(node), function(child){
+      element.appendChild(child);
+    });
   },
 
-  addClass: function( selector ) {
-    if (!this.hasClass(selector)) {
-      this[0].className = trim(this[0].className + ' ' + selector);
-    }
+  remove: function(element) {
+    JQLiteDealoc(element);
+    var parent = element.parentNode;
+    if (parent) parent.removeChild(element);
   },
 
-  css: function(name, value) {
-    var style = this[0].style;
-    if (isString(name)) {
-      if (isDefined(value)) {
-        style[name] = value;
-      } else {
-        return style[name];
-      }
-    } else {
-      extend(style, name);
-    }
+  after: function(element, newElement) {
+    var index = element, parent = element.parentNode;
+    forEach(new JQLite(newElement), function(node){
+      parent.insertBefore(node, index.nextSibling);
+      index = node;
+    });
   },
 
-  attr: function(name, value){
-    var e = this[0];
-    if (isObject(name)) {
-      forEach(name, function(value, name){
-        e.setAttribute(name, value);
-      });
-    } else if (isDefined(value)) {
-      e.setAttribute(name, value);
-    } else {
-      // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
-      // some elements (e.g. Document) don't have get attribute, so return undefined
-      if (e.getAttribute) return e.getAttribute(name, 2);
-    }
-  },
+  addClass: JQLiteAddClass,
+  removeClass: JQLiteRemoveClass,
 
-  text: function(value) {
-    if (isDefined(value)) {
-      this[0].textContent = value;
+  toggleClass: function(element, selector, condition) {
+    if (isUndefined(condition)) {
+      condition = !JQLiteHasClass(element, selector);
     }
-    return this[0].textContent;
+    (condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector);
   },
 
-  val: function(value) {
-    if (isDefined(value)) {
-      this[0].value = value;
-    }
-    return this[0].value;
+  parent: function(element) {
+    // in IE it returns undefined, but we need differentiate it from functions which have no return
+    return element.parentNode || null;
   },
 
-  html: function(value) {
-    if (isDefined(value)) {
-      var i = 0, childNodes = this[0].childNodes;
-      for ( ; i < childNodes.length; i++) {
-        jqLite(childNodes[i]).dealoc();
-      }
-      this[0].innerHTML = value;
-    }
-    return this[0].innerHTML;
+  find: function(element, selector) {
+    return element.getElementsByTagName(selector);
   },
 
-  parent: function() {
-    return jqLite(this[0].parentNode);
-  },
-
-  clone: cloneNode,
-  cloneNode: cloneNode
-};
-function cloneNode() { return jqLite(this[0].cloneNode(true)); }
-
-if (msie) {
-  extend(JQLite.prototype, {
-    text: function(value) {
-      var e = this[0];
-      // NodeType == 3 is text node
-      if (e.nodeType == 3) {
-        if (isDefined(value)) e.nodeValue = value;
-        return e.nodeValue;
+  clone: JQLiteCloneNode,
+  cloneNode: JQLiteCloneNode
+}, function(fn, name){
+  /**
+   * chaining functions
+   */
+  JQLite.prototype[name] = function(arg1, arg2) {
+    var value;
+    for(var i=0; i < this.length; i++) {
+      if (value == undefined) {
+        value = fn(this[i], arg1, arg2);
+        if (value !== undefined) {
+          // any function which returns a value needs to be wrapped
+          value = jqLite(value);
+        }
       } else {
-        if (isDefined(value)) e.innerText = value;
-        return e.innerText;
+        JQLiteAddNodes(value, fn(this[i], arg1, arg2));
       }
     }
-  });
-}
+    return value == undefined ? this : value;
+  };
+});
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index d1f1eb33..81c85a61 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -256,6 +256,12 @@ function browserTrigger(element, type) {
         element.checked = !element.checked;
         break;
     }
+    // WTF!!! Error: Unspecified error.
+    // Don't know why, but some elements when detached seem to be in inconsistent state and
+    // calling .fireEvent() on them will result in very unhelpful error (Error: Unspecified error)
+    // forcing the browser to compute the element position (by reading its CSS)
+    // puts the element in consistent state.
+    element.style.posLeft;
     element.fireEvent('on' + type);
     if (lowercase(element.type) == 'submit') {
       while(element) {
-- 
cgit v1.2.3