]> source.dussan.org Git - jquery-ui.git/commitdiff
Widget: Add classes option and _add/_remove/_toggleClass methods
authorAlexander Schmitz <arschmitz@gmail.com>
Wed, 3 Dec 2014 16:20:20 +0000 (11:20 -0500)
committerAlexander Schmitz <arschmitz@gmail.com>
Wed, 11 Mar 2015 20:00:04 +0000 (16:00 -0400)
Fixes #7053
Closes gh-1411

20 files changed:
tests/unit/accordion/accordion_common.js
tests/unit/autocomplete/autocomplete_common.js
tests/unit/button/button_common.js
tests/unit/dialog/dialog_common.js
tests/unit/draggable/draggable_common.js
tests/unit/droppable/droppable_common.js
tests/unit/menu/menu_common.js
tests/unit/progressbar/progressbar_common.js
tests/unit/resizable/resizable_common.js
tests/unit/selectable/selectable_common.js
tests/unit/selectmenu/selectmenu_common.js
tests/unit/slider/slider_common.js
tests/unit/sortable/sortable_common.js
tests/unit/spinner/spinner_common.js
tests/unit/tabs/tabs_common.js
tests/unit/tooltip/tooltip_common.js
tests/unit/widget/widget.html
tests/unit/widget/widget_classes.js [new file with mode: 0644]
tests/unit/widget/widget_core.js
ui/widget.js

