diff --git a/css/structure/jquery.mobile.filterbar.css b/css/structure/jquery.mobile.filterbar.css
new file mode 100644
index 00000000000..1000b8a55d9
--- /dev/null
+++ b/css/structure/jquery.mobile.filterbar.css
@@ -0,0 +1,25 @@
+.ui-filter {
+ border-width: 0;
+ overflow: hidden;
+ margin: -1em -1em 1em -1em;
+}
+.ui-filter-inset {
+ margin: 1em -.3125em -1em;
+ background: transparent;
+}
+.ui-collapsible-content .ui-filter {
+ margin: -.625em -1em .625em -1em;
+ border-bottom: inherit;
+}
+.ui-collapsible-content .ui-filter-inset {
+ margin: -.3125em;
+ border-bottom-width: 0;
+}
+.ui-filter .ui-input-search {
+ margin: .3125em;
+ width: auto;
+ display: block;
+}
+
+
+
diff --git a/js/widgets/filter.js b/js/widgets/filter.js
new file mode 100644
index 00000000000..1d5d295cdc5
--- /dev/null
+++ b/js/widgets/filter.js
@@ -0,0 +1,314 @@
+//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
+//>>description: Adds a filterbar to an element collection
+//>>label: Filter
+//>>group: Widgets
+
+
+define( [ "jquery", "./forms/textinput" ], function( jQuery ) {
+//>>excludeEnd("jqmBuildExclude");
+(function( $, undefined ) {
+ "use strict";
+
+ // TODO rename filterCallback/deprecate and default to the item itself as the first argument
+ var defaultfilterCallback = function( text, searchValue /*, item */) {
+ return text.toString().toLowerCase().indexOf( searchValue ) === -1;
+ };
+
+ $.widget("mobile.filterbar", $.mobile.widget, $.extend( {
+
+ options: {
+ filterTheme: "a",
+ filterPlaceholder: "Filter items...",
+ filterReveal: false,
+ filterCallback: defaultfilterCallback,
+ classes: "",
+ id: null,
+ inset: false,
+ enhance: true,
+ target: null,
+ mini: false,
+ selector: null
+ },
+
+ _onKeyUp: function() {
+ var self = this,
+ search = self._search[ 0 ],
+ o = self.options,
+ getAttrFixed = $.mobile.getAttribute,
+ val = search.value.toLowerCase(),
+ lastval = getAttrFixed( search, "lastval", true ) + "";
+
+ if ( lastval && lastval === val ) {
+ // Execute the handler only once per value change
+ return;
+ }
+
+ if (o.timer !== undefined) {
+ window.clearTimeout(o.timer);
+ }
+
+ o.timer = window.setTimeout(function() {
+
+ self._trigger( "beforefilter", "beforefilter", { input: search } );
+
+ // Change val as lastval for next execution
+ search.setAttribute( "data-" + $.mobile.ns + "lastval" , val );
+
+ self._filterItems( search, val, lastval )
+ }, 250);
+ },
+
+ _getFilterableItems: function() {
+ var self = this,
+ el = self.element,
+ o = self.options,
+ items = [];
+
+ if (typeof o.selector === "string") {
+ items = $("." + o.selector).children();
+ } else {
+ items = el.find("> li, > option, tbody tr, .ui-controlgroup-controls .ui-btn");
+ }
+ return items;
+ },
+
+ _setFilterableItems: function(val, lastval) {
+ var self = this,
+ o = self.options,
+ filterItems = [],
+ isCustomfilterCallback = o.filterCallback !== defaultfilterCallback,
+ _getFilterableItems = self._getFilterableItems();
+
+ if ( isCustomfilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) {
+
+ // Custom filter callback applies or removed chars or pasted something totally different, check all items
+ filterItems = _getFilterableItems;
+ } else {
+
+ // Only chars added, not removed, only use visible subset
+ filterItems = _getFilterableItems.filter( ":not(.ui-screen-hidden)" );
+
+ if ( !filterItems.length && o.filterReveal ) {
+ filterItems = _getFilterableItems.filter( ".ui-screen-hidden" );
+ }
+ }
+ return filterItems;
+ },
+
+ _filterItems: function( search, val, lastval ){
+ var self = this,
+ el = self.element,
+ o = self.options,
+ getAttrFixed = $.mobile.getAttribute,
+ filterItems = self._setFilterableItems(val, lastval),
+ _getFilterableItems = self._getFilterableItems(),
+ childItems = false,
+ itemtext = "",
+ item,
+ select = self.element.parents( ".ui-select" ),
+ i;
+
+ self._setOption( "timer", undefined );
+
+ if ( val ) {
+
+ for ( i = filterItems.length - 1; i >= 0; i-- ) {
+ item = $( filterItems[ i ] );
+ // NOTE: should itemtext be stored somewhere? Will text() change much
+ // and does this need to be re-parsed on every iteration? Also, no
+ // chance to run data-filtertext on anything that JQM wraps, because
+ // we filter the wrapper and can't access the input/a. Can we?
+ itemtext = getAttrFixed(filterItems[ i ], "filtertext", true) || item.text();
+
+ if ( item.is( ".ui-li-divider" ) ) {
+
+ item.toggleClass( "ui-filter-hidequeue" , !childItems );
+
+ // New bucket!
+ childItems = false;
+
+ } else if ( o.filterCallback( itemtext, val, item ) ) {
+
+ //mark to be hidden
+ item.toggleClass( "ui-filter-hidequeue" , true );
+ } else {
+
+ // There's a shown item in the bucket
+ childItems = true;
+ }
+ }
+
+ self._toggleFilterableItems( filterItems, select, o.filterReveal , true);
+ } else {
+ self._toggleFilterableItems( filterItems, select, o.filterReveal );
+ }
+
+ self._addFirstLastClasses( _getFilterableItems, self._getVisibles( _getFilterableItems, false ), false );
+ },
+
+ _toggleFilterableItems: function( filterItems, select, reveal, isVal ) {
+
+ if (isVal) {
+ // Show items, not marked to be hidden
+ filterItems
+ .filter( ":not(.ui-filter-hidequeue)" )
+ .toggleClass( "ui-screen-hidden", false );
+
+ // Hide items, marked to be hidden
+ filterItems
+ .filter( ".ui-filter-hidequeue" )
+ .toggleClass( "ui-screen-hidden", true )
+ .toggleClass( "ui-filter-hidequeue", false );
+
+ // select - hide parent when no options match?
+ if ( select ) {
+ if ( filterItems.length === filterItems.filter( ".ui-screen-hidden").length ) {
+ select.addClass( "ui-screen-hidden" );
+ }
+ }
+ } else {
+ //filtervalue is empty => show all
+ filterItems.toggleClass( "ui-screen-hidden", !!reveal );
+ // select
+ if ( select ) {
+ select.removeClass( "ui-screen-hidden", !!reveal );
+ }
+ }
+ },
+
+ _enhance: function () {
+ var self = this,
+ el = this.element,
+ o = self.options,
+ wrapper = $( "
", {
+ "class": o.classes + " ui-filter ",
+ "role": "search",
+ "id" : o.id || "ui-filter-" + self.uuid
+ }),
+ search = $( "
", {
+ placeholder: o.filterPlaceholder
+ })
+ .attr( "data-" + $.mobile.ns + "type", "search" )
+ .appendTo( wrapper )
+ .textinput({
+ theme: o.filterTheme,
+ mini: o.mini
+ });
+
+ if ( o.inset ) {
+ wrapper.addClass( "ui-filter-inset" );
+ }
+
+ if ( typeof o.target === "string" ) {
+ wrapper.prependTo( $( "." + o.target + "" ) );
+ } else {
+ wrapper.insertBefore( el );
+ }
+
+ return search;
+ },
+
+ _create: function() {
+ var self = this,
+ o = self.options,
+ search,
+ items = self._getFilterableItems();
+
+ if ( o.filterReveal ) {
+ items.addClass( "ui-screen-hidden" );
+ }
+
+ self._setOption( "timer", undefined );
+
+ if (o.enhance) {
+ search = self._enhance();
+ } else {
+ // NOTE: DIY requires data-id, otherwise how do we find the search
+ // input. We could always wrap the filterable element (e.g. ul) in
+ // ui-filter as well, but I'm not sure I want to move elements around
+ // that much.
+ search = $( "#" + o.id ).find( "input" );
+ }
+
+ self._on( search, { keyup: "_onKeyUp", change: "_onKeyUp", input: "_onKeyUp" } );
+
+ $.extend( self, {
+ _search: search
+ });
+
+ // NOTE: since the filter was based on the listview, some unit tests seem
+ // to listen for the initial addFirstLastClasses call when the listview
+ // is setup (at least I cannot recreate a refreshCornerCount in Qunit
+ // without setting first and last classes on the filterable elements on
+ // create). If refresh corners is to be run on the filter, I would prefer
+ // it being solely called by the filter being triggered and not be the
+ // "_super()-widget" calling it. So 2x input on the filter should trigger
+ // 2x addFirstLastClasses vs. currently 3x because of including the call
+ // when setting up the parent listview.
+ self._addFirstLastClasses( items, self._getVisibles( items, true ), true );
+ },
+
+ _setOptions: function( options ) {
+ var self = this,
+ key;
+
+ for ( key in options ) {
+ self._setOption( key, options[ key ] );
+ }
+
+ return self;
+ },
+
+ _setOption: function( key, value ) {
+ var self = this,
+ o = self.options,
+ wrapper = document.getElementById( o.id || "ui-filter-" + self.uuid ),
+ $input = $( wrapper ).find( "input" );
+
+ // always update
+ o[ key ] = value;
+
+ if ( key === "disabled" ) {
+ $input
+ .toggleClass( self.widgetFullName + "-disabled ui-state-disabled", !!value )
+ .attr( "aria-disabled", value )
+ .textinput( value ? "disable" : "enable" );
+ }
+ return self;
+ },
+
+ widget: function() {
+ return this.element
+ },
+
+ enable: function() {
+ return this._setOption( "disabled", false );
+ },
+
+ disable: function() {
+ return this._setOption( "disabled", true );
+ },
+
+ destroy: function() {
+ var self = this,
+ o = self.options,
+ wrapper = document.getElementById( o.id || "ui-filter-" + self.uuid );
+
+ if ( o.enhance ) {
+ wrapper.parentNode.removeChild( wrapper );
+ }
+ self._toggleFilterableItems( self._getFilterableItems(), false, false );
+ self._destroy();
+ }
+
+ }, $.mobile.behaviors.addFirstLastClasses ) );
+
+ $.mobile.filterbar.initSelector = ':jqmData(filter="true")';
+
+ //auto self-init widgets
+ $.mobile._enhancer.add( "mobile.filterbar" );
+
+})( jQuery );
+//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
+});
+//>>excludeEnd("jqmBuildExclude");
diff --git a/tests/integration/filter/filter_core.js b/tests/integration/filter/filter_core.js
new file mode 100644
index 00000000000..6af26edeeef
--- /dev/null
+++ b/tests/integration/filter/filter_core.js
@@ -0,0 +1,942 @@
+/*
+ * mobile filter unit tests - listview
+ */
+
+// TODO split out into seperate test files
+(function($){
+ var home = $.mobile.path.parseUrl( location.href ).pathname + location.search,
+ insetVal = $.mobile.filterbar.prototype.options.inset;
+
+ $.mobile.defaultTransition = "none";
+
+ module( "Filter Widget Core Functions" );
+
+ var searchFilterId = "#search-filter-test";
+
+ asyncTest( "Filter downs results when the user enters information", function() {
+ var $searchPage = $(searchFilterId);
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage(searchFilterId);
+ },
+ function() {
+ $searchPage.find('input').val('at');
+ $searchPage.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual($searchPage.find('li.ui-screen-hidden').length, 2);
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Redisplay results when user removes values", function() {
+ var $searchPage = $(searchFilterId);
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage(searchFilterId);
+ },
+ function() {
+ $searchPage.find('input').val('a');
+ $searchPage.find('input').trigger('change');
+ deepEqual($searchPage.find("li[style^='display: none;']").length, 0);
+ start();
+ }
+ ]);
+ });
+
+ asyncTest( "Filter downs results with multiple entries by user", function() {
+ var $searchPage = $("#search-filter-test-multiple");
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage("#search-filter-test-multiple");
+ },
+ function() {
+ // first input
+
+ $searchPage.find('input').val('a');
+ $searchPage.find('input').trigger('change');
+ window.setTimeout(function() {
+ deepEqual(
+ $searchPage.find('li.ui-screen-hidden').length,
+ 3,
+ "Filtering hides non matching columns"
+ );
+ // second input
+ $searchPage.find('input').val('aa');
+ $searchPage.find('input').trigger('change');
+ window.setTimeout(function() {
+ deepEqual(
+ $searchPage.find('li.ui-screen-hidden').length,
+ 4,
+ "Filtering again hides all columns"
+ );
+ // clear last input
+ $searchPage.find('input').val('a');
+ $searchPage.find('input').trigger('change');
+ window.setTimeout(function() {
+ deepEqual(
+ $searchPage.find('li.ui-screen-hidden').length,
+ 3,
+ "Removing one character shows some columns"
+ );
+ // empty input
+ $searchPage.find('input').val('');
+ $searchPage.find('input').trigger('change');
+ window.setTimeout(function() {
+ deepEqual(
+ $searchPage.find('li.ui-screen-hidden').length,
+ 0,
+ "Emptying input shows all columns"
+ );
+ start();
+ },500);
+ },500);
+ },500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Filter works fine with \\W- or regexp-special-characters",
+ function() {
+ var $searchPage = $(searchFilterId);
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage(searchFilterId);
+ },
+ function() {
+ $searchPage.find('input').val('*');
+ $searchPage.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual($searchPage.find('li.ui-screen-hidden').length, 4);
+ start();
+ }, 500);
+ }
+ ]
+ );
+ });
+
+ asyncTest( "Event filterbarbeforefilter firing", function() {
+ var $searchPage = $( searchFilterId );
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage( searchFilterId );
+ },
+
+ function() {
+ var beforeFilterCount = 0;
+ $searchPage.on( "filterbarbeforefilter", function( e ) {
+ beforeFilterCount += 1;
+ });
+
+ $searchPage.find( 'input' ).val( "a" );
+ $searchPage.find( 'input' ).trigger('input');
+ $searchPage.find( 'input' ).trigger('keyup');
+ $searchPage.find( 'input' ).trigger('change');
+ window.setTimeout(function() {
+ equal(
+ beforeFilterCount,
+ 1,
+ "filterbarbeforefilter should fire only once for the same value"
+ );
+ $searchPage.find( 'input' ).val( "ab" );
+ $searchPage.find( 'input' ).trigger('input');
+ $searchPage.find( 'input' ).trigger('keyup');
+ window.setTimeout(function() {
+ equal(
+ beforeFilterCount,
+ 2,
+ "filterbarbeforefilter should fire twice since value has changed"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Filter downs results and dividers when the user enters information",
+ function() {
+ var $searchPage = $("#search-filter-with-dividers-test");
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage("#search-filter-with-dividers-test");
+ },
+ // wait for the page to become active/enhanced
+ function(){
+ $searchPage.find('input').val('at');
+ $searchPage.find('input').trigger('change');
+ setTimeout(function() {
+ //there should be four hidden list entries
+ deepEqual(
+ $searchPage.find('li.ui-screen-hidden').length,
+ 4
+ );
+ //there should be two list entries that are list dividers and hidden
+ deepEqual(
+ $searchPage
+ .find('li.ui-screen-hidden:jqmData(role=list-divider)')
+ .length,
+ 2
+ );
+ //there should be two list entries that are not list dividers and hidden
+ deepEqual(
+ $searchPage
+ .find('li.ui-screen-hidden:not(:jqmData(role=list-divider))')
+ .length,
+ 2
+ );
+ start();
+ }, 500);
+ }
+ ]);
+ }
+ );
+
+ asyncTest( "Redisplay results when user removes values", function() {
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage("#search-filter-with-dividers-test");
+ },
+
+ function() {
+ $('.ui-page-active input').val('a');
+ $('.ui-page-active input').trigger('change');
+
+ setTimeout(function() {
+ deepEqual($('.ui-page-active input').val(), 'a');
+ deepEqual($('.ui-page-active li[style^="display: none;"]').length, 0);
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Dividers are hidden when preceding hidden rows and shown when preceding shown rows", function () {
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage("#search-filter-with-dividers-test");
+ },
+
+ function() {
+ var $page = $('.ui-page-active');
+
+ $page.find('input').val('at');
+ $page.find('input').trigger('change');
+
+ setTimeout(function() {
+ deepEqual($page.find('li:jqmData(role=list-divider):hidden').length, 2);
+ deepEqual($page.find('li:jqmData(role=list-divider):hidden + li:not(:jqmData(role=list-divider)):hidden').length, 2);
+ deepEqual($page.find('li:jqmData(role=list-divider):not(:hidden) + li:not(:jqmData(role=list-divider)):not(:hidden)').length, 2);
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Inset List View should refresh corner classes after filtering", 4 * 2, function () {
+ var checkClasses = function() {
+ var $page = $( ".ui-page-active" ),
+ $li = $page.find( "li:visible" );
+ ok($li.first().hasClass( "ui-first-child" ), $li.length+" li elements: First visible element should have class ui-first-child");
+ ok($li.last().hasClass( "ui-last-child" ), $li.length+" li elements: Last visible element should have class ui-last-child");
+ };
+
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage("#search-filter-inset-test");
+ },
+
+ function() {
+ var $page = $('.ui-page-active');
+ $.testHelper.sequence([
+ function() {
+ checkClasses();
+
+ $page.find('input').val('man');
+ $page.find('input').trigger('change');
+ },
+
+ function() {
+ checkClasses();
+
+ $page.find('input').val('at');
+ $page.find('input').trigger('change');
+ },
+
+ function() {
+ checkClasses();
+
+ $page.find('input').val('catwoman');
+ $page.find('input').trigger('change');
+ },
+
+ function() {
+ checkClasses();
+ start();
+ }
+ ], 50);
+ }
+ ]);
+ });
+
+ module( "Filter Widget Custom Filter", {
+ setup: function() {
+ var self = this;
+ this._refreshCornersCount = 0;
+ this._refreshCornersFn = $.mobile.filterbar.prototype._addFirstLastClasses;
+
+ // _refreshCorners is the last method called in the filter loop
+ // so we count the number of times _refreshCorners gets invoked to stop the test
+ $.mobile.filterbar.prototype._addFirstLastClasses = function() {
+ self._refreshCornersCount += 1;
+ self._refreshCornersFn.apply( this, arguments );
+ }
+ },
+ teardown: function() {
+ $.mobile.filterbar.prototype._refreshCorners = this._refreshCornersFn;
+ }
+ });
+
+ asyncTest( "Custom filterCallback should cause iteration on all list elements", function(){
+ var listPage = $( "#search-customfilter-test" ),
+ filterCallbackCount = 0,
+ expectedCount = 2 * listPage.find("li").length;
+ expect( 1 );
+
+ $.testHelper.pageSequence( [
+ function(){
+ //reset for relative url refs
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-customfilter-test" );
+ },
+
+ function() {
+ // set the listview instance callback
+ listPage.find( "ul" ).filterbar( "option", "filterCallback", function( text, searchValue, item ) {
+ filterCallbackCount += 1;
+ return text.toString().toLowerCase().indexOf( searchValue ) === -1;
+ });
+
+ // trigger a change in the search filter
+ listPage.find( "input" ).val( "at" ).trigger( "change" );
+ // need to wait because of the filterdelay
+ window.setTimeout(function() {
+ listPage.find( "input" ).val( "atw" ).trigger( "change" );
+ },500);
+ },
+
+ function() {
+ equal( filterCallbackCount, expectedCount, "filterCallback should be called exactly "+ expectedCount +" times" );
+ start();
+ }
+ ]);
+ });
+
+ asyncTest( "filterCallback can be altered after widget creation", function(){
+ var listPage = $( "#search-customfilter-test" ),
+ filterChangedCallbackCount = 0,
+ expectedCount = 1 * listPage.find("li").length,
+ runtest;
+ expect( 1 );
+
+ $.testHelper.pageSequence( [
+ function(){
+ //reset for relative url refs
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-customfilter-test" );
+ },
+
+ function() {
+ // set the filter instance callback
+ listPage.find( "ul" ).filterbar( "option", "filterCallback", function() {
+ filterChangedCallbackCount += 1;
+ });
+
+ listPage.find( "input" ).val( "foo" )
+ listPage.find( "input" ).trigger( "change" );
+ },
+
+ function() {
+ equal( filterChangedCallbackCount, expectedCount, "filterChangeCallback should be called exactly "+ expectedCount +" times" );
+ start();
+ }
+ ]);
+ });
+
+ module( "Filter Widget Reveal/Autocomplete" );
+
+ asyncTest( "Filter downs results when the user enters information", 3, function() {
+ var $searchPage = $( "#search-filter-reveal-test" );
+
+ $.testHelper.pageSequence([
+ function() {
+ $.mobile.changePage( $searchPage );
+ },
+
+ function() {
+ deepEqual( $searchPage.find( 'li.ui-screen-hidden' ).length, 22);
+ },
+
+ function() {
+ $searchPage.find( 'input' ).val( 'a' );
+ $searchPage.find( 'input' ).trigger('change');
+ window.setTimeout(function() {
+ deepEqual( $searchPage.find('li.ui-screen-hidden').length, 11);
+ },500);
+ },
+
+ function() {
+ $searchPage.find( 'input' ).val( '' );
+ $searchPage.find( 'input' ).trigger('change');
+ window.setTimeout(function() {
+ deepEqual( $searchPage.find('li.ui-screen-hidden').length, 22);
+ start();
+ },500);
+ }
+ ]);
+ });
+
+ module( "Filter Widget Configuration" );
+
+ asyncTest( "Custom id and classes are set on filter", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-custom-id-classes-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $list = $page.find( "ul" );
+
+ ok($filter.hasClass( "baz" ), "filter element has custom classed set by user");
+ ok($filter.attr( "id" ) === "foo", "filter has custom id");
+
+ $list.filterbar("destroy");
+
+ ok($page.find( ".ui-filter" ).length === 0, "filter can be destroyed using custom user id");
+ start();
+ }
+ ]);
+ });
+
+ asyncTest( "Placing the filter at a location specified by data-target", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-target-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $list = $page.find( "ul" );
+
+ ok($filter.parent().hasClass( "baz" ), "filter appended to element specified by data-target")
+
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual($list.find('li.ui-screen-hidden').length, 3);
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Selector attribute allows filtering of multiple datasets", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-selector-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $list_a = $page.find( "ul" ).eq(0),
+ $list_b = $page.find( "ul" ).eq(1);
+
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual($list_a.find('li.ui-screen-hidden').length, $list_b.find('li.ui-screen-hidden').length);
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Filter can be set pre-enhanced (if data-id is provided)", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-pre-enhance-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $list = $page.find( "ul" ).eq(0);
+
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $list.find('li.ui-screen-hidden').length,
+ 3,
+ "Custom filter can be used for filtering"
+ );
+ $list.filterbar( "destroy" );
+ ok(
+ $page.find( ".ui-filter" ).length === 1,
+ "Pre-enhanced filter element is not removed on destroy"
+ );
+ deepEqual(
+ $list.find('li.ui-screen-hidden').length,
+ 0,
+ "destroying a filter shows all elements"
+ );
+ start();
+ }, 500);
+ }
+ ]);
+ });
+
+ module( "Filter Widget Methods/Options" );
+
+ asyncTest( "Disabling, enabling text input", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-disable-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $list = $page.find( "ul" ).eq(0);
+
+ $list.filterbar( "disable" );
+
+ deepEqual(
+ $page.find('input').attr( "disabled" ),
+ "disabled",
+ "Setting disable option on widget (ul) disables filter textinput"
+ );
+
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $list.find('li.ui-screen-hidden').length,
+ 0,
+ "Disabled filters cannot filter"
+ );
+
+ $list.filterbar( "enable" );
+
+ deepEqual(
+ $page.find('input').attr( "disabled" ),
+ undefined,
+ "Enabling widget also enables textinput"
+ );
+
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $list.find('li.ui-screen-hidden').length,
+ 3,
+ "Enabled filter is working again"
+ );
+ start();
+ },500);
+ }, 500);
+ }
+ ]);
+ });
+
+ module( "Filter Widget Using Different Elements" );
+
+ asyncTest( "Filtering Table Rows based on Cells", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-table-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $table = $page.find( "table" ).eq(0);
+
+ $page.find('input').val('12:12');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $table.find('.ui-screen-hidden').length,
+ 4,
+ "Filtering table rows hides based on table cell values"
+ );
+ $page.find('input').val('');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $table.find('.ui-screen-hidden').length,
+ 0,
+ "Removing filter value shows all table rows again"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Controlgroup Search Filter", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-controlgroup-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $controlgroup = $page.find( "div.helper" );
+
+ // filter
+ $page.find('input').val('ac');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $controlgroup.find('.ui-screen-hidden').length,
+ 3,
+ "Filtering controlgroup input/a buttons by value"
+ );
+
+ // clear
+ $page.find('input').val('');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $controlgroup.find('.ui-screen-hidden').length,
+ 0,
+ "Removing filter value shows all controlgroup buttons again"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Native Select Search Filter", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-select-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $select = $page.find( ".ui-select" );
+
+ // filter
+ $page.find('input').val('a');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $select.find('.ui-screen-hidden').length,
+ 9,
+ "Filtering select options by option text"
+ );
+
+ // clear
+ $page.find('input').val('');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $select.find('.ui-screen-hidden').length,
+ 0,
+ "Removing filter value shows all select options again"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Native Select Search Filter - using data-filtertext", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-select-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $select = $page.find( ".ui-select" );
+
+ // filter
+ $page.find('input').val('this goes');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $select.find('.ui-screen-hidden').length,
+ 9,
+ "Filtering select options by option text"
+ );
+
+ // clear
+ $page.find('input').val('');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $select.find('.ui-screen-hidden').length,
+ 0,
+ "Removing filter value shows all select options again"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Native Select Search Filter - select is hidden if no options match", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-select-test" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ),
+ $select = $page.find( ".ui-select" );
+
+ // filter
+ $page.find('input').val('aaaaaaaa');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ ok(
+ $select.is( '.ui-screen-hidden' ),
+ "Select element itself is hidden when no options match filter"
+ );
+
+ // clear
+ $page.find('input').val('');
+ $page.find('input').trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $select.is( '.ui-screen-hidden' ),
+ false,
+ "Clearing filter also shows select again"
+ );
+ start();
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+ /* ================= the next block is tested on different elements ======= */
+ asyncTest( "Random Elements Filter -
", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-random-test-p" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ).eq(1),
+ $selection = $page.find( ".elements_p" );
+ $selection2 = $page.find( ".elements_b_p" );
+
+ // filter
+ $page.find('input').eq(0).val('b');
+ $page.find('input').eq(0).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 5,
+ "Filtering
elements by text"
+ );
+
+ // clear
+ $page.find('input').eq(0).val('');
+ $page.find('input').eq(0).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 0,
+ "Clearing filter shows all elements again"
+ );
+
+ // filter second set
+ $page.find('input').eq(1).val('f');
+ $page.find('input').eq(1).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection2.find('.ui-screen-hidden').length,
+ 5,
+ "Filtering
on 2nd set using data-filtertext works"
+ );
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 0,
+ "Filtering
on 2nd set does not change first dataset"
+ );
+ ok((
+ $selection2.children().not( '.ui-screen-hidden' ).length === 1 &&
+ $selection2.children().not( '.ui-screen-hidden' ).text() === "a"
+ ), "Filtering works on data-filtertext and not text"
+ );
+ // clear
+ $page.find('input').eq(1).val('');
+ $page.find('input').eq(1).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection2.find('.ui-screen-hidden').length,
+ 0,
+ "Clearing filter shows all elements again"
+ );
+ $page.find( "#ticktick_p" ).filterbar("destroy");
+ ok(
+ $page.find(".ui-filter").length === 1,
+ "Destroying one filter does not destroy another filter"
+ );
+ start();
+ },500);
+ }, 500);
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+
+ asyncTest( "Random Elements Filter - ", function () {
+ $.testHelper.pageSequence( [
+ function(){
+ $.mobile.changePage( home );
+ },
+
+ function() {
+ $.mobile.changePage( "#search-random-test-span" );
+ },
+
+ function() {
+ var $page = $( ".ui-page-active" ),
+ $filter = $page.find( ".ui-filter" ).eq(1),
+ $selection = $page.find( ".elements_span" );
+ $selection2 = $page.find( ".elements_b_span" );
+
+ // filter
+ $page.find('input').eq(0).val('b');
+ $page.find('input').eq(0).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 5,
+ "Filtering elements by text"
+ );
+
+ // clear
+ $page.find('input').eq(0).val('');
+ $page.find('input').eq(0).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 0,
+ "Clearing filter shows all elements again"
+ );
+
+ // filter second set
+ $page.find('input').eq(1).val('f');
+ $page.find('input').eq(1).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection2.find('.ui-screen-hidden').length,
+ 5,
+ "Filtering on 2nd set using data-filtertext works"
+ );
+ deepEqual(
+ $selection.find('.ui-screen-hidden').length,
+ 0,
+ "Filtering on 2nd set does not change first dataset"
+ );
+ ok((
+ $selection2.children().not( '.ui-screen-hidden' ).length === 1 &&
+ $selection2.children().not( '.ui-screen-hidden' ).text() === "a"
+ ), "Filtering works on data-filtertext and not text"
+ );
+ // clear
+ $page.find('input').eq(1).val('');
+ $page.find('input').eq(1).trigger('change');
+ setTimeout(function() {
+ deepEqual(
+ $selection2.find('.ui-screen-hidden').length,
+ 0,
+ "Clearing filter shows all elements again"
+ );
+ $page.find( "#ticktick_span" ).filterbar("destroy");
+ ok(
+ $page.find(".ui-filter").length === 1,
+ "Destroying one filter does not destroy another filter"
+ );
+ start();
+ },500);
+ }, 500);
+ }, 500);
+ }, 500);
+ }
+ ]);
+ });
+})(jQuery);
diff --git a/tests/integration/filter/index.html b/tests/integration/filter/index.html
new file mode 100644
index 00000000000..7ef9bd74d5b
--- /dev/null
+++ b/tests/integration/filter/index.html
@@ -0,0 +1,450 @@
+
+
+
+
+
+ jQuery Mobile Filter Integration Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Split List View
+
+
+
+ - a is for aquaman
+ - b is for batman
+ - c is for catwoman
+ - d is for darkwing
+
+
+
+
+
+
+
+
Split List View Multiple Entries
+
+
+
+
+
+
+
+
Filtered List View
+
+
+
+ - a is for aquaman
+ - b is for batman
+ - c is for catwoman
+ - d is for darkwing
+
+
+
+
+
+
+
+
Split List View
+
+
+
+ - a
+ - a is for aquaman
+ - b
+ - b is for batman
+ - c
+ - c is for catwoman
+ - d
+ - d is for darkwing
+
+
+
+
+
+
+
+
Inset Filter List View
+
+
+
+ - a is for aquaman
+ - b is for batman
+ - c is for catwoman
+ - d is for darkwing
+
+
+
+
+
+
+
+
Reveal Listview
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+ - Dodge
+ - Ferrari
+ - Ford
+ - GMC
+ - Honda
+ - Hyundai
+ - Infiniti
+ - Jeep
+ - Kia
+ - Lexus
+ - Mini
+ - Nissan
+ - Porsche
+ - Subaru
+ - Toyota
+ - Volkswagon
+ - Volvo
+
+
+
+
+
+
+
+
Using custom id and classes
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
+
+
+
+
+
+
Using target selector
+
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
a
+
b
+
a
+
b
+
a
+
b
+
+
b
+
+
+
+
+
+
+
Using selector to filter multiple sets
+
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
+
+
+
+
+
+
Using pre-enhanced filter
+
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
+
+
+
+
+
+
Disabling a filter
+
+
+
+ - Acura
+ - Audi
+ - BMW
+ - Cadillac
+ - Chrysler
+
+
+
+
+
+
+
+
Filtering different elements: table
+
+
+
+
+
+ Company |
+ Last Trade |
+ Trade Time |
+ Change |
+ Prev Close |
+ Open |
+ Stuff |
+
+
+ Bid |
+ Ask |
+ 1y Target Est |
+
+
+
+
+ GOOG Google Inc. |
+ 597.74 |
+ 12:12PM |
+ 14.81 (2.54%) |
+ 582.93 |
+ 597.95 |
+ 597.73 x 100 |
+ 597.91 x 300 |
+ 731.10 |
+
+
+ AAPL Apple Inc. |
+ 378.94 |
+ 12:22PM |
+ 5.74 (1.54%) |
+ 373.20 |
+ 381.02 |
+ 378.92 x 300 |
+ 378.99 x 100 |
+ 505.94 |
+
+
+ AMZN Amazon.com Inc. |
+ 191.55 |
+ 12:23PM |
+ 3.16 (1.68%) |
+ 188.39 |
+ 194.99 |
+ 191.52 x 300 |
+ 191.58 x 100 |
+ 240.32 |
+
+
+ ORCL Oracle Corporation |
+ 31.15 |
+ 12:44PM |
+ 1.41 (4.72%) |
+ 29.74 |
+ 30.67 |
+ 31.14 x 6500 |
+ 31.15 x 3200 |
+ 36.11 |
+
+
+ MSFT Microsoft Corporation |
+ 25.50 |
+ 12:27PM |
+ 0.66 (2.67%) |
+ 24.84 |
+ 25.37 |
+ 25.50 x 71100 |
+ 25.51 x 17800 |
+ 31.50 |
+
+
+
+
+
+
+
+
+
+
Filtering different elements: controlgroup
+
+
+
+
+
+
+
+
Filtering different elements: select
+
+
+
+
+
+
+
+
+
+
+
+
Filtering different elements: p
+
+
+
+
+
a
+
b
+
c
+
d
+
e
+
f
+
+
+
+
a
+
b
+
c
+
d
+
e
+
f
+
+
+
+
+
+
+
+
Filtering different elements: span
+
+
+
+
+ a
+ b
+ c
+ d
+ e
+ f
+
+
+
+ a
+ b
+ c
+ d
+ e
+ f
+
+
+
+
+
+