From a8641722e6b515607e549d892b2b198ed6cd1de9 Mon Sep 17 00:00:00 2001
From: Alexander Schmitz
Date: Mon, 2 Feb 2015 19:21:44 -0500
Subject: [PATCH 001/107] Build: Add qunit-assert-classes plug-in for classes
tests
---
Gruntfile.js | 3 ++
bower.json | 1 +
external/qunit-assert-classes/LICENSE.txt | 22 +++++++++
.../qunit-assert-classes.js | 47 +++++++++++++++++++
4 files changed, 73 insertions(+)
create mode 100644 external/qunit-assert-classes/LICENSE.txt
create mode 100644 external/qunit-assert-classes/qunit-assert-classes.js
diff --git a/Gruntfile.js b/Gruntfile.js
index 277169a7217..b08547b256c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -229,6 +229,9 @@ grunt.initConfig({
"qunit/qunit.css": "qunit/qunit/qunit.css",
"qunit/MIT-LICENSE.txt": "qunit/MIT-LICENSE.txt",
+ "qunit-assert-classes/qunit-assert-classes.js": "qunit-assert-classes/qunit-assert-classes.js",
+ "qunit-assert-classes/LICENSE.txt": "qunit-assert-classes/LICENSE",
+
"jquery-mousewheel/jquery.mousewheel.js": "jquery-mousewheel/jquery.mousewheel.js",
"jquery-mousewheel/LICENSE.txt": "jquery-mousewheel/LICENSE.txt",
diff --git a/bower.json b/bower.json
index cab4b258e5b..29f884f64d7 100644
--- a/bower.json
+++ b/bower.json
@@ -15,6 +15,7 @@
"jquery-simulate": "1.0.0",
"jshint": "2.4.4",
"qunit": "1.17.1",
+ "qunit-assert-classes": "0.1.5",
"jquery-1.7.0": "jquery#1.7.0",
"jquery-1.7.1": "jquery#1.7.1",
diff --git a/external/qunit-assert-classes/LICENSE.txt b/external/qunit-assert-classes/LICENSE.txt
new file mode 100644
index 00000000000..938db036815
--- /dev/null
+++ b/external/qunit-assert-classes/LICENSE.txt
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Alexander Schmitz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/external/qunit-assert-classes/qunit-assert-classes.js b/external/qunit-assert-classes/qunit-assert-classes.js
new file mode 100644
index 00000000000..f61046bc85b
--- /dev/null
+++ b/external/qunit-assert-classes/qunit-assert-classes.js
@@ -0,0 +1,47 @@
+( function( QUnit ) {
+ function inArray( haystack, needle ) {
+ for ( var i = 0; i < haystack.length; i++ ) {
+ if ( haystack[ i ] === needle ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function check( element, classes, stateVal, message ) {
+ var i, result, classAttribute, elementClassArray,
+ classArray = classes.split( " " ),
+ missing = [],
+ found = [];
+
+ if ( element.jquery && element.length !== 1 ) {
+ throw( "Class checks can only be performed on a single element on a collection" );
+ }
+ element = element.jquery ? element[ 0 ] : element;
+ classAttribute = element.getAttribute( "class" );
+ message = message || "Element must " + ( stateVal? "" : "not " ) + "have classes";
+ if ( classAttribute ) {
+ elementClassArray = classAttribute.split( " " );
+ for( i = 0; i < classArray.length; i++ ) {
+ if ( !inArray( elementClassArray, classArray[ i ] ) ) {
+ missing.push( classArray[ i ] );
+ } else {
+ found.push( classArray[ i ] );
+ }
+ }
+ } else {
+ missing = classArray;
+ }
+
+ result = stateVal ? !missing.length : !found.length;
+ QUnit.push( result, classes, result ? classes : found.join( " " ), message );
+ }
+
+ QUnit.extend( QUnit.assert, {
+ hasClasses: function( element, classes, message ) {
+ check( element, classes, true, message );
+ },
+ lacksClasses: function( element, classes, message ) {
+ check( element, classes, false, message );
+ }
+ });
+})( QUnit );
\ No newline at end of file
From 2f20d0ec42bd4b53f1ff82d1b46ed7b0696d60cd Mon Sep 17 00:00:00 2001
From: Alexander Schmitz
Date: Wed, 3 Dec 2014 11:20:20 -0500
Subject: [PATCH 002/107] Widget: Add classes option and
_add/_remove/_toggleClass methods
---
tests/unit/widget/widget.html | 1 +
tests/unit/widget/widget_classes.js | 129 ++++++++++++++++++++++++++++
tests/unit/widget/widget_core.js | 3 +
ui/widget.js | 115 ++++++++++++++++++++++---
4 files changed, 234 insertions(+), 14 deletions(-)
create mode 100644 tests/unit/widget/widget_classes.js
diff --git a/tests/unit/widget/widget.html b/tests/unit/widget/widget.html
index 2b764abab9e..1738b12aa07 100644
--- a/tests/unit/widget/widget.html
+++ b/tests/unit/widget/widget.html
@@ -21,6 +21,7 @@
+
diff --git a/tests/unit/widget/widget_classes.js b/tests/unit/widget/widget_classes.js
new file mode 100644
index 00000000000..9a801c91bae
--- /dev/null
+++ b/tests/unit/widget/widget_classes.js
@@ -0,0 +1,129 @@
+(function( $ ) {
+
+module( "widget factory classes", {
+ teardown: function() {
+ if ( $.ui ) {
+ delete $.ui.classWidget;
+ delete $.fn.classWidget;
+ }
+ }
+});
+
+$.widget( "ui.classesWidget", {
+ options: {
+ classes: {
+ "test-wrapper": "self-wrapper",
+ "test-self": "self-class self-class-2"
+ }
+ },
+ _create: function() {
+ this.span = $( "" )
+ .appendTo( this.element );
+
+ this.element.wrap( "" );
+
+ this.wrapper = this.element.parent();
+
+ this._addClass( "test-self", "test-self-extra" )
+ ._addClass( "test-self-2" )
+ ._addClass( null, "test-self-extra-null" )
+ ._addClass( this.span, null, "test-span-extra-null" )
+ ._addClass( this.span, "test-span", "test-span-extra" )
+ ._addClass( this.wrapper, "test-wrapper" );
+
+ },
+ toggleClasses: function( bool ) {
+ this._toggleClass( "test-self", "test-self-extra", bool )
+ ._toggleClass( "test-self-2", null, bool )
+ ._toggleClass( null, "test-self-extra-null", bool )
+ ._toggleClass( this.span, null, "test-span-extra-null", bool )
+ ._toggleClass( this.span, "test-span", "test-span-extra", bool )
+ ._toggleClass( this.wrapper, "test-wrapper", null, bool );
+ },
+ removeClasses: function() {
+ this._removeClass( "test-self", "test-self-extra" )
+ ._removeClass( "test-self-2" )
+ ._removeClass( null, "test-self-extra-null" )
+ ._removeClass( this.span, null, "test-span-extra-null" )
+ ._removeClass( this.span, "test-span", "test-span-extra" )
+ ._removeClass( this.wrapper, "test-wrapper" );
+ },
+ _destroy: function() {
+ this.span.remove();
+ this.element.unwrap();
+ },
+ widget: function() {
+ return this.wrapper;
+ }
+});
+
+test( ".option() - classes setter", function(){
+ expect( 12 );
+
+ var testWidget = $.ui.classesWidget(),
+ currentWrapperClass = testWidget.option( "classes.test-wrapper" );
+
+ testElementClasses( testWidget.element, true, "add" );
+
+ testWidget.option({
+ classes: {
+ "test-span": "self-span-new",
+ "test-wrapper": currentWrapperClass + " self-wrapper-new",
+ "test-self": "self-class-2"
+ }
+ });
+
+ equal( testWidget.element.is( ".test-self.self-class-2" ), true,
+ "Removing a class leaves the structure and other classes in value" );
+ equal( !testWidget.element.is( ".self-class" ), true,
+ "Removing a class from the value removes the class" );
+ testWidget.option( "classes.test-self", "" );
+ equal( testWidget.element.is( ".test-self" ), true,
+ "Setting to empty value leaves structure class" );
+ equal( !testWidget.element.is( ".self-class-2" ), true,
+ "Setting empty value removes previous value classes" );
+ equal( testWidget.span.is( ".test-span.self-span-new" ), true,
+ "Adding a class to an empty value works as expected" );
+ equal( testWidget.wrapper.is( ".test-wrapper.self-wrapper-new" ), true,
+ "Appending a class to the current value works as expected" );
+});
+
+test( ".destroy() - class removal", function(){
+ expect( 1 );
+
+ domEqual( "#widget", function(){
+ $( "#widget" ).classesWidget().classesWidget( "destroy" );
+ });
+});
+
+function testElementClasses( widget, bool, method, toggle ) {
+ toggle = toggle || "";
+ equal( widget.is( ".test-self.self-class.self-class-2" ), bool,
+ "_" + method + "Class works with ( keys, extra " + toggle + ")" );
+ equal( widget.is( ".test-self-2" ), bool,
+ "_" + method + "Class works with ( keys, null " + toggle + ")" );
+ equal( widget.is( ".test-self-extra-null" ), bool,
+ "_" + method + "Class works with ( null, extra " + toggle + ")" );
+ equal( widget.parent().is( ".test-wrapper.self-wrapper" ), bool,
+ "_" + method + "Class works with ( element, null, extra " + toggle + ")" );
+ equal( widget.find( "span" ).is( ".test-span.test-span-extra" ), bool,
+ "_" + method + "Class works with ( element, keys, extra " + toggle + ")" );
+ equal( widget.find( "span" ).is( ".test-span-extra-null" ), bool,
+ "_" + method + "Class works with ( element, keys, null " + toggle + ")" );
+}
+
+test( "._add/_remove/_toggleClass()", function(){
+ expect( 24 );
+
+ var widget = $( "#widget" ).classesWidget();
+
+ testElementClasses( widget, true, "add" );
+ widget.classesWidget( "toggleClasses", false );
+ testElementClasses( widget, false, "toggle", ", false ");
+ widget.classesWidget( "toggleClasses", true );
+ testElementClasses( widget, true, "toggle", ", true ");
+ widget.classesWidget( "removeClasses" );
+ testElementClasses( widget, false, "remove" );
+});
+
+}( jQuery ) );
diff --git a/tests/unit/widget/widget_core.js b/tests/unit/widget/widget_core.js
index 2b88e39ef22..4059447da3d 100644
--- a/tests/unit/widget/widget_core.js
+++ b/tests/unit/widget/widget_core.js
@@ -228,6 +228,7 @@ test( "merge multiple option arguments", function() {
_create: function() {
deepEqual( this.options, {
create: null,
+ classes: {},
disabled: false,
option1: "value1",
option2: "value2",
@@ -282,6 +283,7 @@ test( "._getCreateOptions()", function() {
_create: function() {
deepEqual( this.options, {
create: null,
+ classes: {},
disabled: false,
option1: "override1",
option2: "value2",
@@ -486,6 +488,7 @@ test( ".option() - getter", function() {
options = div.testWidget( "option" );
deepEqual( options, {
create: null,
+ classes: {},
disabled: false,
foo: "bar",
baz: 5,
diff --git a/ui/widget.js b/ui/widget.js
index 33e0d156e4d..348a7795d65 100644
--- a/ui/widget.js
+++ b/ui/widget.js
@@ -251,6 +251,7 @@ $.Widget.prototype = {
defaultElement: "
",
options: {
disabled: false,
+ classes: {},
// callbacks
create: null
@@ -264,6 +265,7 @@ $.Widget.prototype = {
this.bindings = $();
this.hoverable = $();
this.focusable = $();
+ this.classesElementLookup = {};
if ( element !== this ) {
$.data( element, this.widgetFullName, this );
@@ -297,7 +299,12 @@ $.Widget.prototype = {
_init: $.noop,
destroy: function() {
+ var that = this;
this._destroy();
+ $.each( this.classesElementLookup, function( key, value ) {
+ that._removeClass( value, key );
+ });
+
// we can probably remove the unbind calls in 2.0
// all event bindings should go through this._on()
this.element
@@ -305,15 +312,10 @@ $.Widget.prototype = {
.removeData( this.widgetFullName );
this.widget()
.unbind( this.eventNamespace )
- .removeAttr( "aria-disabled" )
- .removeClass(
- this.widgetFullName + "-disabled " +
- "ui-state-disabled" );
+ .removeAttr( "aria-disabled" );
// clean up events and states
this.bindings.unbind( this.eventNamespace );
- this.hoverable.removeClass( "ui-state-hover" );
- this.focusable.removeClass( "ui-state-focus" );
},
_destroy: $.noop,
@@ -369,17 +371,44 @@ $.Widget.prototype = {
return this;
},
+ _updateClassesOption: function( value ) {
+ var classKey, elements, currentElements;
+ for ( classKey in value ) {
+ currentElements = this.classesElementLookup[ classKey ];
+ if ( value[ classKey ] === this.options.classes[ classKey ] ||
+ !currentElements ||
+ !currentElements.length ) {
+ continue;
+ }
+ elements = $( currentElements.get() );
+ this._removeClass( currentElements, classKey );
+
+ // We don't use _addClass here because that uses this.options.classes
+ // for generating the string of classes we want to use value is the new
+ // value for classes which was passed to _setOption so we call _classes
+ // directly to pass in this value.
+ elements.addClass( this._classes({
+ element: elements,
+ keys: classKey,
+ classes: value,
+ add: true
+ }));
+ }
+ },
_setOption: function( key, value ) {
+ if ( key === "classes" ) {
+ this._updateClassesOption( value );
+ }
+
this.options[ key ] = value;
if ( key === "disabled" ) {
- this.widget()
- .toggleClass( this.widgetFullName + "-disabled", !!value );
+ this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );
// If the widget is becoming disabled, then nothing is interactive
if ( value ) {
- this.hoverable.removeClass( "ui-state-hover" );
- this.focusable.removeClass( "ui-state-focus" );
+ this._removeClass( this.hoverable, null, "ui-state-hover" );
+ this._removeClass( this.focusable, null, "ui-state-focus" );
}
}
@@ -393,6 +422,62 @@ $.Widget.prototype = {
return this._setOptions({ disabled: true });
},
+ _classes: function( options ) {
+ var full = [],
+ that = this,
+ settings = $.extend({
+ element: this.element,
+ classes: this.options.classes || {}
+ }, options );
+
+ function processClassString( classes, checkOption ) {
+ var current, i;
+ for ( i = 0; i < classes.length; i++ ) {
+ current = that.classesElementLookup[ classes[ i ] ] || $();
+ if ( settings.add ) {
+ current = $( $.unique( current.get().concat( settings.element.get() ) ) );
+ } else {
+ current = $( current.not( settings.element ).get() );
+ }
+ that.classesElementLookup[ classes[ i ] ] = current;
+ full.push( classes[ i ] );
+ if ( checkOption && settings.classes[ classes[ i ] ] ) {
+ full.push( settings.classes[ classes[ i ] ] );
+ }
+ }
+ }
+
+ if ( settings.keys ) {
+ processClassString( settings.keys.split( " " ), true );
+ }
+ if ( settings.extra ) {
+ processClassString( settings.extra.split( " " ) );
+ }
+
+ return full.join( " " );
+ },
+
+ _removeClass: function( element, keys, extra ) {
+ return this._toggleClass( element, keys, extra, false );
+ },
+
+ _addClass: function( element, keys, extra ) {
+ return this._toggleClass( element, keys, extra, true );
+ },
+
+ _toggleClass: function( element, keys, extra, add ) {
+ add = ( typeof add === "boolean" ) ? add : extra;
+ var shift = ( typeof element === "string" || element === null ),
+ options = {
+ extra: shift ? keys : extra,
+ keys: shift ? element : keys,
+ element: shift ? this.element : element,
+ add: add
+ };
+ options.element.toggleClass( this._classes( options ), add );
+ return this;
+ },
+
_on: function( suppressDisabledCheck, element, handlers ) {
var delegateElement,
instance = this;
@@ -466,25 +551,27 @@ $.Widget.prototype = {
},
_hoverable: function( element ) {
+ var that = this;
this.hoverable = this.hoverable.add( element );
this._on( element, {
mouseenter: function( event ) {
- $( event.currentTarget ).addClass( "ui-state-hover" );
+ that._addClass( $( event.currentTarget ), null, "ui-state-hover" );
},
mouseleave: function( event ) {
- $( event.currentTarget ).removeClass( "ui-state-hover" );
+ that._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
}
});
},
_focusable: function( element ) {
+ var that = this;
this.focusable = this.focusable.add( element );
this._on( element, {
focusin: function( event ) {
- $( event.currentTarget ).addClass( "ui-state-focus" );
+ that._addClass( $( event.currentTarget ), null, "ui-state-focus" );
},
focusout: function( event ) {
- $( event.currentTarget ).removeClass( "ui-state-focus" );
+ that._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
}
});
},
From 11d25df102b9456c6e3c7dc7c14cd35303be52f1 Mon Sep 17 00:00:00 2001
From: Alexander Schmitz
Date: Mon, 2 Feb 2015 20:27:31 -0500
Subject: [PATCH 003/107] Button: Add classes option to please the tests
---
ui/button.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/button.js b/ui/button.js
index a3a40b46d58..f4a8430cc65 100644
--- a/ui/button.js
+++ b/ui/button.js
@@ -58,6 +58,7 @@ var lastActive,
$.widget( "ui.button", {
version: "@VERSION",
+ classes: {},
defaultElement: "