index ef24cf25ec5178678dfe471d495bb626f8c2c3eb..70e04e8475bb7ce1bc92dcaa8a780eb0b1f8557a 100644 (file)
@@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "accordion", {
        defaults: {
                active: 0,
                animate: {},
+               classes: {},
                collapsible: false,
                disabled: false,
                event: "click",
index 63b24d384b93937a93b2f1baebfa878c7a42bc9a..b82ec1d1c501ea40133852bbac8d85888b61720b 100644 (file)
@@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "autocomplete", {
        defaults: {
                appendTo: null,
                autoFocus: false,
+               classes: {},
                delay: 300,
                disabled: false,
                messages: {
index ef22d30114fa6b663fac6a617fe5e36780f21887..1564a1beeee6bc488e781da13e084df87270b9d6 100644 (file)
@@ -1,5 +1,6 @@
 TestHelpers.commonWidgetTests( "button", {
        defaults: {
+               classes: {},
                disabled: null,
                icons: {
                        primary: null,
index fc10fabaab6fb8a8956751f2b672c8d8e5ba876b..87f7f7d15783834721e0bf338f50258ad260fffc 100644 (file)
@@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "dialog", {
                appendTo: "body",
                autoOpen: true,
                buttons: [],
+               classes: {},
                closeOnEscape: true,
                closeText: "Close",
                disabled: false,
index 5abd09e49354bb8f351ab5672d1e0b8940c0522a..00193bdaf4febc5bf71dbd9b28ae9feed9396bd7 100644 (file)
@@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "draggable", {
                appendTo: "parent",
                axis: false,
                cancel: "input,textarea,button,select,option",
+               classes: {},
                connectToSortable: false,
                containment: false,
                cursor: "auto",
index c112def3cf437128b1f8a3c13e421188b6750824..bd56aa35f9b3867b3953c8da13fb029cc6ba0af9 100644 (file)
@@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "droppable", {
                accept: "*",
                activeClass: false,
                addClasses: true,
+               classes: {},
                disabled: false,
                greedy: false,
                hoverClass: false,
index ae0ab9887855d8a0c03b94579ed6e95cc51b9827..942e9e9f954e95dca38da5ac3b94b4d035eca7e6 100644 (file)
@@ -1,5 +1,6 @@
 TestHelpers.commonWidgetTests( "menu", {
        defaults: {
+               classes: {},
                disabled: false,
                icons: {
                        submenu: "ui-icon-caret-1-e"
index 0768576f568b551fd6870b8ad7500b962a2954a7..c603b4efd2b3ee2deaec349cb77b654b2fb86fbe 100644 (file)
@@ -1,5 +1,6 @@
 TestHelpers.commonWidgetTests( "progressbar", {
        defaults: {
+               classes: {},
                disabled: false,
                max: 100,
                value: 0,
index c261ac5b9c895d030827f8458dd25d374a66147e..a68103101bc2bebe88d697f6307a39a1b870e7fe 100644 (file)
@@ -7,6 +7,7 @@ TestHelpers.commonWidgetTests( "resizable", {
                aspectRatio: false,
                autoHide: false,
                cancel: "input,textarea,button,select,option",
+               classes: {},
                containment: false,
                delay: 0,
                disabled: false,
index d00a47be59386d0fa74790759919282220d4c932..0f9adf5401815d592ddde353a435ee25a4abdf33 100644 (file)
@@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests("selectable", {
                appendTo: "body",
                autoRefresh: true,
                cancel: "input,textarea,button,select,option",
+               classes: {},
                delay: 0,
                disabled: false,
                distance: 0,
index bc245f9626647e75f86dbd1f517ac5194ceff612..cb8712e7b5abe0efff8575976f5de0d96c256fea 100644 (file)
@@ -1,6 +1,7 @@
 TestHelpers.commonWidgetTests( "selectmenu", {
        defaults: {
                appendTo: null,
+               classes: {},
                disabled: null,
                icons: {
                        button: "ui-icon-triangle-1-s"
index 6d7278de6a2f63fcaa3185c4dbf7c675d0c49228..a72071f793dfaafa6d998b6475c1fe49e5cf0ea2 100644 (file)
@@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "slider", {
        defaults: {
                animate: false,
                cancel: "input,textarea,button,select,option",
+               classes: {},
                delay: 0,
                disabled: false,
                distance: 0,
index 86850a658c3c77bb100e122664e50df3d97ec645..6733714a755e945151facfdc98a90abbebfa0b60 100644 (file)
@@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "sortable", {
                appendTo: "parent",
                axis: false,
                cancel: "input,textarea,button,select,option",
+               classes: {},
                connectWith: false,
                containment: false,
                cursor: "auto",
index b494e3ca34d0960c025d7ba1464acda544e29ff9..871af7db447d159e5a6160f81c54dc69ad564811 100644 (file)
@@ -1,5 +1,6 @@
 TestHelpers.commonWidgetTests( "spinner", {
        defaults: {
+               classes: {},
                culture: null,
                disabled: false,
                icons: {
index a589515f8067682165424a6a81d4832277810af0..8f6bbc67bc9a66b2757df16b90482ce774c4b924 100644 (file)
@@ -1,6 +1,7 @@
 TestHelpers.commonWidgetTests( "tabs", {
        defaults: {
                active: null,
+               classes: {},
                collapsible: false,
                disabled: false,
                event: "click",
index 2d6ea91eccd98fb2743d3049ec9acdeed1ff9999..0611d724cecd3da20d7f8e4278d254e0d4b39bfd 100644 (file)
@@ -1,5 +1,6 @@
 TestHelpers.commonWidgetTests( "tooltip", {
        defaults: {
+               classes: {},
                content: function() {},
                disabled: false,
                hide: true,
index 2b764abab9e41a3fe8f004e77350308f41e40e07..ac106ea32949ec3149b7fa9b0c77d1010167703f 100644 (file)
@@ -9,6 +9,7 @@
        <script src="../../../external/qunit/qunit.js"></script>
        <script src="../../../external/jquery-simulate/jquery.simulate.js"></script>
        <script src="../testsuite.js"></script>
+       <script src="../../../external/qunit-assert-classes/qunit-assert-classes.js"></script>
        <script>
        TestHelpers.loadResources({
                css: [ "core" ],
@@ -21,6 +22,7 @@
        <script src="widget_core.js"></script>
        <script src="widget_extend.js"></script>
        <script src="widget_animation.js"></script>
+       <script src="widget_classes.js"></script>
 
        <script src="../swarminject.js"></script>
 </head>
diff --git a/tests/unit/widget/widget_classes.js b/tests/unit/widget/widget_classes.js
new file mode 100644 (file)
index 0000000..0202d26
--- /dev/null
@@ -0,0 +1,143 @@
+(function( $ ) {
+
+module( "widget factory classes", {
+       setup: function() {
+               $.widget( "ui.classesWidget", {
+                       options: {
+                               classes: {
+                                       "ui-classes-widget": "ui-theme-widget",
+                                       "ui-classes-element": "ui-theme-element ui-theme-element-2"
+                               }
+                       },
+                       _create: function() {
+                               this.span = $( "<span>" )
+                                       .appendTo( this.element );
+
+                               this.element.wrap( "<div>" );
+
+                               this.wrapper = this.element.parent();
+
+                               this._addClass( "ui-classes-element", "ui-core-element" )
+                                       ._addClass( "ui-classes-element-2" )
+                                       ._addClass( null, "ui-core-element-null" )
+                                       ._addClass( this.span, null, "ui-core-span-null" )
+                                       ._addClass( this.span, "ui-classes-span", "ui-core-span" )
+                                       ._addClass( this.wrapper, "ui-classes-widget" );
+
+                       },
+                       toggleClasses: function( bool ) {
+                               this._toggleClass( "ui-classes-element", "ui-core-element", bool )
+                                       ._toggleClass( "ui-classes-element-2", null, bool )
+                                       ._toggleClass( null, "ui-core-element-null", bool )
+                                       ._toggleClass( this.span, null, "ui-core-span-null", bool )
+                                       ._toggleClass( this.span, "ui-classes-span", "ui-core-span", bool )
+                                       ._toggleClass( this.wrapper, "ui-classes-widget", null, bool );
+                       },
+                       removeClasses: function() {
+                               this._removeClass( "ui-classes-element", "ui-core-element" )
+                                       ._removeClass( "ui-classes-element-2" )
+                                       ._removeClass(  null, "ui-core-element-null"  )
+                                       ._removeClass( this.span, null, "ui-core-span-null" )
+                                       ._removeClass( this.span, "ui-classes-span", "ui-core-span" )
+                                       ._removeClass( this.wrapper, "ui-classes-widget" );
+                       },
+                       _destroy: function() {
+                               this.span.remove();
+                               this.element.unwrap();
+                       }
+               });
+       },
+       teardown: function() {
+               delete $.ui.classesWidget;
+               delete $.fn.classesWidget;
+       }
+});
+
+function elementHasClasses( widget, method, assert ) {
+       var toggle = method === "toggle" ? ( ", true" ) : "";
+
+       assert.hasClasses( widget, "ui-classes-element ui-theme-element ui-theme-element-2",
+               "_" + method + "Class works with ( keys, extra" + toggle + " )" );
+       assert.hasClasses( widget, "ui-classes-element-2",
+               "_" + method + "Class works with ( keys, null" + toggle + " )" );
+       assert.hasClasses( widget, "ui-core-element-null",
+               "_" + method + "Class works with ( null, extra" + toggle + " )" );
+       assert.hasClasses( widget.parent(), "ui-classes-widget ui-theme-widget",
+               "_" + method + "Class works with ( element, null, extra" + toggle + " )" );
+       assert.hasClasses( widget.find( "span" ), "ui-classes-span ui-core-span",
+               "_" + method + "Class works with ( element, keys, extra" + toggle + " )" );
+       assert.hasClasses( widget.find( "span" ), "ui-core-span-null",
+               "_" + method + "Class works with ( element, keys, null" + toggle + " )" );
+}
+function elementLacksClasses( widget, method, assert ) {
+       var toggle = method === "toggle" ? ( ", false" ) : "";
+
+       assert.lacksClasses( widget, "ui-classes-element ui-theme-element ui-theme-element-2",
+               "_" + method + "Class works with ( keys, extra" + toggle + " )" );
+       assert.lacksClasses( widget, "ui-classes-element-2",
+               "_" + method + "Class works with ( keys, null" + toggle + " )" );
+       assert.lacksClasses( widget, "ui-core-element-null",
+               "_" + method + "Class works with ( null, extra" + toggle + " )" );
+       assert.lacksClasses( widget.parent(), "ui-classes-widget ui-theme-widget",
+               "_" + method + "Class works with ( element, null, extra" + toggle + " )" );
+       assert.lacksClasses( widget.find( "span" ), "ui-classes-span ui-core-span",
+               "_" + method + "Class works with ( element, keys, extra" + toggle + " )" );
+       assert.lacksClasses( widget.find( "span" ), "ui-core-span-null",
+               "_" + method + "Class works with ( element, keys, null" + toggle + " )" );
+}
+
+test( ".option() - classes setter", function( assert ) {
+       expect( 11 );
+
+       var testWidget = $.ui.classesWidget();
+
+       elementHasClasses( testWidget.element, "add", assert );
+
+       testWidget.option({
+               classes: {
+                       "ui-classes-span": "custom-theme-span",
+                       "ui-classes-widget": "ui-theme-widget custom-theme-widget",
+                       "ui-classes-element": "ui-theme-element-2"
+               }
+       });
+
+       assert.lacksClasses( testWidget.element, "ui-theme-element",
+               "Removing a class from the value removes the class" );
+
+       testWidget.option( "classes.ui-classes-element", "" );
+       assert.hasClasses( testWidget.element, "ui-classes-element",
+               "Setting to empty value leaves structure class" );
+       assert.lacksClasses( testWidget.element, "ui-theme-element-2",
+               "Setting empty value removes previous value classes" );
+       assert.hasClasses( testWidget.span, "ui-classes-span custom-theme-span",
+               "Adding a class to an empty value works as expected" );
+       assert.hasClasses( testWidget.wrapper, "ui-classes-widget custom-theme-widget",
+               "Appending a class to the current value works as expected" );
+});
+
+test( ".destroy() - class removal", function() {
+       expect( 1 );
+
+       domEqual( "#widget", function() {
+               $( "#widget" ).classesWidget().classesWidget( "destroy" );
+       });
+});
+
+test( "._add/_remove/_toggleClass()", function( assert ) {
+       expect( 24 );
+
+       var widget = $( "#widget" ).classesWidget();
+
+       elementHasClasses( widget, "add", assert );
+
+       widget.classesWidget( "toggleClasses", false );
+       elementLacksClasses( widget, "toggle", assert );
+
+       widget.classesWidget( "toggleClasses", true );
+       elementHasClasses( widget, "toggle", assert );
+
+       widget.classesWidget( "removeClasses" );
+       elementLacksClasses( widget, "remove", assert );
+});
+
+}( jQuery ) );
index 2b88e39ef222df221ddb6a1a2fe7798f4fb24464..4e5b023490a65d3c9ec06de3aac7bcb9eeba3e50 100644 (file)
@@ -227,6 +227,7 @@ test( "merge multiple option arguments", function() {
        $.widget( "ui.testWidget", {
                _create: function() {
                        deepEqual( this.options, {
+                               classes: {},
                                create: null,
                                disabled: false,
                                option1: "value1",
@@ -281,6 +282,7 @@ test( "._getCreateOptions()", function() {
                },
                _create: function() {
                        deepEqual( this.options, {
+                               classes: {},
                                create: null,
                                disabled: false,
                                option1: "override1",
@@ -485,10 +487,11 @@ test( ".option() - getter", function() {
 
        options = div.testWidget( "option" );
        deepEqual( options, {
+               baz: 5,
+               classes: {},
                create: null,
                disabled: false,
                foo: "bar",
-               baz: 5,
                qux: [ "quux", "quuux" ]
        }, "full options hash returned" );
        options.foo = "notbar";
index 33e0d156e4d407d5dfcf70abd6f96c53b703e801..4999048f6f0ccab0779d9b09f42ac7e9d2692fba 100644 (file)
@@ -250,6 +250,7 @@ $.Widget.prototype = {
        widgetEventPrefix: "",
        defaultElement: "<div>",
        options: {
+               classes: {},
                disabled: false,
 
                // callbacks
@@ -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,13 @@ $.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 +313,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,
 
@@ -370,21 +373,54 @@ $.Widget.prototype = {
                return this;
        },
        _setOption: function( key, value ) {
+               if ( key === "classes" ) {
+                       this._setOptionClasses( 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" );
                        }
                }
 
                return this;
        },
+       _setOptionClasses: 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;
+                       }
+
+                       // We are doing this to create a new jQuery object because the _removeClass() call
+                       // on the next line is going to destroy the reference to the current elements being
+                       // tracked. We need to save a copy of this collection so that we can add the new classes
+                       // below.
+                       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 the value passed in from
+                       // _setOption(), this is the new value of the classes option which was passed to
+                       // _setOption(). We pass this value directly to _classes().
+                       elements.addClass( this._classes({
+                               element: elements,
+                               keys: classKey,
+                               classes: value,
+                               add: true
+                       }));
+               }
+       },
 
        enable: function() {
                return this._setOptions({ disabled: false });
@@ -393,6 +429,63 @@ $.Widget.prototype = {
                return this._setOptions({ disabled: true });
        },
 
+       _classes: function( options ) {
+               var full = [],
+                       that = this;
+
+               options = $.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 ( options.add ) {
+                                       current = $( $.unique( current.get().concat( options.element.get() ) ) );
+                               } else {
+                                       current = $( current.not( options.element ).get() );
+                               }
+                               that.classesElementLookup[ classes[ i ] ] = current;
+                               full.push( classes[ i ] );
+                               if ( checkOption && options.classes[ classes[ i ] ] ) {
+                                       full.push( options.classes[ classes[ i ] ] );
+                               }
+                       }
+               }
+
+               if ( options.keys ) {
+                       processClassString( options.keys.match( /\S+/g ) || [], true );
+               }
+               if ( options.extra ) {
+                       processClassString( options.extra.match( /\S+/g ) || [] );
+               }
+
+               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;
@@ -469,10 +562,10 @@ $.Widget.prototype = {
                this.hoverable = this.hoverable.add( element );
                this._on( element, {
                        mouseenter: function( event ) {
-                               $( event.currentTarget ).addClass( "ui-state-hover" );
+                               this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
                        },
                        mouseleave: function( event ) {
-                               $( event.currentTarget ).removeClass( "ui-state-hover" );
+                               this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
                        }
                });
        },
@@ -481,10 +574,10 @@ $.Widget.prototype = {
                this.focusable = this.focusable.add( element );
                this._on( element, {
                        focusin: function( event ) {
-                               $( event.currentTarget ).addClass( "ui-state-focus" );
+                               this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
                        },
                        focusout: function( event ) {
-                               $( event.currentTarget ).removeClass( "ui-state-focus" );
+                               this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
                        }
                });
        },