Skip to content
This repository has been archived by the owner on Oct 8, 2021. It is now read-only.

Commit

Permalink
filter widget first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
frequent committed Jul 9, 2013
1 parent de6c4f9 commit f41dde4
Show file tree
Hide file tree
Showing 4 changed files with 1,731 additions and 0 deletions.
25 changes: 25 additions & 0 deletions 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;
}



314 changes: 314 additions & 0 deletions 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 = $( "<div>", {
"class": o.classes + " ui-filter ",
"role": "search",
"id" : o.id || "ui-filter-" + self.uuid
}),
search = $( "<input>", {
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");

0 comments on commit f41dde4

Please sign in to comment.