diff --git a/pom.xml b/pom.xml index aeeea7b..b2c3247 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,17 @@ false + + org.apache.maven.plugins + maven-source-plugin + + + + jar + + + + diff --git a/src/main/java/pl/labno/bernard/htmlunified/DomChangeLogger.java b/src/main/java/pl/labno/bernard/htmlunified/DomChangeLogger.java index b809ca9..ff18684 100644 --- a/src/main/java/pl/labno/bernard/htmlunified/DomChangeLogger.java +++ b/src/main/java/pl/labno/bernard/htmlunified/DomChangeLogger.java @@ -7,9 +7,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Node; +import java.util.HashSet; +import java.util.Set; + public class DomChangeLogger implements DomChangeListener { private static final Log LOG = LogFactory.getLog(DomChangeLogger.class); + private Set detailIds = new HashSet(); public void nodeAdded(DomChangeEvent event) { logEvent(event, "added"); @@ -19,6 +23,14 @@ public class DomChangeLogger implements DomChangeListener { logEvent(event, "removed"); } + public void showDetails(String id) { + detailIds.add(id); + } + + public void hideDetails(String id) { + detailIds.remove(id); + } + private void logEvent(DomChangeEvent event, String action) { String changed; final DomNode changedNode = event.getChangedNode(); @@ -36,7 +48,7 @@ public class DomChangeLogger implements DomChangeListener { } LOG.info("DomChangeEvent[action=" + action + ";changedElement=" + changed + ";source=" + event.getSource() + ";parent=" + parent + "]"); final Node id = event.getChangedNode().getAttributes().getNamedItem("id"); - if (id != null && "searchClass:searchStudent".equals(id.getTextContent())) { + if (id != null && detailIds.contains(id.getTextContent())) { LOG.info(event.getChangedNode().asXml()); } } diff --git a/src/main/java/pl/labno/bernard/htmlunified/WebClientUtils.java b/src/main/java/pl/labno/bernard/htmlunified/WebClientUtils.java index 8c23130..b65af72 100644 --- a/src/main/java/pl/labno/bernard/htmlunified/WebClientUtils.java +++ b/src/main/java/pl/labno/bernard/htmlunified/WebClientUtils.java @@ -1,11 +1,15 @@ package pl.labno.bernard.htmlunified; import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.DomNode; import com.gargoylesoftware.htmlunit.html.DomNodeList; import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlTableCell; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class WebClientUtils { @@ -29,19 +33,27 @@ public class WebClientUtils { WebClientUtils.defaultTimeout = defaultTimeout; } - public static int waitForJSJob(WebClient webClient) { - return waitForJSJob(webClient, webClient.waitForBackgroundJavaScript(10) - 1); + public static int waitForJSJob(String message, WebClient webClient) { + return waitForJSJob(message, webClient, webClient.waitForBackgroundJavaScript(10) - 1); } - public static int waitForJSJob(WebClient webClient, int initialJobCount) { - return waitForJSJob(webClient, initialJobCount, defaultTimeout); + public static int waitForJSJob(String message, WebClient webClient, int initialJobCount) { + return waitForJSJob(message, webClient, initialJobCount, defaultTimeout); } - public static int waitForJSJob(WebClient webClient, int initialJobCount, long timeout) { - return waitForJSJob(webClient, initialJobCount, timeout, defaultCheckInterval); + public static int waitForJSJob(WebClient webClient, int initialJobCount, int timeout) { + return waitForJSJob(null, webClient, initialJobCount, timeout); } - public static int waitForJSJob(WebClient webClient, int initialJobCount, long timeout, long checkInterval) { + public static int waitForJSJob(String message, WebClient webClient, int initialJobCount, long timeout) { + return waitForJSJob(message, webClient, initialJobCount, timeout, defaultCheckInterval); + } + + public static int waitForJSJob(String message, WebClient webClient, int initialJobCount, int timeout) { + return waitForJSJob(message, webClient, initialJobCount, timeout, defaultCheckInterval); + } + + public static int waitForJSJob(String message, WebClient webClient, int initialJobCount, long timeout, long checkInterval) { int jobs; long startTime = System.currentTimeMillis(); do { @@ -50,7 +62,7 @@ public class WebClientUtils { throw new RuntimeException("Number of JavaScript jobs doesn't drop to initial level for " + timeout + " seconds. It's memory leak in your JavaScript rather then request taking so long!"); } } while (jobs > initialJobCount); - System.out.println("Waiting took: " + (System.currentTimeMillis() - startTime) + "ms"); + System.out.println("Waiting" + (message == null ? "" : " for " + message) + " took: " + (System.currentTimeMillis() - startTime) + "ms"); return jobs; } @@ -88,4 +100,32 @@ public class WebClientUtils { } } while (startTime + timeout > System.currentTimeMillis()); } + + public static void executeAjaxReRenderedScripts(HtmlPage page) { + final DomNodeList scripts = page.getElementsByTagName("script"); + /** + * We cannot iterate over html DomNodeList cause it depends on sibling relationship which we will modify. + */ + final List scriptsList = new ArrayList(); + for (HtmlElement element : scripts) { + scriptsList.add(element); + } + for (HtmlElement element : scriptsList) { + if (element.getChildNodes().size() > 1) { + element.removeChild(element.getFirstChild()); + final DomNode sibling = element.getNextSibling(); + final DomNode parentNode = element.getParentNode(); + /** + * Script will be executed upon inserting into DOM tree, so we removed and add it again. + */ + if (sibling != null) { + parentNode.removeChild(element); + sibling.insertBefore(element); + } else { + parentNode.removeChild(element); + parentNode.appendChild(element); + } + } + } + } } diff --git a/src/test/java/pl/labno/bernard/htmlunified/ClassDetailsTest.java b/src/test/java/pl/labno/bernard/htmlunified/ClassDetailsTest.java new file mode 100644 index 0000000..18cc828 --- /dev/null +++ b/src/test/java/pl/labno/bernard/htmlunified/ClassDetailsTest.java @@ -0,0 +1,97 @@ +package pl.labno.bernard.htmlunified; + +import com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlSpan; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +public class ClassDetailsTest { + + @Test + public void openDetailsModalPanel() throws IOException { + WebClient client = new WebClient(BrowserVersion.FIREFOX_3_6); + HtmlPage page = login(client); + @SuppressWarnings("unchecked") + final List titleElements = (List) page.getElementById("results:schedule").getByXPath(".//span[@class='fc-event-title']"); + Assert.assertNotSame(0, titleElements.size()); + new RequestResponseLogger(client); + page.addDomChangeListener(new DomChangeLogger()); + WebClientUtils.waitForJSJob("initial javascript to finish", client, 0, 30000); + System.out.println("item:" + titleElements.get(0).asText()); + ((HtmlElement) titleElements.get(0).getParentNode()).mouseOver(); + ((HtmlElement) titleElements.get(0).getParentNode()).click(); + WebClientUtils.waitForJSJob("classDetailsModalPanel shown after schedule event clicked", client, 0, 30000); + Assert.assertNotNull(page.getElementById("classDetailsModalPanelContainer")); + } + + @Test + public void closeDetailsModalPanel() throws IOException { + WebClient client = new WebClient(BrowserVersion.FIREFOX_3_6); + HtmlPage page = login(client); + @SuppressWarnings("unchecked") + final List titleElements = (List) page.getElementById("results:schedule").getByXPath(".//span[@class='fc-event-title']"); + Assert.assertNotSame(0, titleElements.size()); +// new RequestResponseLogger(client); +// page.addDomChangeListener(new DomChangeLogger()); + WebClientUtils.waitForJSJob("initial javascript to finish", client, 0, 30000); + System.out.println("item:" + titleElements.get(0).asText()); + ((HtmlElement) titleElements.get(0).getParentNode()).mouseOver(); + ((HtmlElement) titleElements.get(0).getParentNode()).click(); + WebClientUtils.waitForJSJob("classDetailsModalPanel shown after schedule event clicked", client, 0, 30000); + Assert.assertNotNull(page.getElementById("classDetailsModalPanelContainer")); + ((HtmlElement) page.getElementById("classDetailsModalPanelContainer").getByXPath("//*[contains(@src,'close.png')]").get(0)).click(); + WebClientUtils.waitForJSJob("classDetailsModalPanel hidden after cancel button", client, 0, 30000); + Assert.assertNull(page.getElementById("classDetailsModalPanelContainer")); + } + + @Test + public void removeClass() throws IOException { + WebClient client = new WebClient(BrowserVersion.FIREFOX_2); + final RequestResponseLogger requestResponseLogger = new RequestResponseLogger(client); + requestResponseLogger.off(); + HtmlPage page = login(client); + @SuppressWarnings("unchecked") + final List titleElements = (List) page.getElementById("results:schedule").getByXPath(".//span[@class='fc-event-title']"); + Assert.assertEquals(6, titleElements.size()); + Assert.assertNotSame(0, titleElements.size()); + WebClientUtils.waitForJSJob("initial javascript to finish", client, 0, 30000); + + client.setAlertHandler(new AlertLogger()); + final DomChangeLogger domChangeLogger = new DomChangeLogger(); + domChangeLogger.showDetails("results:j_id2158"); +// page.addDomChangeListener(domChangeLogger); +// page.addHtmlAttsributeChangeListener(new HtmlAttributeChangeLogger()); +// requestResponseLogger.on(); + + ((HtmlElement) titleElements.get(0).getParentNode()).mouseOver(); + ((HtmlElement) titleElements.get(0).getParentNode()).click(); + WebClientUtils.waitForJSJob("classDetailsModalPanel shown after schedule event clicked", client, 0, 30000); + WebClientUtils.executeAjaxReRenderedScripts(page); + Assert.assertNotNull(page.getElementById("classDetailsModalPanelContainer")); + ((HtmlElement) page.getElementById("classDetailsModalPanelContainer").getByXPath("//*[contains(@src,'remove.png')]").get(0)).click(); + WebClientUtils.waitForJSJob("classDetailsModalPanel hidden after remove button", client, 0, 30000); + WebClientUtils.executeAjaxReRenderedScripts(page); + WebClientUtils.waitForJSJob("schedule refresh", client, 0, 30000); + Assert.assertNull(page.getElementById("classDetailsModalPanelContainer")); + Assert.assertEquals(5, page.getElementById("results:schedule").getByXPath(".//span[@class='fc-event-title']").size()); + } + + private HtmlPage login(WebClient client) throws IOException { + HtmlPage page = (HtmlPage) client.getPage("http://localhost:8080/schoolmanager/view/class/current.seam?networkId=salsafactory"); + ((HtmlInput) page.getElementById("loginForm:email")).setValueAttribute("it.crowd.test@gmail.com"); + ((HtmlInput) page.getElementById("loginForm:password")).setValueAttribute("aaaaa"); + HtmlPage oldPage = page; + page = page.getElementById("loginForm:submit").click(); +// This assert would fail (when page redidrects than you need to obtain new page) +// Assert.assertEquals(page,oldPage); + Assert.assertEquals(client.getCurrentWindow().getEnclosedPage(), page); + return page; + } +} diff --git a/src/test/java/pl/labno/bernard/htmlunified/SandboxTest.java b/src/test/java/pl/labno/bernard/htmlunified/SandboxTest.java index 22a4039..8623fca 100644 --- a/src/test/java/pl/labno/bernard/htmlunified/SandboxTest.java +++ b/src/test/java/pl/labno/bernard/htmlunified/SandboxTest.java @@ -1,6 +1,7 @@ package pl.labno.bernard.htmlunified; import com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.CollectingAlertHandler; import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlPage; @@ -8,11 +9,12 @@ import org.junit.Assert; import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; public class SandboxTest { @Test - public void test() throws IOException { + public void testClickImage() throws IOException { final WebClient client = new WebClient(BrowserVersion.FIREFOX_3_6); client.setAjaxController(new NicelyResynchronizingAjaxController()); HtmlPage page = (HtmlPage) client.getPage("file:./target/test-classes/sandbox.html"); @@ -20,4 +22,23 @@ public class SandboxTest { page.getElementById("close").click(); Assert.assertEquals("", page.getElementById("elementToHide").asText()); } + + @Test + public void testClickJQueryBoundElement() throws IOException { + final WebClient client = new WebClient(BrowserVersion.FIREFOX_3_6); + client.setAjaxController(new NicelyResynchronizingAjaxController()); + HtmlPage page = (HtmlPage) client.getPage("file:./target/test-classes/sandbox.html"); + final ArrayList alerts = new ArrayList(); + client.setAlertHandler(new CollectingAlertHandler(alerts)); + page.getElementById("clickable").click(); + System.out.println(alerts); + Assert.assertEquals(1, alerts.size()); + Assert.assertEquals("Clicked clickable", alerts.get(0)); + + alerts.clear(); + page.getElementById("clickable2child").click(); + System.out.println(alerts); + Assert.assertEquals(1, alerts.size()); + Assert.assertEquals("Clicked clickable2", alerts.get(0)); + } } diff --git a/src/test/java/pl/labno/bernard/htmlunified/ScheduleTest.java b/src/test/java/pl/labno/bernard/htmlunified/ScheduleTest.java index f906a31..a7206c7 100644 --- a/src/test/java/pl/labno/bernard/htmlunified/ScheduleTest.java +++ b/src/test/java/pl/labno/bernard/htmlunified/ScheduleTest.java @@ -30,7 +30,7 @@ public class ScheduleTest { for (HtmlSpan o : titleElements) { titles.add(o.asText()); } - Assert.assertEquals(4, titles.size()); + Assert.assertEquals(6, titles.size()); } @Test @@ -58,7 +58,6 @@ public class ScheduleTest { System.out.println("Success!"); -// page = (HtmlPage) client.getPage("http://localhost:8080/schoolmanager/view/class/current.seam?networkId=salsafactory"); WebClientUtils.waitForJSJob(client, 0, 30000); Assert.assertEquals(client.getCurrentWindow().getEnclosedPage(), page); studentInput = (HtmlInput) page.getElementById("searchClass:student_i"); @@ -66,7 +65,6 @@ public class ScheduleTest { studentInput.type('l'); studentInput.type('i'); Assert.assertEquals(client.getCurrentWindow().getEnclosedPage(), page); -// WebClientUtils.forceWait(2000); // Here the suggestionBox is still hidden WebClientUtils.waitForJSJob(client, 0, 30000); // Here suggestionBox is visible @@ -138,7 +136,7 @@ public class ScheduleTest { private HtmlPage login(WebClient client) throws IOException { HtmlPage page = (HtmlPage) client.getPage("http://localhost:8080/schoolmanager/view/class/current.seam?networkId=salsafactory"); - ((HtmlInput) page.getElementById("loginForm:email")).setValueAttribute("s4237@pjwstk.edu.pl"); + ((HtmlInput) page.getElementById("loginForm:email")).setValueAttribute("it.crowd.test@gmail.com"); ((HtmlInput) page.getElementById("loginForm:password")).setValueAttribute("aaaaa"); HtmlPage oldPage = page; page = page.getElementById("loginForm:submit").click(); diff --git a/src/test/resources/jquery-1.3.2.js b/src/test/resources/jquery-1.3.2.js new file mode 100644 index 0000000..e5fb9a5 --- /dev/null +++ b/src/test/resources/jquery-1.3.2.js @@ -0,0 +1,4420 @@ +/*! + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function() { + + var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + // Map over the $ in case of overwrite + _$ = window.$, + + jQuery = window.jQuery = window.$ = function(selector, context) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init(selector, context); + }, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + + jQuery.fn = jQuery.prototype = { + init: function(selector, context) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if (selector.nodeType) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if (typeof selector === "string") { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec(selector); + + // Verify a match, and that no context was specified for #id + if (match && (match[1] || !context)) { + + // HANDLE: $(html) -> $(array) + if (match[1]) + selector = jQuery.clean([ match[1] ], context); + + // HANDLE: $("#id") + else { + var elem = document.getElementById(match[3]); + + // Handle the case where IE and Opera return items + // by name instead of ID + if (elem && elem.id != match[3]) + return jQuery().find(selector); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery(elem || []); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery(context).find(selector); + + // HANDLE: $(function) + // Shortcut for document ready + } else if (jQuery.isFunction(selector)) + return jQuery(document).ready(selector); + + // Make sure that old selector state is passed along + if (selector.selector && selector.context) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.isArray(selector) ? + selector : + jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.2", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function(num) { + return num === undefined ? + + // Return a 'clean' array + Array.prototype.slice.call(this) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function(elems, name, selector) { + // Build a new jQuery matched element set + var ret = jQuery(elems); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if (name === "find") + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if (name) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function(elems) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply(this, elems); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function(callback, args) { + return jQuery.each(this, callback, args); + }, + + // Determine the position of an element within + // the matched set of elements + index: function(elem) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this); + }, + + attr: function(name, value, type) { + var options = name; + + // Look for the case where we're accessing a style value + if (typeof name === "string") + if (value === undefined) + return this[0] && jQuery[ type || "attr" ](this[0], name); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i) { + // Set all the styles + for (name in options) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop(this, options[ name ], type, i, name) + ); + }); + }, + + css: function(key, value) { + // ignore negative width and height values + if ((key == 'width' || key == 'height') && parseFloat(value) < 0) + value = undefined; + return this.attr(key, value, "curCSS"); + }, + + text: function(text) { + if (typeof text !== "object" && text != null) + return this.empty().append((this[0] && this[0].ownerDocument || document).createTextNode(text)); + + var ret = ""; + + jQuery.each(text || this, function() { + jQuery.each(this.childNodes, function() { + if (this.nodeType != 8) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text([ this ]); + }); + }); + + return ret; + }, + + wrapAll: function(html) { + if (this[0]) { + // The elements to wrap the target around + var wrap = jQuery(html, this[0].ownerDocument).clone(); + + if (this[0].parentNode) + wrap.insertBefore(this[0]); + + wrap.map(function() { + var elem = this; + + while (elem.firstChild) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function(html) { + return this.each(function() { + jQuery(this).contents().wrapAll(html); + }); + }, + + wrap: function(html) { + return this.each(function() { + jQuery(this).wrapAll(html); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem) { + if (this.nodeType == 1) + this.appendChild(elem); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem) { + if (this.nodeType == 1) + this.insertBefore(elem, this.firstChild); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem) { + this.parentNode.insertBefore(elem, this); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem) { + this.parentNode.insertBefore(elem, this.nextSibling); + }); + }, + + end: function() { + return this.prevObject || jQuery([]); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: [].push, + sort: [].sort, + splice: [].splice, + + find: function(selector) { + if (this.length === 1) { + var ret = this.pushStack([], "find", selector); + ret.length = 0; + jQuery.find(selector, this[0], ret); + return ret; + } else { + return this.pushStack(jQuery.unique(jQuery.map(this, function(elem) { + return jQuery.find(selector, elem); + })), "find", selector); + } + }, + + clone: function(events) { + // Do the clone + var ret = this.map(function() { + if (!jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this)) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML; + if (!html) { + var div = this.ownerDocument.createElement("div"); + div.appendChild(this.cloneNode(true)); + html = div.innerHTML; + } + + return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; + } else + return this.cloneNode(true); + }); + + // Copy the events from the original to the clone + if (events === true) { + var orig = this.find("*").andSelf(), i = 0; + + ret.find("*").andSelf().each(function() { + if (this.nodeName !== orig[i].nodeName) + return; + + var events = jQuery.data(orig[i], "events"); + + for (var type in events) { + for (var handler in events[ type ]) { + jQuery.event.add(this, type, events[ type ][ handler ], events[ type ][ handler ].data); + } + } + + i++; + }); + } + + // Return the cloned set + return ret; + }, + + filter: function(selector) { + return this.pushStack( + jQuery.isFunction(selector) && + jQuery.grep(this, function(elem, i) { + return selector.call(elem, i); + }) || + + jQuery.multiFilter(selector, jQuery.grep(this, function(elem) { + return elem.nodeType === 1; + })), "filter", selector); + }, + + closest: function(selector) { + var pos = jQuery.expr.match.POS.test(selector) ? jQuery(selector) : null, + closer = 0; + + return this.map(function() { + var cur = this; + while (cur && cur.ownerDocument) { + if (pos ? pos.index(cur) > -1 : jQuery(cur).is(selector)) { + jQuery.data(cur, "closest", closer); + return cur; + } + cur = cur.parentNode; + closer++; + } + }); + }, + + not: function(selector) { + if (typeof selector === "string") + // test special case where just one selector is passed in + if (isSimple.test(selector)) + return this.pushStack(jQuery.multiFilter(selector, this, true), "not", selector); + else + selector = jQuery.multiFilter(selector, this); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray(this, selector) < 0 : this != selector; + }); + }, + + add: function(selector) { + return this.pushStack(jQuery.unique(jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery(selector) : + jQuery.makeArray(selector) + ))); + }, + + is: function(selector) { + return !!selector && jQuery.multiFilter(selector, this).length > 0; + }, + + hasClass: function(selector) { + return !!selector && this.is("." + selector); + }, + + val: function(value) { + if (value === undefined) { + var elem = this[0]; + + if (elem) { + if (jQuery.nodeName(elem, 'option')) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if (jQuery.nodeName(elem, "select")) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if (index < 0) + return null; + + // Loop through all the selected options + for (var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++) { + var option = options[ i ]; + + if (option.selected) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if (one) + return value; + + // Multi-Selects return an array + values.push(value); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if (typeof value === "number") + value += ''; + + return this.each(function() { + if (this.nodeType != 1) + return; + + if (jQuery.isArray(value) && /radio|checkbox/.test(this.type)) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if (jQuery.nodeName(this, "select")) { + var values = jQuery.makeArray(value); + + jQuery("option", this).each(function() { + this.selected = (jQuery.inArray(this.value, values) >= 0 || + jQuery.inArray(this.text, values) >= 0); + }); + + if (!values.length) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function(value) { + return value === undefined ? + (this[0] ? + this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : + null) : + this.empty().append(value); + }, + + replaceWith: function(value) { + return this.after(value).remove(); + }, + + eq: function(i) { + return this.slice(i, +i + 1); + }, + + slice: function() { + return this.pushStack(Array.prototype.slice.apply(this, arguments), + "slice", Array.prototype.slice.call(arguments).join(",")); + }, + + map: function(callback) { + return this.pushStack(jQuery.map(this, function(elem, i) { + return callback.call(elem, i, elem); + })); + }, + + andSelf: function() { + return this.add(this.prevObject); + }, + + domManip: function(args, table, callback) { + if (this[0]) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean(args, (this[0].ownerDocument || this[0]), fragment), + first = fragment.firstChild; + + if (first) + for (var i = 0, l = this.length; i < l; i++) + callback.call(root(this[i], first), this.length > 1 || i > 0 ? + fragment.cloneNode(true) : fragment); + + if (scripts) + jQuery.each(scripts, evalScript); + } + + return this; + + function root(elem, cur) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } + }; + + // Give the init function the jQuery prototype for later instantiation + jQuery.fn.init.prototype = jQuery.fn; + + function evalScript(i, elem) { + if (elem.src) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval(elem.text || elem.textContent || elem.innerHTML || ""); + + if (elem.parentNode) + elem.parentNode.removeChild(elem); + } + + function now() { + return +new Date; + } + + jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if (typeof target === "boolean") { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== "object" && !jQuery.isFunction(target)) + target = {}; + + // extend jQuery itself if only one argument is passed + if (length == i) { + target = this; + --i; + } + + for (; i < length; i++) + // Only deal with non-null/undefined values + if ((options = arguments[ i ]) != null) + // Extend the base object + for (var name in options) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if (target === copy) + continue; + + // Recurse if we're merging object values + if (deep && copy && typeof copy === "object" && !copy.nodeType) + target[ name ] = jQuery.extend(deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy); + + // Don't bring in undefined values + else if (copy !== undefined) + target[ name ] = copy; + + } + + // Return the modified object + return target; + }; + + // exclude the following css properties to add px + var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + + jQuery.extend({ + noConflict: function(deep) { + window.$ = _$; + + if (deep) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function(obj) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function(obj) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function(elem) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc(elem.ownerDocument); + }, + + // Evalulates a script in a global context + globalEval: function(data) { + if (data && /\S/.test(data)) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if (jQuery.support.scriptEval) + script.appendChild(document.createTextNode(data)); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore(script, head.firstChild); + head.removeChild(script); + } + }, + + nodeName: function(elem, name) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function(object, callback, args) { + var name, i = 0, length = object.length; + + if (args) { + if (length === undefined) { + for (name in object) + if (callback.apply(object[ name ], args) === false) + break; + } else + for (; i < length;) + if (callback.apply(object[ i++ ], args) === false) + break; + + // A special, fast, case for the most common use of each + } else { + if (length === undefined) { + for (name in object) + if (callback.call(object[ name ], name, object[ name ]) === false) + break; + } else + for (var value = object[0]; + i < length && callback.call(value, i, value) !== false; value = object[++i]) { + } + } + + return object; + }, + + prop: function(elem, value, type, i, name) { + // Handle executable functions + if (jQuery.isFunction(value)) + value = value.call(elem, i); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test(name) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function(elem, classNames) { + jQuery.each((classNames || "").split(/\s+/), function(i, className) { + if (elem.nodeType == 1 && !jQuery.className.has(elem.className, className)) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function(elem, classNames) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className) { + return !jQuery.className.has(classNames, className); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function(elem, className) { + return elem && jQuery.inArray(className, (elem.className || elem).toString().split(/\s+/)) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function(elem, options, callback) { + var old = {}; + // Remember the old values, and insert the new ones + for (var name in options) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call(elem); + + // Revert the old values + for (var name in options) + elem.style[ name ] = old[ name ]; + }, + + css: function(elem, name, force, extra) { + if (name == "width" || name == "height") { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + + if (extra === "border") + return; + + jQuery.each(which, function() { + if (!extra) + val -= parseFloat(jQuery.curCSS(elem, "padding" + this, true)) || 0; + if (extra === "margin") + val += parseFloat(jQuery.curCSS(elem, "margin" + this, true)) || 0; + else + val -= parseFloat(jQuery.curCSS(elem, "border" + this + "Width", true)) || 0; + }); + } + + if (elem.offsetWidth !== 0) + getWH(); + else + jQuery.swap(elem, props, getWH); + + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS(elem, name, force); + }, + + curCSS: function(elem, name, force) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if (name == "opacity" && !jQuery.support.opacity) { + ret = jQuery.attr(style, "opacity"); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if (name.match(/float/i)) + name = styleFloat; + + if (!force && style && style[ name ]) + ret = style[ name ]; + + else if (defaultView.getComputedStyle) { + + // Only "float" is needed here + if (name.match(/float/i)) + name = "float"; + + name = name.replace(/([A-Z])/g, "-$1").toLowerCase(); + + var computedStyle = defaultView.getComputedStyle(elem, null); + + if (computedStyle) + ret = computedStyle.getPropertyValue(name); + + // We should always get a number back from opacity + if (name == "opacity" && ret == "") + ret = "1"; + + } else if (elem.currentStyle) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter) { + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if (!/^\d+(px)?$/i.test(ret) && /^\d/.test(ret)) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function(elems, context, fragment) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if (typeof context.createElement === "undefined") + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if (!fragment && elems.length === 1 && typeof elems[0] === "string") { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if (match) + return [ context.createElement(match[1]) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem) { + if (typeof elem === "number") + elem += ''; + + if (!elem) + return; + + // Convert html string into DOM nodes + if (typeof elem === "string") { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag) { + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + close
elementToHide
+ +
Click me
+ \ No newline at end of file