define("dojox/mobile/FilteredListMixin", [
"require",
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom",
"dojo/dom-class",
"dojo/dom-construct",
"dojo/aspect",
"dijit/registry",
"./SearchBox",
"./ScrollableView",
"./viewRegistry"
], function(require, array, declare, lang, dom, domClass, domConstruct,
aspect, registry, SearchBox, ScrollableView, viewRegistry){
// module:
// dojox/mobile/FilteredListMixin
return declare("dojox.mobile.FilteredListMixin", null, {
// summary:
// Mixin for filtered lists.
// description:
// This mixin adds filtering capabilities to all dojox/mobile list widgets:
// dojox/mobile/RoundRectList and any of its subclasses (RoundRectStoreList,
// RoundRectDataList, EdgeToEdgeList, EdgeToEdgeStoreList, EdgeToEdgeDataList).
// When mixing this class into a list widget, the list items are dynamically
// filtered depending on the filtering string that the user enters in a
// dojox/mobile/SearchBox.
//
// This mixin supports the following use-cases:
// 1. For user's convenience, by simply mixing this class into a list widget
// the mixin creates a dojox/mobile/SearchBox and a dojox/mobile/ScrollableView.
// The list is placed inside the ScrollableView and the SearchBox, which allows
// filtering the list, is placed on top of the ScrollableView.
// 2. Alternatively, the user can create (and style) the instance of dojox/mobile/SearchBox,
// and specify its id using the property filterBoxRef of this mixin. This allows
// placing the SearchBox anywhere in the DOM, while the mixin takes care of
// the necessary glue to ensure the list is filtered according to the filter criteria
// entered in the SearchBox.
//
// The filtering works for lists backed by a store (dojo/store or dojo/data), as well
// as for lists not backed by a store. When filtering a list backed by a store
// containing hierarchical data (data items that are children of a parent data item),
// the store must support recursive search queries such that the filtering can match
// child items.
//
// For configuration purposes, the instance of dojox/mobile/SearchBox can be retrieved
// using the method getFilterBox(). If a dojox/mobile/ScrollableView is created by
// this mixin, it can be retrieved using getScrollableView().
//
// example:
// |
// |
// |
// |
// Implementation notes:
// - The mixin requires dojox/mobile/ScrollableView statically. It could be required
// dynamically, only when needed, that is in the use-case when the mixin creates the
// ScrollableView by itself. But this would create an usability trouble: if the user
// would want to get the instance of ScrollableView in a dojo/ready (say, for configuring
// it), he would need to require it upfront, to cope with the case of asynchronous
// loading. Thus, requiring it statically has been preferred, because it avoids this
// constraint on user's side and it is not a serious overhead, because in practice
// the filtering is used for long lists, for which a ScrollableView is anyway likely
// to be used.
// - Differently, the loading of the store/data modules is performed dynamically,
// in order to avoid their overhead when they are not actually needed.
// filterBoxRef: String
// The reference for the search box allowing to enter the filtering criteria.
// Only used at construction time:
// - If unspecified, the mixin creates a dojox/mobile/SearchBox and
// a dojox/mobile/ScrollableView. The list is placed inside the ScrollableView and the
// SearchBox, wrapped in a DIV, is placed on top of the ScrollableView.
// - If the string is the id of a widget which is an instance of dojox/mobile/SearchBox
// or a subclass, the mixin uses this SearchBox for filtering the list.
// - If the id is specified but does not reference a dojox/mobile/SearchBox or
// subclass, an error is thrown.
filterBoxRef: null,
// placeHolder: String
// Defines a hint to help users fill out the input field (as defined in HTML 5) of the
// dojox/mobile/SearchBox. This should only contain plain text (no HTML markup).
// When the SearchBox is provided by the user (not created by this mixin), its placeHolder
// property takes precedence.
placeHolder: "",
// filterBoxVisible: Boolean
// A flag which allows to show or hide the dojox/mobile/SearchBox associated with
// the list.
filterBoxVisible: true,
// _filterBox: [private] dojox/mobile/SearchBox
// The instance of dojox/mobile/SearchBox used by this mixin.
// Stored for getFilterBox().
_filterBox: null,
// _createdFilterBox: [private] dojox/mobile/SearchBox
// The instance of dojox/mobile/SearchBox created by this mixin, or null if none
// has been created. Stored for being able to destroy it together with the list widget.
_createdFilterBox: null,
// _createdScrollableView: [private] dojox/mobile/ScrollableView
// The instance of dojox/mobile/ScrollableView created by this mixin, if any.
// Stored for getScrollableView() and for being able to destroy it together
// with the list widget.
_createdScrollableView: null,
startup: function(){
if(this._started){ return; }
this.inherited(arguments);
if(this.filterBoxRef){
// Case #1: search box provided by the user
this._filterBox = registry.byId(this.filterBoxRef);
if (this._filterBox && this._filterBox.isInstanceOf(SearchBox)){
// If the list is backed by a dojox/mobile/_StoreListMixin, it
// has a labelProperty which is given precedence.
this._filterBox.set("searchAttr", this.labelProperty ? this.labelProperty : "label");
if(!this._filterBox.placeHolder){
// Give precedence to the placeHolder that may be specified on the provided SearchBox
this._filterBox.set("placeHolder", this.placeHolder);
}
this._filterBox.on("search", lang.hitch(this, "_onFilter"));
}else{
throw new Error("Cannot find a widget of type dojox/mobile/SearchBox or subclass " +
"at the specified filterBoxRef: " + this.filterBoxRef);
}
}else{
// Case #2: automatic mode. The mixin creates a SearchBox and a ScrollableView.
this._filterBox =
new SearchBox({
// If the list is backed by a dojox/mobile/_StoreListMixin, it
// has a labelProperty which is given precedence.
searchAttr: this.labelProperty ? this.labelProperty : "label",
ignoreCase: true,
incremental: true,
onSearch: lang.hitch(this, "_onFilter"),
selectOnClick: true,
placeHolder: this.placeHolder
});
// Store them to be able to destroy them together with the list (see destroy()).
this._createdFilterBox = this._filterBox;
this._createdScrollableView = new ScrollableView();
var currentDomNode = this.domNode,
listParentNode = this.domNode.parentNode;
listParentNode.replaceChild(this._createdScrollableView.domNode, this.domNode);
// Put the list inside the ScrollableView:
domConstruct.place(currentDomNode, this._createdScrollableView.containerNode);
var searchBoxParentDiv = domConstruct.create("div");
// Put the SearchBox as child of the DIV
domConstruct.place(this._createdFilterBox.domNode, searchBoxParentDiv);
// Put the DIV as sibling of the ScrollableView:
domConstruct.place(searchBoxParentDiv, this._createdScrollableView.domNode, "before");
if(this.filterBoxClass){
// Only adding the class when the mixin creates the SearchBox by itself.
domClass.add(searchBoxParentDiv, this.filterBoxClass);
}
this._createdFilterBox.startup();
this._createdScrollableView.startup();
this._createdScrollableView.resize();
}
// Do not use this.getScrollableView() here, because this doesn't cover the
// use-case when the scrollable is not created by this mixin.
var sv = viewRegistry.getEnclosingScrollable(this.domNode);
if(sv){
this.connect(sv, "onFlickAnimationEnd", lang.hitch(this, function(){
if(!this._filterBox.focusNode.value){ // if search criteria is empty
// store the scroll position such that we can reset the
// initial scroll when the user goes back to the unfiltered
// list (as done by some native mobile apps).
this._previousUnfilteredScrollPos = sv.getPos();
}
}));
}
if(!this.store){
this._createStore(this._initStore);
}else{
this._initStore();
}
},
_setFilterBoxVisibleAttr: function(/* Boolean */ visible){
// tags:
// private
this._set("filterBoxVisible", visible);
if (this._filterBox && this._filterBox.domNode){
this._filterBox.domNode.style.display = visible ? "" : "none";
}
},
_setPlaceHolderAttr: function(/* String */ placeHolder){
// tags:
// private
this._set("placeHolder", placeHolder);
if (this._filterBox){ // allow update after construction time
this._filterBox.set("placeHolder", placeHolder);
}
},
getFilterBox: function(){
// summary:
// Returns the dojox/mobile/SearchBox widget used for entering the filtering criteria.
// If an instance has been referenced at construction time using the property filterBoxRef,
// this instance is returned. Otherwise, returns the instance created by the mixin.
// This function allows the user to get the instance of SearchBox in order to customize
// its parameters.
return this._filterBox;
},
getScrollableView: function(){
// summary:
// Returns the instance of dojox/mobile/ScrollableView created by this mixin,
// or null if none has been created. The mixin creates a ScrollableView if and
// only if the property filterBoxRef is unspecified.
// This function allows the user to get the instance of ScrollableView in order to
// customize its parameters.
return this._createdScrollableView;
},
_initStore: function(){
// description:
// Initializes the store.
// tags:
// private
var store = this.store;
if(!store.get || !store.query){ // if old store (dojo/data)
// Detect the old dojo/data stores (since the stores don't actually extend a common
// base class, there is no direct way to do this check. Hence we rely on the presence
// or absence of these two properties of the new stores which are required for the
// list widgets).
// TODO: to be removed when removing the support for lists backed by the old dojo/data
// (EdgeToEdgeDataStore, RoundRectDataList).
require(["dojo/store/DataStore"], lang.hitch(this, function(DataStore){
// wrap the dojo/data store into a dojo/store
store = new DataStore({store: store});
this._filterBox.store = store;
}));
}else{
this._filterBox.store = store;
}
},
_createStore: function(initStoreFunction/* Function */){
// summary:
// Creates the store.
// description:
// This method is used when the list is not backed by a store. In this case,
// a store is created and filled with items containing the text of the list items.
// tags:
// private
require(["./_StoreListMixin", "dojo/store/Memory"], lang.hitch(this, function(module, Memory){
declare.safeMixin(this, new module());
this.append = true; // to avoid that _StoreListMixin.generateList destroys the items
// _StoreListMixin.createListItem creates a new item. Instead, we want to reuse the
// original item instance, hence:
this.createListItem = function(/*Object*/item){
return item.listItem;
};
aspect.before(this, "generateList", function(){
// remove all children
array.forEach(this.getChildren(), function(child){
child.domNode.parentNode.removeChild(child.domNode);
});
});
// Collect the text from the list items
var items = [];
var text = null;
array.forEach(this.getChildren(), function(child){
text = child.label ? child.label : (child.domNode.innerText || child.domNode.textContent);
items.push({label: text, listItem: child});
});
var listData = {items: items};
// store for the dojox/mobile/EdgeToEdgeStoreList
var store = new Memory({idProperty:"label", data: listData});
this.store = null;
this.query = {};
this.setStore(store, this.query, this.queryOptions);
lang.hitch(this, initStoreFunction)();
}));
},
_onFilter: function(results, query, options){
// summary:
// Internal handler for filtering events.
// tags:
// private
if(this.onFilter(results, query, options) === false){ return; } // user's filtering action
this.setQuery(query);
// Do not use this.getScrollableView() because this doesn't cover the
// use-case when the scrollable is not created by this mixin.
var sv = viewRegistry.getEnclosingScrollable(this.domNode);
if(sv){
// When the user goes back to the unfiltered list, restore the previous
// scroll position stored for unfiltered list (as done by some native mobile apps).
// Otherwise, reset the scroll position, to ensure that the new subset of
// items is visible.
sv.scrollTo(this._filterBox.focusNode.value ?
{x:0, y:0} :
this._previousUnfilteredScrollPos || {x:0, y:0});
}
},
onFilter: function(/*===== results, query, options =====*/){
// summary:
// User-defined function to handle filter actions. If the function returns false,
// the filtering is cancelled.
// tags:
// callback
},
destroy: function(/*Boolean?*/ preserveDom){
// summary:
// Destroys the widget. If the list has created dojox/mobile/SearchBox
// or dojox/mobile/ScrollableView widgets, these widgets are also destroyed.
// preserveDom: Boolean
// If true, this method will leave the original DOM structure alone.
this.inherited(arguments);
// Only destroy widgets created (thus, owned) by this mixin (if any).
if(this._createdFilterBox){
this._createdFilterBox.destroy(preserveDom);
this._createdFilterBox = null;
}
if(this._createdScrollableView){
this._createdScrollableView.destroy(preserveDom);
this._createdScrollableView = null;
}
}
});
});