From c192d4086d9bbaf09d5f870857af30c60a427e22 Mon Sep 17 00:00:00 2001 From: Alexander Schmitz Date: Wed, 3 Dec 2014 11:20:20 -0500 Subject: [PATCH] Widget: Add classes option and _add/_remove/_toggleClass methods Fixes #7053 Closes gh-1411 --- tests/unit/accordion/accordion_common.js | 1 + .../unit/autocomplete/autocomplete_common.js | 1 + tests/unit/button/button_common.js | 1 + tests/unit/dialog/dialog_common.js | 1 + tests/unit/draggable/draggable_common.js | 1 + tests/unit/droppable/droppable_common.js | 1 + tests/unit/menu/menu_common.js | 1 + tests/unit/progressbar/progressbar_common.js | 1 + tests/unit/resizable/resizable_common.js | 1 + tests/unit/selectable/selectable_common.js | 1 + tests/unit/selectmenu/selectmenu_common.js | 1 + tests/unit/slider/slider_common.js | 1 + tests/unit/sortable/sortable_common.js | 1 + tests/unit/spinner/spinner_common.js | 1 + tests/unit/tabs/tabs_common.js | 1 + tests/unit/tooltip/tooltip_common.js | 1 + tests/unit/widget/widget.html | 2 + tests/unit/widget/widget_classes.js | 143 ++++++++++++++++++ tests/unit/widget/widget_core.js | 5 +- ui/widget.js | 121 +++++++++++++-- 20 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 tests/unit/widget/widget_classes.js diff --git a/tests/unit/accordion/accordion_common.js b/tests/unit/accordion/accordion_common.js index ef24cf25e..70e04e847 100644 --- a/tests/unit/accordion/accordion_common.js +++ b/tests/unit/accordion/accordion_common.js @@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "accordion", { defaults: { active: 0, animate: {}, + classes: {}, collapsible: false, disabled: false, event: "click", diff --git a/tests/unit/autocomplete/autocomplete_common.js b/tests/unit/autocomplete/autocomplete_common.js index 63b24d384..b82ec1d1c 100644 --- a/tests/unit/autocomplete/autocomplete_common.js +++ b/tests/unit/autocomplete/autocomplete_common.js @@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "autocomplete", { defaults: { appendTo: null, autoFocus: false, + classes: {}, delay: 300, disabled: false, messages: { diff --git a/tests/unit/button/button_common.js b/tests/unit/button/button_common.js index ef22d3011..1564a1bee 100644 --- a/tests/unit/button/button_common.js +++ b/tests/unit/button/button_common.js @@ -1,5 +1,6 @@ TestHelpers.commonWidgetTests( "button", { defaults: { + classes: {}, disabled: null, icons: { primary: null, diff --git a/tests/unit/dialog/dialog_common.js b/tests/unit/dialog/dialog_common.js index fc10fabaa..87f7f7d15 100644 --- a/tests/unit/dialog/dialog_common.js +++ b/tests/unit/dialog/dialog_common.js @@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "dialog", { appendTo: "body", autoOpen: true, buttons: [], + classes: {}, closeOnEscape: true, closeText: "Close", disabled: false, diff --git a/tests/unit/draggable/draggable_common.js b/tests/unit/draggable/draggable_common.js index 5abd09e49..00193bdaf 100644 --- a/tests/unit/draggable/draggable_common.js +++ b/tests/unit/draggable/draggable_common.js @@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "draggable", { appendTo: "parent", axis: false, cancel: "input,textarea,button,select,option", + classes: {}, connectToSortable: false, containment: false, cursor: "auto", diff --git a/tests/unit/droppable/droppable_common.js b/tests/unit/droppable/droppable_common.js index c112def3c..bd56aa35f 100644 --- a/tests/unit/droppable/droppable_common.js +++ b/tests/unit/droppable/droppable_common.js @@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "droppable", { accept: "*", activeClass: false, addClasses: true, + classes: {}, disabled: false, greedy: false, hoverClass: false, diff --git a/tests/unit/menu/menu_common.js b/tests/unit/menu/menu_common.js index ae0ab9887..942e9e9f9 100644 --- a/tests/unit/menu/menu_common.js +++ b/tests/unit/menu/menu_common.js @@ -1,5 +1,6 @@ TestHelpers.commonWidgetTests( "menu", { defaults: { + classes: {}, disabled: false, icons: { submenu: "ui-icon-caret-1-e" diff --git a/tests/unit/progressbar/progressbar_common.js b/tests/unit/progressbar/progressbar_common.js index 0768576f5..c603b4efd 100644 --- a/tests/unit/progressbar/progressbar_common.js +++ b/tests/unit/progressbar/progressbar_common.js @@ -1,5 +1,6 @@ TestHelpers.commonWidgetTests( "progressbar", { defaults: { + classes: {}, disabled: false, max: 100, value: 0, diff --git a/tests/unit/resizable/resizable_common.js b/tests/unit/resizable/resizable_common.js index c261ac5b9..a68103101 100644 --- a/tests/unit/resizable/resizable_common.js +++ b/tests/unit/resizable/resizable_common.js @@ -7,6 +7,7 @@ TestHelpers.commonWidgetTests( "resizable", { aspectRatio: false, autoHide: false, cancel: "input,textarea,button,select,option", + classes: {}, containment: false, delay: 0, disabled: false, diff --git a/tests/unit/selectable/selectable_common.js b/tests/unit/selectable/selectable_common.js index d00a47be5..0f9adf540 100644 --- a/tests/unit/selectable/selectable_common.js +++ b/tests/unit/selectable/selectable_common.js @@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests("selectable", { appendTo: "body", autoRefresh: true, cancel: "input,textarea,button,select,option", + classes: {}, delay: 0, disabled: false, distance: 0, diff --git a/tests/unit/selectmenu/selectmenu_common.js b/tests/unit/selectmenu/selectmenu_common.js index bc245f962..cb8712e7b 100644 --- a/tests/unit/selectmenu/selectmenu_common.js +++ b/tests/unit/selectmenu/selectmenu_common.js @@ -1,6 +1,7 @@ TestHelpers.commonWidgetTests( "selectmenu", { defaults: { appendTo: null, + classes: {}, disabled: null, icons: { button: "ui-icon-triangle-1-s" diff --git a/tests/unit/slider/slider_common.js b/tests/unit/slider/slider_common.js index 6d7278de6..a72071f79 100644 --- a/tests/unit/slider/slider_common.js +++ b/tests/unit/slider/slider_common.js @@ -2,6 +2,7 @@ TestHelpers.commonWidgetTests( "slider", { defaults: { animate: false, cancel: "input,textarea,button,select,option", + classes: {}, delay: 0, disabled: false, distance: 0, diff --git a/tests/unit/sortable/sortable_common.js b/tests/unit/sortable/sortable_common.js index 86850a658..6733714a7 100644 --- a/tests/unit/sortable/sortable_common.js +++ b/tests/unit/sortable/sortable_common.js @@ -3,6 +3,7 @@ TestHelpers.commonWidgetTests( "sortable", { appendTo: "parent", axis: false, cancel: "input,textarea,button,select,option", + classes: {}, connectWith: false, containment: false, cursor: "auto", diff --git a/tests/unit/spinner/spinner_common.js b/tests/unit/spinner/spinner_common.js index b494e3ca3..871af7db4 100644 --- a/tests/unit/spinner/spinner_common.js +++ b/tests/unit/spinner/spinner_common.js @@ -1,5 +1,6 @@ TestHelpers.commonWidgetTests( "spinner", { defaults: { + classes: {}, culture: null, disabled: false, icons: { diff --git a/tests/unit/tabs/tabs_common.js b/tests/unit/tabs/tabs_common.js index a589515f8..8f6bbc67b 100644 --- a/tests/unit/tabs/tabs_common.js +++ b/tests/unit/tabs/tabs_common.js @@ -1,6 +1,7 @@ TestHelpers.commonWidgetTests( "tabs", { defaults: { active: null, + classes: {}, collapsible: false, disabled: false, event: "click", diff --git a/tests/unit/tooltip/tooltip_common.js b/tests/unit/tooltip/tooltip_common.js index 2d6ea91ec..0611d724c 100644 --- a/tests/unit/tooltip/tooltip_common.js +++ b/tests/unit/tooltip/tooltip_common.js @@ -1,5 +1,6 @@ TestHelpers.commonWidgetTests( "tooltip", { defaults: { + classes: {}, content: function() {}, disabled: false, hide: true, diff --git a/tests/unit/widget/widget.html b/tests/unit/widget/widget.html index 2b764abab..ac106ea32 100644 --- a/tests/unit/widget/widget.html +++ b/tests/unit/widget/widget.html @@ -9,6 +9,7 @@ + + diff --git a/tests/unit/widget/widget_classes.js b/tests/unit/widget/widget_classes.js new file mode 100644 index 000000000..0202d2683 --- /dev/null +++ b/tests/unit/widget/widget_classes.js @@ -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 = $( "" ) + .appendTo( this.element ); + + this.element.wrap( "
" ); + + 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 ) ); diff --git a/tests/unit/widget/widget_core.js b/tests/unit/widget/widget_core.js index 2b88e39ef..4e5b02349 100644 --- a/tests/unit/widget/widget_core.js +++ b/tests/unit/widget/widget_core.js @@ -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"; diff --git a/ui/widget.js b/ui/widget.js index 33e0d156e..4999048f6 100644 --- a/ui/widget.js +++ b/ui/widget.js @@ -250,6 +250,7 @@ $.Widget.prototype = { widgetEventPrefix: "", defaultElement: "
", 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" ); } }); }, -- 2.39.5