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