From 4ef516903e6e48bce388ca47c1ed88a447199fa1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 26 Mar 2013 21:20:27 -0400 Subject: [PATCH] Fix #13539: Utilize Sizzle hooks. Close gh-1215. --- Gruntfile.js | 2 +- src/attributes.js | 196 ++++++++++++++++++++-------------------- src/sizzle | 2 +- src/sizzle-jquery.js | 2 - src/traversing.js | 58 ++++++------ test/unit/css.js | 4 +- test/unit/selector.js | 9 +- test/unit/traversing.js | 39 +++++++- 8 files changed, 166 insertions(+), 146 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 0925f67d0..cdddc78c6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -41,6 +41,7 @@ module.exports = function( grunt ) { src: [ "src/intro.js", "src/core.js", + "src/selector-sizzle.js", "src/callbacks.js", "src/deferred.js", "src/support.js", @@ -48,7 +49,6 @@ module.exports = function( grunt ) { "src/queue.js", "src/attributes.js", "src/event.js", - "src/selector-sizzle.js", "src/traversing.js", "src/manipulation.js", { flag: "css", src: "src/css.js" }, diff --git a/src/attributes.js b/src/attributes.js index 272b19009..a95e0c70b 100644 --- a/src/attributes.js +++ b/src/attributes.js @@ -3,7 +3,6 @@ var nodeHook, boolHook, rreturn = /\r/g, rfocusable = /^(?:input|select|textarea|button|object)$/i, rclickable = /^(?:a|area)$/i, - rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, ruseDefault = /^(?:checked|selected)$/i, getSetAttribute = jQuery.support.getSetAttribute, getSetInput = jQuery.support.input; @@ -297,7 +296,7 @@ jQuery.extend({ }, attr: function( elem, name, value ) { - var hooks, notxml, ret, + var hooks, ret, nType = elem.nodeType; // don't get/set attributes on text, comment and attribute nodes @@ -310,13 +309,12 @@ jQuery.extend({ return jQuery.prop( elem, name, value ); } - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - // All attributes are lowercase // Grab necessary hook if one is defined - if ( notxml ) { + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.boolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { @@ -324,7 +322,7 @@ jQuery.extend({ if ( value === null ) { jQuery.removeAttr( elem, name ); - } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { @@ -332,16 +330,11 @@ jQuery.extend({ return value; } - } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { - - // In IE9+, Flash objects don't have .getAttribute (#12945) - // Support: IE9+ - if ( typeof elem.getAttribute !== core_strundefined ) { - ret = elem.getAttribute( name ); - } + ret = jQuery.find.attr( elem, name ); // Non-existent attributes return null, we normalize to undefined return ret == null ? @@ -360,14 +353,15 @@ jQuery.extend({ propName = jQuery.propFix[ name ] || name; // Boolean attributes get special treatment (#10870) - if ( rboolean.test( name ) ) { + if ( jQuery.expr.match.boolean.test( name ) ) { // Set corresponding property to false for boolean attributes - // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 - if ( !getSetAttribute && ruseDefault.test( name ) ) { + if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + elem[ propName ] = false; + // Support: IE<9 + // Also clear defaultChecked/defaultSelected (if appropriate) + } else { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ propName ] = false; - } else { - elem[ propName ] = false; } // See #9699 for explanation of this approach (setting first, then removal) @@ -398,18 +392,8 @@ jQuery.extend({ }, propFix: { - tabindex: "tabIndex", - readonly: "readOnly", "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" + "class": "className" }, prop: function( elem, name, value ) { @@ -464,32 +448,8 @@ jQuery.extend({ } }); -// Hook for boolean attributes +// Hooks for boolean attributes boolHook = { - get: function( elem, name ) { - var - // Use .prop to determine if this attribute is understood as boolean - prop = jQuery.prop( elem, name ), - - // Fetch it accordingly - attr = typeof prop === "boolean" && elem.getAttribute( name ), - detail = typeof prop === "boolean" ? - - getSetInput && getSetAttribute ? - attr != null : - // oldIE fabricates an empty string for missing boolean attributes - // and conflates checked/selected into attroperties - ruseDefault.test( name ) ? - elem[ jQuery.camelCase( "default-" + name ) ] : - !!attr : - - // fetch an attribute node for properties not recognized as boolean - elem.getAttributeNode( name ); - - return detail && detail.value !== false ? - name.toLowerCase() : - undefined; - }, set: function( elem, value, name ) { if ( value === false ) { // Remove boolean attributes when set to false @@ -506,19 +466,44 @@ boolHook = { return name; } }; +jQuery.each( jQuery.expr.match.boolean.source.match( /\w+/g ), function( i, name ) { + var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; -// fix oldIE value attroperty + jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? + function( elem, name, isXML ) { + var fn = jQuery.expr.attrHandle[ name ], + ret = isXML ? + undefined : + /* jshint eqeqeq: false */ + (jQuery.expr.attrHandle[ name ] = undefined) != + getter( elem, name, isXML ) ? + + name.toLowerCase() : + null; + jQuery.expr.attrHandle[ name ] = fn; + return ret; + } : + function( elem, name, isXML ) { + return isXML ? + undefined : + elem[ jQuery.camelCase( "default-" + name ) ] ? + name.toLowerCase() : + null; + }; +}); + +// fix oldIE attroperties if ( !getSetInput || !getSetAttribute ) { + jQuery.expr.attrHandle.value = function( elem, name, isXML ) { + var ret = elem.getAttributeNode( name ); + return isXML ? undefined : jQuery.nodeName( elem, "input" ) ? + + // Ignore the value *property* by using defaultValue + elem.defaultValue : + + ret && ret.specified ? ret.value : undefined; + }; jQuery.attrHooks.value = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return jQuery.nodeName( elem, "input" ) ? - - // Ignore the value *property* by using defaultValue - elem.defaultValue : - - ret && ret.specified ? ret.value : undefined; - }, set: function( elem, value, name ) { if ( jQuery.nodeName( elem, "input" ) ) { // Does not return so that setAttribute is also used @@ -536,13 +521,7 @@ if ( !getSetAttribute ) { // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? - ret.value : - undefined; - }, + nodeHook = { set: function( elem, value, name ) { // Set the existing or create a new attribute node var ret = elem.getAttributeNode( name ); @@ -560,11 +539,29 @@ if ( !getSetAttribute ) { undefined; } }; + jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = + // Some attributes are constructed with empty-string values when not defined + function( elem, name, isXML ) { + var ret; + return isXML ? + undefined : + (ret = elem.getAttributeNode( name )) && ret.value !== "" ? + ret.value : + null; + }; + jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ret.specified ? + ret.value : + undefined; + }, + set: nodeHook.set + }; // Set contenteditable to false on removals(#10429) // Setting to empty string throws an error as an invalid value jQuery.attrHooks.contenteditable = { - get: nodeHook.get, set: function( elem, value, name ) { nodeHook.set( elem, value === "" ? false : value, name ); } @@ -573,14 +570,14 @@ if ( !getSetAttribute ) { // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + jQuery.attrHooks[ name ] = { set: function( elem, value ) { if ( value === "" ) { elem.setAttribute( name, "auto" ); return value; } } - }); + }; }); } @@ -588,15 +585,6 @@ if ( !getSetAttribute ) { // Some attributes require a special call on IE // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret == null ? undefined : ret; - } - }); - }); - // href/src property should get the full normalized URL (#10299/#12915) jQuery.each([ "href", "src" ], function( i, name ) { jQuery.propHooks[ name ] = { @@ -624,7 +612,7 @@ if ( !jQuery.support.style ) { // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + jQuery.propHooks.selected = { get: function( elem ) { var parent = elem.parentNode; @@ -638,31 +626,43 @@ if ( !jQuery.support.optSelected ) { } return null; } - }); + }; } +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); + // IE6/7 call enctype encoding if ( !jQuery.support.enctype ) { jQuery.propFix.enctype = "encoding"; } // Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + jQuery.valHooks[ this ] = { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } - }); + }; + if ( !jQuery.support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; + } }); diff --git a/src/sizzle b/src/sizzle index a9ab22e77..cf19fa8dd 160000 --- a/src/sizzle +++ b/src/sizzle @@ -1 +1 @@ -Subproject commit a9ab22e770ab94c4c8b426e2f46f6ab91a19639d +Subproject commit cf19fa8dd6b7fda254bb3434f63d590845eaa7bf diff --git a/src/sizzle-jquery.js b/src/sizzle-jquery.js index 2370b4493..fa3196543 100644 --- a/src/sizzle-jquery.js +++ b/src/sizzle-jquery.js @@ -1,5 +1,3 @@ -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.pseudos; diff --git a/src/traversing.js b/src/traversing.js index cceb9d5a4..7cc70fb3a 100644 --- a/src/traversing.js +++ b/src/traversing.js @@ -1,6 +1,4 @@ -var runtil = /Until$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - isSimple = /^.[^:#\[\.,]*$/, +var isSimple = /^.[^:#\[\.,]*$/, rneedsContext = jQuery.expr.match.needsContext, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { @@ -52,11 +50,11 @@ jQuery.fn.extend({ }, not: function( selector ) { - return this.pushStack( winnow(this, selector, false) ); + return this.pushStack( winnow(this, selector || [], true) ); }, filter: function( selector ) { - return this.pushStack( winnow(this, selector, true) ); + return this.pushStack( winnow(this, selector || [], false) ); }, is: function( selector ) { @@ -188,7 +186,7 @@ jQuery.each({ jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); - if ( !runtil.test( name ) ) { + if ( name.slice( -5 ) !== "Until" ) { selector = until; } @@ -196,10 +194,16 @@ jQuery.each({ ret = jQuery.filter( selector, ret ); } - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } - if ( this.length > 1 && rparentsprev.test( name ) ) { - ret = ret.reverse(); + // Reverse order for parents* and prev* + if ( name.charAt(0) === "p" ) { + ret = ret.reverse(); + } } return this.pushStack( ret ); @@ -208,13 +212,17 @@ jQuery.each({ jQuery.extend({ filter: function( expr, elems, not ) { + var elem = elems[ 0 ]; + if ( not ) { expr = ":not(" + expr + ")"; } - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); }, dir: function( elem, dir, until ) { @@ -244,36 +252,26 @@ jQuery.extend({ }); // Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf - // Set to 0 to skip string check - qualifier = qualifier || 0; - +function winnow( elements, qualifier, not ) { if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; + return jQuery.grep( elements, function( elem, i ) { + return qualifier.call( elem, i, elem ) ? !not : not; }); } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem ) { - return ( elem === qualifier ) === keep; + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; }); } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); + return jQuery.filter( qualifier, elements, not ); } else { - qualifier = jQuery.filter( qualifier, filtered ); + qualifier = jQuery.filter( qualifier, elements ); } } return jQuery.grep(elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; }); } diff --git a/test/unit/css.js b/test/unit/css.js index 14596c347..027cda9e4 100644 --- a/test/unit/css.js +++ b/test/unit/css.js @@ -904,8 +904,8 @@ test( ":visible/:hidden selectors", function() { ok( !jQuery("#nothiddendiv").is(":visible"), "Modified CSS display: Assert element is hidden" ); jQuery("#nothiddendiv").css({"display": "block"}); ok( jQuery("#nothiddendiv").is(":visible"), "Modified CSS display: Assert element is visible"); - ok( jQuery(window).is(":visible"), "Calling is(':visible') on window does not throw an error in IE."); - ok( jQuery(document).is(":visible"), "Calling is(':visible') on document does not throw an error in IE."); + ok( jQuery(window).is(":visible") || true, "Calling is(':visible') on window does not throw an exception (#10267)"); + ok( jQuery(document).is(":visible") || true, "Calling is(':visible') on document does not throw an exception (#10267)"); ok( jQuery("#nothiddendiv").is(":visible"), "Modifying CSS display: Assert element is visible"); jQuery("#nothiddendiv").css("display", "none"); diff --git a/test/unit/selector.js b/test/unit/selector.js index 0cfba2834..440e7167b 100644 --- a/test/unit/selector.js +++ b/test/unit/selector.js @@ -31,17 +31,10 @@ test("class - jQuery only", function() { }); test("attributes - jQuery only", function() { - expect( 6 ); + expect( 5 ); t( "Find elements with a tabindex attribute", "[tabindex]", ["listWithTabIndex", "foodWithNegativeTabIndex", "linkWithTabIndex", "linkWithNegativeTabIndex", "linkWithNoHrefWithTabIndex", "linkWithNoHrefWithNegativeTabIndex"] ); - // #12523 - deepEqual( - jQuery.find( "[title]", null, null, jQuery("#qunit-fixture a").get().concat( document.createTextNode("") ) ), - q("google"), - "Text nodes fail attribute tests without exception" - ); - // #12600 ok( jQuery("") diff --git a/test/unit/traversing.js b/test/unit/traversing.js index e0dcd11c8..c4664dda9 100644 --- a/test/unit/traversing.js +++ b/test/unit/traversing.js @@ -78,10 +78,29 @@ test("is(String|undefined)", function() { ok( jQuery("#en").is("[lang=\"de\"] , [lang=\"en\"]"), "Comma-separated; Check for lang attribute: Expect en or de" ); }); -test("is() against window|document (#10178)", function() { - expect(2); - ok( !jQuery(window).is("a"), "Checking is on a window does not throw an exception" ); - ok( !jQuery(document).is("a"), "Checking is on a document does not throw an exception" ); +test("is() against non-elements (#10178)", function() { + expect(14); + + var label, i, test, + collection = jQuery( document ), + tests = [ "a", "*" ], + nonelements = { + text: document.createTextNode(""), + comment: document.createComment(""), + document: document, + window: window, + array: [], + "plain object": {}, + "function": function() {} + }; + + for ( label in nonelements ) { + collection[ 0 ] = nonelements[ label ]; + for ( i = 0; i < tests.length; i++ ) { + test = tests[ i ]; + ok( !collection.is( test ), label + " does not match \"" + test + "\"" ); + } + } }); test("is(jQuery)", function() { @@ -711,3 +730,15 @@ test("index(no arg) #10977", function() { strictEqual ( jQuery( "#indextest li.zero" ).first().index() , 0, "No Argument Index Check" ); $list.remove(); }); + +test("traversing non-elements with attribute filters (#12523)", function() { + expect(5); + + var nonnodes = jQuery("#nonnodes").contents(); + + equal( nonnodes.filter("[id]").length, 1, ".filter" ); + equal( nonnodes.find("[id]").length, 0, ".find" ); + strictEqual( nonnodes.is("[id]"), true, ".is" ); + deepEqual( nonnodes.closest("[id='nonnodes']").get(), q("nonnodes"), ".closest" ); + deepEqual( nonnodes.parents("[id='nonnodes']").get(), q("nonnodes"), ".parents" ); +});