Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

On-the-fly options #3820

Closed
wants to merge 7 commits into from

3 participants

@gabrielschulhof
Collaborator

Makes a whole bunch of options work on-the-fly.

This is done in the following way:

  1. In $.mobile.widget, implement _setOption in such a way that it searches for a function _set which performs the actual setting. If not found, it calls $.ui.widget._setOption.
  2. If the function is found, it is assumed that it will correctly update the widget to reflect the new value of the option, and the option value will be set in the widget element's data-option-name attribute as well.

The PR implements the _set functions for a lot of the widgets, and adds a functional test for a bunch of widgets. Not all widgets are covered yet.

@gabrielschulhof
Collaborator

@scottgonzalez - Do you think you might be able to review this PR when you have some time?

@scottgonzalez

_recordOption() seems bad. You should never check for the state of a widget by looking at data- attributes. This will cause .attr( "data-foo" ) and .data( "foo" ) to be inconsistent (why would you use .attr() anyway?).

As for the individual set methods, this is planend for jQuery UI. jQuery Mobile should not stray from UI's path so it'd be better to implement this in UI and then backport it to Mobile (or just pull in the the widget factory from master).

@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.button.js
@@ -110,16 +110,20 @@ $.widget( "mobile.button", $.mobile.widget, {
this.refresh();
},
- enable: function() {
- this.element.attr( "disabled", false );
- this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
- return this._setOption( "disabled", false );
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value );
+ this.button[ value ? "addClass" : "removeClass" ]( "ui-disabled" ).attr( "aria-disabled", value );
@scottgonzalez Owner

.toggleClass() is shorter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.button.js
((11 lines not shown))
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
+ _setOption: function( key, value ) {
+ if ( key === "disabled" ) {
@scottgonzalez Owner

Isn't the whole point of this PR to avoid this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.button.js
((11 lines not shown))
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
+ _setOption: function( key, value ) {
+ if ( key === "disabled" ) {
+ this._setDisabled( value );
+ }
+ else {
+ this.button.buttonMarkup( ( function() { var ret = {}; ret[ key ] = value; return ret; } )() );
+ }
+
+ this._recordOption( key, value );
@scottgonzalez Owner

This redundancy seems bad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.button.js
((11 lines not shown))
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
+ _setOption: function( key, value ) {
+ if ( key === "disabled" ) {
+ this._setDisabled( value );
+ }
+ else {
+ this.button.buttonMarkup( ( function() { var ret = {}; ret[ key ] = value; return ret; } )() );
@scottgonzalez Owner

Avoid function invocations for really simple operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.checkboxradio.js
((8 lines not shown))
},
- enable: function() {
- this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" );
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value ).parent()[ value ? "addClass" : "removeClass" ]( "ui-disabled" );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.select.js
((6 lines not shown))
this.button.attr( "aria-disabled", value );
- return this._setOption( "disabled", value );
+ this.button[ value ? "addClass" : "removeClass" ]( "ui-disabled" );
+ },
+
+ _setOption: function( key, value ) {
+ // This will take care of those options that buttonMarkup knows how to handle
+ this.button.buttonMarkup( ( function() { var ret = {}; ret[ key ] = value; return ret; } )() );
@scottgonzalez Owner

Don't create functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.slider.js
@@ -122,6 +121,8 @@ $.widget( "mobile.slider", $.mobile.widget, {
sliderImg.setAttribute('role','img');
sliderImg.appendChild(document.createTextNode(options[i].innerHTML));
$(sliderImg).prependTo( slider );
+ if (!i)
@scottgonzalez Owner

Is there something crazy going on with indentation here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.slider.js
@@ -393,18 +394,34 @@ $.widget( "mobile.slider", $.mobile.widget, {
}
},
- enable: function() {
- this.element.attr( "disabled", false );
- this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
- return this._setOption( "disabled", false );
+ _setMini: function( value ) {
+ this.slider[ value ? "addClass" : "removeClass" ]( "ui-slider-mini" );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.slider.js
((10 lines not shown))
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
- }
+ _setTheme: function( value ) {
+ this.handle.buttonMarkup( { theme: ( value || $.mobile.getInheritedTheme( this.element, "c" ) ) } );
+ },
+
+ _setTrackTheme: function( value ) {
+ var currentTheme = ( this.options.trackTheme || $.mobile.getInheritedTheme( this.element, "c" ) );
+
+ value = ( value || $.mobile.getInheritedTheme( this.element, "c" ) );
@scottgonzalez Owner

This logic is repeated quite a bit, why not abstract it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.slider.js
((29 lines not shown))
+ if ( this.handleSpans ) {
+ this.handleSpans
+ .removeClass( "ui-btn-down-" + currentTheme )
+ .addClass( "ui-btn-down-" + value );
+ }
+ },
+
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value );
+ this.slider[ value ? "addClass" : "removeClass" ]( "ui-disabled" ).attr( "aria-disabled", value );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.textinput.js
@@ -23,6 +23,7 @@ $.widget( "mobile.textinput", $.mobile.widget, {
o = this.options,
theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ),
themeclass = " ui-body-" + theme,
+ self = this,
@scottgonzalez Owner

Use that or $.proxy or _bind() (from new widget factory). If Mobile is already using self everywhere else, don't worry about this for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.textinput.js
@@ -50,8 +51,12 @@ $.widget( "mobile.textinput", $.mobile.widget, {
if ( input.is( "[type='search'],:jqmData(type='search')" ) ) {
focusedEl = input.wrap( "<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield" + themeclass + miniclass + "'></div>" ).parent();
- clearbtn = $( "<a href='#' class='ui-input-clear' title='" + o.clearSearchButtonText + "'>" + o.clearSearchButtonText + "</a>" )
- .bind('click', function( event ) {
+ this._themedElement = focusedEl.add( input );
+ this._clearSpan = $( "<span>" ).text( o.clearSearchButtonText );
+ this._clearBtn =
+ clearbtn = $( "<a href='#' class='ui-input-clear' title='" + o.clearSearchButtonText + "'></a>" )
+ .append( this._clearSpan )
+ .bind( 'click', function( event ) {
@scottgonzalez Owner

double quotes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez scottgonzalez commented on the diff
js/jquery.mobile.forms.textinput.js
((23 lines not shown))
},
- enable: function(){
- ( this.element.attr( "disabled", false).is( "[type='search'],:jqmData(type='search')" ) ?
- this.element.parent() : this.element ).removeClass( "ui-disabled" );
+ _setDisabled: function( value ) {
+ ( this.element.prop( "disabled", value ).is( "[type='search'],:jqmData(type='search')" ) ?
@scottgonzalez Owner

I realize this was already in this form, but expand this to an if.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@scottgonzalez

I got halfway though and stopped. I don't even know how the Mobile team feels about this since it's been open for 2 months with no discussion.

@gabrielschulhof
Collaborator

This is not a priority for jQuery Mobile 1.2, and that's why it's lingering. However, it's not off the table. In fact, we were talking about asking you to go over the PR about two weeks ago in our weekly meeting.

I'm glad UI is implementing the option demultiplexing. I suppose this can wait until we grab the widget factory once more. How exactly will it be done? Is it very different from the way I did it? I ask, because I'd like to fix all the issues you pointed out, but if the future mechanism will be very different, then I may need to start over.

I wasn't sure about recordOption either, however, I was under the impression that the data-* attributes of the DOM element should be kept in sync with this.options.*, otherwise you end up having this.options.caption = "New Value", while data-caption="Old Value" or absent. So you cannot tell the value of a given option by looking at the DOM element alone.

OTOH, if data-* attributes are used only for initialization, and need not reflect changes to the options which happen later on, then _recordOption() can be omitted.

@johnbender

@gabrielschulhof @scottgonzalez

Can we defer this until we upgrade the widget factory? Is there anything here that the widget factory won't support when we do upgrade?

If we should just wait let's close the pr.

@gabrielschulhof
Collaborator

OK. Let's revisit the topic once the widget factory supports the option demultiplexing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
20 js/jquery.mobile.forms.button.js
@@ -110,16 +110,20 @@ $.widget( "mobile.button", $.mobile.widget, {
this.refresh();
},
- enable: function() {
- this.element.attr( "disabled", false );
- this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
- return this._setOption( "disabled", false );
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value );
+ this.button[ value ? "addClass" : "removeClass" ]( "ui-disabled" ).attr( "aria-disabled", value );
@scottgonzalez Owner

.toggleClass() is shorter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
+ _setOption: function( key, value ) {
+ if ( key === "disabled" ) {
@scottgonzalez Owner

Isn't the whole point of this PR to avoid this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ this._setDisabled( value );
+ }
+ else {
+ this.button.buttonMarkup( ( function() { var ret = {}; ret[ key ] = value; return ret; } )() );
@scottgonzalez Owner

Avoid function invocations for really simple operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+
+ this._recordOption( key, value );
@scottgonzalez Owner

This redundancy seems bad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
},
refresh: function() {
View
8 js/jquery.mobile.forms.checkboxradio.js
@@ -189,12 +189,12 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, {
}
},
- disable: function() {
- this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" );
+ _setTheme: function( value ) {
+ this.label.buttonMarkup( { theme: ( value || $.mobile.getInheritedTheme( this.element, "c" ) ) } );
},
- enable: function() {
- this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" );
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value ).parent()[ value ? "addClass" : "removeClass" ]( "ui-disabled" );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
}
});
View
18 js/jquery.mobile.forms.select.custom.js
@@ -61,10 +61,11 @@ define( [
if( widget.isMultiple ) {
headerClose = $( "<a>", {
- "text": widget.options.closeText,
"href": "#",
"class": "ui-btn-left"
- }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
+ })
+ .append( widget.headerCloseText = $( "<span>" ).text( widget.options.closeText ) )
+ .attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
}
$.extend( widget, {
@@ -516,6 +517,19 @@ define( [
self.list.listview();
},
+ _setCloseText: function( value ) {
+ if ( this.headerCloseText ) {
+ this.headerClose.attr( "title", value );
+ this.headerCloseText.text( value );
+ }
+ },
+
+ _setOverlayTheme: function( value ) {
+ this.listbox
+ .removeClass( "ui-body-" + ( this.options.overlayTheme || $.mobile.getInheritedTheme( this.listbox, "a" ) ) )
+ .addClass( "ui-body-" + ( value || $.mobile.getInheritedTheme( this._button(), "a" ) ) );
+ },
+
_button: function(){
return $( "<a>", {
"href": "#",
View
26 js/jquery.mobile.forms.select.js
@@ -33,9 +33,19 @@ $.widget( "mobile.selectmenu", $.mobile.widget, {
},
_setDisabled: function( value ) {
- this.element.attr( "disabled", value );
+ this.element.prop( "disabled", value );
this.button.attr( "aria-disabled", value );
- return this._setOption( "disabled", value );
+ this.button[ value ? "addClass" : "removeClass" ]( "ui-disabled" );
+ },
+
+ _setOption: function( key, value ) {
+ // This will take care of those options that buttonMarkup knows how to handle
+ this.button.buttonMarkup( ( function() { var ret = {}; ret[ key ] = value; return ret; } )() );
@scottgonzalez Owner

Don't create functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ // Handle the rest the usual way - it doesn't matter if we end up with the
+ // buttonMarkup-related options in this call, because those are not handled anyway
+ $.mobile.widget.prototype._setOption.apply( this, arguments );
+
+ this._recordOption( key, value );
},
_focusButton : function() {
@@ -232,17 +242,7 @@ $.widget( "mobile.selectmenu", $.mobile.widget, {
// open and close preserved in native selects
// to simplify users code when looping over selects
open: $.noop,
- close: $.noop,
-
- disable: function() {
- this._setDisabled( true );
- this.button.addClass( "ui-disabled" );
- },
-
- enable: function() {
- this._setDisabled( false );
- this.button.removeClass( "ui-disabled" );
- }
+ close: $.noop
});
//auto self-init widgets
View
37 js/jquery.mobile.forms.slider.js
@@ -12,7 +12,6 @@ $.widget( "mobile.slider", $.mobile.widget, {
options: {
theme: null,
trackTheme: null,
- disabled: false,
initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",
mini: false
},
@@ -122,6 +121,8 @@ $.widget( "mobile.slider", $.mobile.widget, {
sliderImg.setAttribute('role','img');
sliderImg.appendChild(document.createTextNode(options[i].innerHTML));
$(sliderImg).prependTo( slider );
+ if (!i)
@scottgonzalez Owner

Is there something crazy going on with indentation here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ self.handleSpans = $(sliderLabel).add(sliderImg);
}
self._labels = $( ".ui-slider-label", slider );
@@ -393,18 +394,34 @@ $.widget( "mobile.slider", $.mobile.widget, {
}
},
- enable: function() {
- this.element.attr( "disabled", false );
- this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
- return this._setOption( "disabled", false );
+ _setMini: function( value ) {
+ this.slider[ value ? "addClass" : "removeClass" ]( "ui-slider-mini" );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
},
- disable: function() {
- this.element.attr( "disabled", true );
- this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
- return this._setOption( "disabled", true );
- }
+ _setTheme: function( value ) {
+ this.handle.buttonMarkup( { theme: ( value || $.mobile.getInheritedTheme( this.element, "c" ) ) } );
+ },
+
+ _setTrackTheme: function( value ) {
+ var currentTheme = ( this.options.trackTheme || $.mobile.getInheritedTheme( this.element, "c" ) );
+
+ value = ( value || $.mobile.getInheritedTheme( this.element, "c" ) );
@scottgonzalez Owner

This logic is repeated quite a bit, why not abstract it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ this.slider
+ .removeClass( "ui-btn-down-" + currentTheme )
+ .addClass( "ui-btn-down-" + value );
+ if ( this.handleSpans ) {
+ this.handleSpans
+ .removeClass( "ui-btn-down-" + currentTheme )
+ .addClass( "ui-btn-down-" + value );
+ }
+ },
+
+ _setDisabled: function( value ) {
+ this.element.prop( "disabled", value );
+ this.slider[ value ? "addClass" : "removeClass" ]( "ui-disabled" ).attr( "aria-disabled", value );
@scottgonzalez Owner

.toggleClass()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
});
//auto self-init widgets
View
35 js/jquery.mobile.forms.textinput.js
@@ -23,6 +23,7 @@ $.widget( "mobile.textinput", $.mobile.widget, {
o = this.options,
theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ),
themeclass = " ui-body-" + theme,
+ self = this,
@scottgonzalez Owner

Use that or $.proxy or _bind() (from new widget factory). If Mobile is already using self everywhere else, don't worry about this for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
mini = input.jqmData("mini") == true,
miniclass = mini ? " ui-mini" : "",
focusedEl, clearbtn;
@@ -50,8 +51,12 @@ $.widget( "mobile.textinput", $.mobile.widget, {
if ( input.is( "[type='search'],:jqmData(type='search')" ) ) {
focusedEl = input.wrap( "<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield" + themeclass + miniclass + "'></div>" ).parent();
- clearbtn = $( "<a href='#' class='ui-input-clear' title='" + o.clearSearchButtonText + "'>" + o.clearSearchButtonText + "</a>" )
- .bind('click', function( event ) {
+ this._themedElement = focusedEl.add( input );
+ this._clearSpan = $( "<span>" ).text( o.clearSearchButtonText );
+ this._clearBtn =
+ clearbtn = $( "<a href='#' class='ui-input-clear' title='" + o.clearSearchButtonText + "'></a>" )
+ .append( this._clearSpan )
+ .bind( 'click', function( event ) {
@scottgonzalez Owner

double quotes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
input
.val( "" )
.focus()
@@ -79,6 +84,7 @@ $.widget( "mobile.textinput", $.mobile.widget, {
input.bind('paste cut keyup focus change blur', toggleClear);
} else {
+ this._themedElement = input;
input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass );
}
@@ -132,14 +138,27 @@ $.widget( "mobile.textinput", $.mobile.widget, {
}
},
- disable: function(){
- ( this.element.attr( "disabled", true ).is( "[type='search'],:jqmData(type='search')" ) ?
- this.element.parent() : this.element ).addClass( "ui-disabled" );
+ _setClearSearchButtonText: function( value ) {
+ if ( this._clearSpan ) {
+ this._clearSpan.text( value );
+ }
+ if ( this._clearBtn ) {
+ this._clearBtn.prop( "title", value );
+ }
+ },
+
+ _setTheme: function( value ) {
+ this._themedElement
+ .removeClass( "ui-body-" + ( this.options.theme || $.mobile.getInheritedTheme( this._themedElement, "c" ) ) )
+ .addClass( "ui-body-" + ( value || $.mobile.getInheritedTheme( this._themedElement, "c" ) ) );
+ if ( this._clearBtn ) {
+ this._clearBtn.buttonMarkup( { theme: value } );
+ }
},
- enable: function(){
- ( this.element.attr( "disabled", false).is( "[type='search'],:jqmData(type='search')" ) ?
- this.element.parent() : this.element ).removeClass( "ui-disabled" );
+ _setDisabled: function( value ) {
+ ( this.element.prop( "disabled", value ).is( "[type='search'],:jqmData(type='search')" ) ?
@scottgonzalez Owner

I realize this was already in this form, but expand this to an if.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ this.element.parent() : this.element )[ value ? "addClass" : "removeClass" ]( "ui-disabled" );
}
});
View
18 js/jquery.mobile.widget.js
@@ -39,6 +39,24 @@ $.widget( "mobile.widget", {
return options;
},
+ _recordOption: function( key, value ) {
+ this.options[ key ] = value;
+ this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value );
+ },
+
+ _setOption: function( key, value ) {
+ var setter = "_set" + key.replace( /^[a-z]/, function( c ) { return c.toUpperCase(); } );
+
+ if (this[ setter ] !== undefined) {
+ this[ setter ]( value );
+ // Make sure the options key and the corresponding data-* attribute is set
+ this._recordOption( key, value );
+ }
+ else {
+ $.Widget.prototype._setOption.apply( this, arguments );
+ }
+ },
+
enhanceWithin: function( target, useKeepNative ) {
this.enhance( $( this.options.initSelector, $( target )), useKeepNative );
},
View
28 tests/functional/inspect-page.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div data-role="page" data-external-page="false" data-dom-cache="true" id="inspect-page" data-add-back-btn="true">
+ <style>
+ .inspect-page-cell {
+ vertical-align: top;
+ padding: 20px;
+ }
+ </style>
+ <div data-role="header">
+ <h1>Test Options</h1>
+ </div>
+ <div data-role="content">
+ <table>
+ <tr>
+ <td id="widget-list" class="inspect-page-cell"></td>
+ <td class="inspect-page-cell">
+ <div data-scroll="y" class="inspect-page-pane">
+ <div id="option-list"></div>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </body>
+</html>
View
88 tests/functional/inspect-page.js
@@ -0,0 +1,88 @@
+// inspect-page
+// Inspect and set options for widgets on a jQuery Mobile page
+//
+// To use:
+// 1. Add inspect-page.js, option-list.js, and inspect-page.html to the folder where the app under test is located
+// 2. Add a reference to this script (inspect-page.js), and to option-list.js to the list of scripts loaded from the app
+// under test
+//
+// The result:
+// Every page of the app which has a jQuery Mobile header (<div data-role='header'>) will have a button on the right
+// labelled "Test Options". When you click this button, a list of all widgets found on the current page will be
+// displayed. If more than one widget is associated with a given element, each one will be shown. When you click on a
+// widget, its options will be shown editably on the right of the list. Modifications to the options will result in a
+// <element>.widgetname("option", <optionName>, <newValue>) call to the widget.
+//
+// To customize:
+// After including this file in your app, you can overwrite the "addInspectionTrigger" function to do something other
+// than what the default does (which is to add the button to the page header), and, within the new handler, you can call
+// "launchInspector" from the context of an element of your choosing. The context is only important if you also have
+// web-ui-fw defined in your app, because the coordinates of the context element are used for launching a popup window
+// which contains the widget list and the option list
+
+(function( $, undefined) {
+
+ function elemName( elem ) {
+ var str = elem.tagName
+ + ( $( elem ).attr( "id" ) === undefined ? "" : ( "#" + $( elem ).attr( "id" ) ) )
+ + ( $( elem ).attr( "class" ) === undefined ? ""
+ : ( "." + $( elem ).attr( "class" ).replace( " ", "." ) ) );
+
+ if ( str.length > 40 )
+ str = str.substring( 0, 40 ) + "...";
+
+ return str;
+ }
+
+ function launchInspector() {
+ var src = $( this ),
+ page = src.closest( ":jqmData(role='page')" ),
+ widgetList = $( "#widget-list" ).empty(),
+ optionList = $( "#option-list" ),
+ realList = $( "<ul data-role='listview' data-scroll='y' class='inspect-page-pane'></ul>" )
+ .appendTo( widgetList );
+
+ if ( optionList.data( "optionlist" ) )
+ optionList.optionlist( "destroy" );
+
+ page.find( "*" ).each( function() {
+ var widgets = $.todons.optionlist.widgetsFromElement( this );
+ if ( widgets ) {
+ realList.append( "<li data-role='list-divider'>" + elemName( this ) + "</li>" );
+ $.each( widgets, function( key, value ) {
+ $( "<li><a>" + value.namespace + "." + value.widgetName + "</a></li>" )
+ .appendTo( realList )
+ .find( "a" )
+ .bind( "vclick", function() {
+ if ( optionList.data( "optionlist" ) )
+ optionList.optionlist( "destroy" );
+ optionList.optionlist();
+ optionList.optionlist( "option", "widget", value );
+ });
+ });
+ }
+ });
+
+ listPage = realList.closest( ":jqmData(role='page')" );
+ if ( listPage.data( "page" ) ) {
+ realList.listview();
+ if ( $.mobile.scrollview )
+ realList.scrollview();
+ }
+ }
+
+ var addInspectionTrigger = function( pg ) {
+ if ( $( pg ).find( "[data-option-inspect-button='true']" ).length === 0 ) {
+ $( "<a href='#inspect-page' data-option-inspect-button='true' class='ui-btn-right' data-iconpos='left' data-icon='grid'>Test Options</a>" )
+ .appendTo( $( ":jqmData(role='header')", pg ) )
+ .buttonMarkup()
+ .bind( "vclick", launchInspector );
+ }
+ }
+
+ $( document )
+ .ready( function() {
+ $.mobile.loadPage( "inspect-page.html" );
+ })
+ .bind( "pagecreate", function( e ) { addInspectionTrigger( e.target ); } );
+})( jQuery );
View
151 tests/functional/option-list.js
@@ -0,0 +1,151 @@
+// option list widget
+//
+// Given a widget created using the widget factory, this widget will display a list of form elements that can be used to
+// set the widget's options at runtime
+//
+// options:
+//
+// widget: a widget instance
+
+( function( $, undefined ) {
+ $.widget( "todons.optionlist", $.mobile.widget, {
+ options: {
+ widget: null
+ },
+
+ _setOption: function( key, value ) {
+ switch ( key ) {
+ case "widget":
+ return this._setWidget( value );
+ default:
+ return $.Widget.prototype._setOption.call( this, key, value );
+ }
+ },
+
+ _setWidget: function( value ) {
+ if ( value !== null ) {
+ var self = this;
+
+ this.options.widget = value;
+ this.element.append( $( "<h2>" + value.namespace + "." + value.widgetName + "</h2>" ) );
+ $.each ( $[ value.namespace ][ value.widgetName ].prototype.options, function( key ) {
+ self._createOption( value.namespace, value.widgetName, value.element, key );
+ });
+ this.element.trigger( "create" );
+ }
+ },
+
+ destroy: function() {
+ this.element.empty();
+ $.Widget.prototype.destroy.call( this );
+ },
+
+ _createOption: function( ns, widgetType, theWidget, key ) {
+ var optionsList = this.element, entry,
+ self = this,
+ id = "todons-optionlist-option-" + ( $.todons.optionlist.optionId++ ) + "-" + key;
+
+ function makeSetter() {
+ switch( typeof $[ ns ][ widgetType ].prototype.options[ key ] ) {
+ case "boolean":
+ return {
+ html: $( "<input/>", {
+ type: "checkbox",
+ checked: theWidget[ widgetType ]( "option", key ),
+ id: id
+ }),
+ getValue: function( elem ) { return elem.is( ":checked" ); }
+ };
+
+ case "integer":
+ return {
+ html: $( "<input/>", {
+ type: "number",
+ value: theWidget[ widgetType ]( "option", key ),
+ id: id
+ }),
+ getValue: function( elem ) { return elem.val(); }
+ };
+
+ default:
+ return {
+ html: $( "<input/>", {
+ type: "text",
+ value: theWidget[ widgetType ]( "option", key ),
+ id: id
+ }),
+ getValue: function( elem ) { return elem.val(); }
+ };
+ }
+ }
+
+ entry = makeSetter();
+ $( "<div/>" )
+ .append( $( "<label/>", { "for": id } ).text( key ) )
+ .append( entry.html )
+ .appendTo( optionsList )
+ .fieldcontain();
+
+ entry.html.bind( "change", function( e ) {
+ theWidget[ widgetType ]( "option", key, entry.getValue( entry.html ) );
+ self.element.triggerHandler( "optionChanged" );
+ });
+ }
+
+ });
+
+ // "duck typing" a widget - thanks, gnarf! :)
+ // Basically, check if an element has any object data-items which contain a function under the key "widget"
+ // and a string under the key "widgetName"
+ //
+ // Returns either false or the list of widgets associated with the element
+ $.todons.optionlist.widgetsFromElement = function( elem ) {
+ var ret = [];
+
+ // for each data item ...
+ $.each( $.data( elem ), function( key, value ) {
+ // ... if it's an object
+ if ( value && typeof value === "object" ) {
+ var hasWidgetFunction = false, hasWidgetName = false, candidate = null;
+
+ // Check all its properties for the magic ones
+ $.each( value, function( innerKey, innerValue ) {
+ if ( innerKey === "widgetName" ) {
+ hasWidgetName = true;
+ }
+ else
+ if ( innerKey === "widget" && typeof innerValue === "function" ) {
+ hasWidgetFunction = true;
+ }
+
+ if ( hasWidgetFunction && hasWidgetName ) {
+ candidate = value;
+ return false;
+ }
+ });
+
+ // If this data item is a widget ...
+ if ( candidate ) {
+ // ... check if we've already collected a widget by the same name and namespace
+ $.each( ret, function( idx, value ) {
+ if ( value.widgetName === candidate.widgetName && value.namespace === candidate.namespace ) {
+ candidate = null;
+ return false;
+ }
+ });
+ }
+
+ // If we haven't seen this widget yet, add it to the array
+ if ( candidate ) {
+ ret.push( candidate );
+ }
+ }
+ });
+
+ return (ret.length === 0 ? false : ret);
+ };
+
+ // monotonically increasing counter for option labels
+ $.todons.optionlist.optionId = 0;
+
+})( jQuery );
View
68 tests/functional/option-test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>jQuery Mobile Widget Option Tester</title>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="../../css/themes/default/jquery.mobile.css" />
+ <link rel="stylesheet" href="../../docs/_assets/css/jqm-docs.css" />
+ <script src="../../js/jquery.tag.inserter.js"></script>
+ <script src="../../js/jquery.js"></script>
+ <script src="../../docs/_assets/js/jqm-docs.js"></script>
+ <script src="../../js/"></script>
+ <script src="option-list.js"></script>
+ <script src="inspect-page.js"></script>
+ </head>
+
+ <body>
+ <div data-role="page">
+ <div data-role="header">
+ <h1>jQuery Mobile Widget Option Tester</h1>
+ </div>
+ <div data-role="contents">
+ <form id="sample" name="sample" action="#" method="get" style="width: 50%; float: left;">
+ <div data-role="fieldcontain">
+ <label for="sample-checkbox" id="sample-checkbox-label">Checkbox</label>
+ <input type="checkbox" name="sample-checkbox" id="sample-checkbox"></input>
+ </div>
+ <div data-role="fieldcontain">
+ <label for="sample-slider" id="sample-slider-label">Slider</label>
+ <input type="range" min="3" max="17" value="5" id="sample-slider"></input>
+ </div>
+ <div data-role="fieldcontain">
+ <label for="sample-select-slider" id="sample-select-slider-label">Select slider</label>
+ <select data-role="slider" id="sample-select-slider">
+ <option value="sss-1">Yes</option>
+ <option value="sss-2">Never</option>
+ </select>
+ </div>
+ <div data-role="fieldcontain">
+ <label for="select-choice-0" class="select">Shipping method:</label>
+ <select name="select-choice-0" id="select-choice-1" data-native-menu="false" multiple>
+ <option>Choose shipping method(s)</option>
+ <optgroup label="USPS">
+ <option value="standard">Standard: 7 day</option>
+ <option value="rush">Rush: 3 days</option>
+ <option value="express">Express: next day</option>
+ <option value="overnight">Overnight</option>
+ </optgroup>
+ <optgroup label="FedEx">
+ <option value="firstOvernight">First Overnight</option>
+ <option value="expressSaver">Express Saver</option>
+ <option value="ground">Ground</option>
+ </optgroup>
+ </select>
+ </div>
+ <input type="submit" value="Submit"></input>
+ <input type="button" value="Hello"></input>
+ <input type="text" id="sample-text"></input>
+ <input type="search" id="sample-search"></input>
+ <ul id="sample-list-view" data-role="listview">
+ <li>List Item 1</li>
+ <li>List Item 2</li>
+ </ul>
+ </form>
+ </div>
+ </div>
+ </body>
+</html>
Something went wrong with that request. Please try again.