webui-aria2/js/libs/dojox/mobile/app/List.js.uncompressed.js
2012-05-01 19:52:07 +08:00

650 lines
17 KiB
JavaScript

//>>built
// wrapped by build app
define("dojox/mobile/app/List", ["dijit","dojo","dojox","dojo/require!dojo/string,dijit/_WidgetBase"], function(dijit,dojo,dojox){
dojo.provide("dojox.mobile.app.List");
dojo.experimental("dojox.mobile.app.List");
dojo.require("dojo.string");
dojo.require("dijit._WidgetBase");
(function(){
var templateCache = {};
dojo.declare("dojox.mobile.app.List", dijit._WidgetBase, {
// summary:
// A templated list widget. Given a simple array of data objects
// and a HTML template, it renders a list of elements, with
// support for a swipe delete action. An optional template
// can be provided for when the list is empty.
// items: Array
// The array of data items that will be rendered.
items: null,
// itemTemplate: String
// The URL to the HTML file containing the markup for each individual
// data item.
itemTemplate: "",
// emptyTemplate: String
// The URL to the HTML file containing the HTML to display if there
// are no data items. This is optional.
emptyTemplate: "",
// dividerTemplate: String
// The URL to the HTML file containing the markup for the dividers
// between groups of list items
dividerTemplate: "",
// dividerFunction: Function
// Function to create divider elements. This should return a divider
// value for each item in the list
dividerFunction: null,
// labelDelete: String
// The label to display for the Delete button
labelDelete: "Delete",
// labelCancel: String
// The label to display for the Cancel button
labelCancel: "Cancel",
// controller: Object
//
controller: null,
// autoDelete: Boolean
autoDelete: true,
// enableDelete: Boolean
enableDelete: true,
// enableHold: Boolean
enableHold: true,
// formatters: Object
// A name/value map of functions used to format data for display
formatters: null,
// _templateLoadCount: Number
// The number of templates remaining to load before the list renders.
_templateLoadCount: 0,
// _mouseDownPos: Object
// The coordinates of where a mouseDown event was detected
_mouseDownPos: null,
baseClass: "list",
constructor: function(){
this._checkLoadComplete = dojo.hitch(this, this._checkLoadComplete);
this._replaceToken = dojo.hitch(this, this._replaceToken);
this._postDeleteAnim = dojo.hitch(this, this._postDeleteAnim);
},
postCreate: function(){
var _this = this;
if(this.emptyTemplate){
this._templateLoadCount++;
}
if(this.itemTemplate){
this._templateLoadCount++;
}
if(this.dividerTemplate){
this._templateLoadCount++;
}
this.connect(this.domNode, "onmousedown", function(event){
var touch = event;
if(event.targetTouches && event.targetTouches.length > 0){
touch = event.targetTouches[0];
}
// Find the node that was tapped/clicked
var rowNode = _this._getRowNode(event.target);
if(rowNode){
// Add the rows data to the event so it can be picked up
// by any listeners
_this._setDataInfo(rowNode, event);
// Select and highlight the row
_this._selectRow(rowNode);
// Record the position that was tapped
_this._mouseDownPos = {
x: touch.pageX,
y: touch.pageY
};
_this._dragThreshold = null;
}
});
this.connect(this.domNode, "onmouseup", function(event){
// When the mouse/finger comes off the list,
// call the onSelect function and deselect the row.
if(event.targetTouches && event.targetTouches.length > 0){
event = event.targetTouches[0];
}
var rowNode = _this._getRowNode(event.target);
if(rowNode){
_this._setDataInfo(rowNode, event);
if(_this._selectedRow){
_this.onSelect(rowNode._data, rowNode._idx, rowNode);
}
this._deselectRow();
}
});
// If swipe-to-delete is enabled, listen for the mouse moving
if(this.enableDelete){
this.connect(this.domNode, "mousemove", function(event){
dojo.stopEvent(event);
if(!_this._selectedRow){
return;
}
var rowNode = _this._getRowNode(event.target);
// Still check for enableDelete in case it's changed after
// this listener is added.
if(_this.enableDelete && rowNode && !_this._deleting){
_this.handleDrag(event);
}
});
}
// Put the data and index onto each onclick event.
this.connect(this.domNode, "onclick", function(event){
if(event.touches && event.touches.length > 0){
event = event.touches[0];
}
var rowNode = _this._getRowNode(event.target, true);
if(rowNode){
_this._setDataInfo(rowNode, event);
}
});
// If the mouse or finger moves off the selected row,
// deselect it.
this.connect(this.domNode, "mouseout", function(event){
if(event.touches && event.touches.length > 0){
event = event.touches[0];
}
if(event.target == _this._selectedRow){
_this._deselectRow();
}
});
// If no item template has been provided, it is an error.
if(!this.itemTemplate){
throw Error("An item template must be provided to " + this.declaredClass);
}
// Load the item template
this._loadTemplate(this.itemTemplate, "itemTemplate", this._checkLoadComplete);
if(this.emptyTemplate){
// If the optional empty template has been provided, load it.
this._loadTemplate(this.emptyTemplate, "emptyTemplate", this._checkLoadComplete);
}
if(this.dividerTemplate){
this._loadTemplate(this.dividerTemplate, "dividerTemplate", this._checkLoadComplete);
}
},
handleDrag: function(event){
// summary:
// Handles rows being swiped for deletion.
var touch = event;
if(event.targetTouches && event.targetTouches.length > 0){
touch = event.targetTouches[0];
}
// Get the distance that the mouse or finger has moved since
// beginning the swipe action.
var diff = touch.pageX - this._mouseDownPos.x;
var absDiff = Math.abs(diff);
if(absDiff > 10 && !this._dragThreshold){
// Make the user drag the row 60% of the width to remove it
this._dragThreshold = dojo.marginBox(this._selectedRow).w * 0.6;
if(!this.autoDelete){
this.createDeleteButtons(this._selectedRow);
}
}
this._selectedRow.style.left = (absDiff > 10 ? diff : 0) + "px";
// If the user has dragged the row more than the threshold, slide
// it off the screen in preparation for deletion.
if(this._dragThreshold && this._dragThreshold < absDiff){
this.preDelete(diff);
}
},
handleDragCancel: function(){
// summary:
// Handle a drag action being cancelled, for whatever reason.
// Reset handles, remove CSS classes etc.
if(this._deleting){
return;
}
dojo.removeClass(this._selectedRow, "hold");
this._selectedRow.style.left = 0;
this._mouseDownPos = null;
this._dragThreshold = null;
this._deleteBtns && dojo.style(this._deleteBtns, "display", "none");
},
preDelete: function(currentLeftPos){
// summary:
// Slides the row offscreen before it is deleted
// TODO: do this with CSS3!
var self = this;
this._deleting = true;
dojo.animateProperty({
node: this._selectedRow,
duration: 400,
properties: {
left: {
end: currentLeftPos +
((currentLeftPos > 0 ? 1 : -1) * this._dragThreshold * 0.8)
}
},
onEnd: dojo.hitch(this, function(){
if(this.autoDelete){
this.deleteRow(this._selectedRow);
}
})
}).play();
},
deleteRow: function(row){
// First make the row invisible
// Put it back where it came from
dojo.style(row, {
visibility: "hidden",
minHeight: "0px"
});
dojo.removeClass(row, "hold");
this._deleteAnimConn =
this.connect(row, "webkitAnimationEnd", this._postDeleteAnim);
dojo.addClass(row, "collapsed");
},
_postDeleteAnim: function(event){
// summary:
// Completes the deletion of a row.
if(this._deleteAnimConn){
this.disconnect(this._deleteAnimConn);
this._deleteAnimConn = null;
}
var row = this._selectedRow;
var sibling = row.nextSibling;
var prevSibling = row.previousSibling;
// If the previous node is a divider and either this is
// the last element in the list, or the next node is
// also a divider, remove the divider for the deleted section.
if(prevSibling && prevSibling._isDivider){
if(!sibling || sibling._isDivider){
prevSibling.parentNode.removeChild(prevSibling);
}
}
row.parentNode.removeChild(row);
this.onDelete(row._data, row._idx, this.items);
// Decrement the index of each following row
while(sibling){
if(sibling._idx){
sibling._idx--;
}
sibling = sibling.nextSibling;
}
dojo.destroy(row);
// Fix up the 'first' and 'last' CSS classes on the rows
dojo.query("> *:not(.buttons)", this.domNode).forEach(this.applyClass);
this._deleting = false;
this._deselectRow();
},
createDeleteButtons: function(aroundNode){
// summary:
// Creates the two buttons displayed when confirmation is
// required before deletion of a row.
// aroundNode:
// The DOM node of the row about to be deleted.
var mb = dojo.marginBox(aroundNode);
var pos = dojo._abs(aroundNode, true);
if(!this._deleteBtns){
// Create the delete buttons.
this._deleteBtns = dojo.create("div",{
"class": "buttons"
}, this.domNode);
this.buttons = [];
this.buttons.push(new dojox.mobile.Button({
btnClass: "mblRedButton",
label: this.labelDelete
}));
this.buttons.push(new dojox.mobile.Button({
btnClass: "mblBlueButton",
label: this.labelCancel
}));
dojo.place(this.buttons[0].domNode, this._deleteBtns);
dojo.place(this.buttons[1].domNode, this._deleteBtns);
dojo.addClass(this.buttons[0].domNode, "deleteBtn");
dojo.addClass(this.buttons[1].domNode, "cancelBtn");
this._handleButtonClick = dojo.hitch(this._handleButtonClick);
this.connect(this._deleteBtns, "onclick", this._handleButtonClick);
}
dojo.removeClass(this._deleteBtns, "fade out fast");
dojo.style(this._deleteBtns, {
display: "",
width: mb.w + "px",
height: mb.h + "px",
top: (aroundNode.offsetTop) + "px",
left: "0px"
});
},
onDelete: function(data, index, array){
// summary:
// Called when a row is deleted
// data:
// The data related to the row being deleted
// index:
// The index of the data in the total array
// array:
// The array of data used.
array.splice(index, 1);
// If the data is empty, rerender in case an emptyTemplate has
// been provided
if(array.length < 1){
this.render();
}
},
cancelDelete: function(){
// summary:
// Cancels the deletion of a row.
this._deleting = false;
this.handleDragCancel();
},
_handleButtonClick: function(event){
// summary:
// Handles the click of one of the deletion buttons, either to
// delete the row or to cancel the deletion.
if(event.touches && event.touches.length > 0){
event = event.touches[0];
}
var node = event.target;
if(dojo.hasClass(node, "deleteBtn")){
this.deleteRow(this._selectedRow);
}else if(dojo.hasClass(node, "cancelBtn")){
this.cancelDelete();
}else{
return;
}
dojo.addClass(this._deleteBtns, "fade out");
},
applyClass: function(node, idx, array){
// summary:
// Applies the 'first' and 'last' CSS classes to the relevant
// rows.
dojo.removeClass(node, "first last");
if(idx == 0){
dojo.addClass(node, "first");
}
if(idx == array.length - 1){
dojo.addClass(node, "last");
}
},
_setDataInfo: function(rowNode, event){
// summary:
// Attaches the data item and index for each row to any event
// that occurs on that row.
event.item = rowNode._data;
event.index = rowNode._idx;
},
onSelect: function(data, index, rowNode){
// summary:
// Dummy function that is called when a row is tapped
},
_selectRow: function(row){
// summary:
// Selects a row, applies the relevant CSS classes.
if(this._deleting && this._selectedRow && row != this._selectedRow){
this.cancelDelete();
}
if(!dojo.hasClass(row, "row")){
return;
}
if(this.enableHold || this.enableDelete){
dojo.addClass(row, "hold");
}
this._selectedRow = row;
},
_deselectRow: function(){
// summary:
// Deselects a row, and cancels any drag actions that were
// occurring.
if(!this._selectedRow || this._deleting){
return;
}
this.handleDragCancel();
dojo.removeClass(this._selectedRow, "hold");
this._selectedRow = null;
},
_getRowNode: function(fromNode, ignoreNoClick){
// summary:
// Gets the DOM node of the row that is equal to or the parent
// of the node passed to this function.
while(fromNode && !fromNode._data && fromNode != this.domNode){
if(!ignoreNoClick && dojo.hasClass(fromNode, "noclick")){
return null;
}
fromNode = fromNode.parentNode;
}
return fromNode == this.domNode ? null : fromNode;
},
applyTemplate: function(template, data){
return dojo._toDom(dojo.string.substitute(
template, data, this._replaceToken, this.formatters || this));
},
render: function(){
// summary:
// Renders the list.
// Delete all existing nodes, except the deletion buttons.
dojo.query("> *:not(.buttons)", this.domNode).forEach(dojo.destroy);
// If there is no data, and an empty template has been provided,
// render it.
if(this.items.length < 1 && this.emptyTemplate){
dojo.place(dojo._toDom(this.emptyTemplate), this.domNode, "first");
}else{
this.domNode.appendChild(this._renderRange(0, this.items.length));
}
if(dojo.hasClass(this.domNode.parentNode, "mblRoundRect")){
dojo.addClass(this.domNode.parentNode, "mblRoundRectList")
}
var divs = dojo.query("> .row", this.domNode);
if(divs.length > 0){
dojo.addClass(divs[0], "first");
dojo.addClass(divs[divs.length - 1], "last");
}
},
_renderRange: function(startIdx, endIdx){
var rows = [];
var row, i;
var frag = document.createDocumentFragment();
startIdx = Math.max(0, startIdx);
endIdx = Math.min(endIdx, this.items.length);
for(i = startIdx; i < endIdx; i++){
// Create a document fragment containing the templated row
row = this.applyTemplate(this.itemTemplate, this.items[i]);
dojo.addClass(row, 'row');
row._data = this.items[i];
row._idx = i;
rows.push(row);
}
if(!this.dividerFunction || !this.dividerTemplate){
for(i = startIdx; i < endIdx; i++){
rows[i]._data = this.items[i];
rows[i]._idx = i;
frag.appendChild(rows[i]);
}
}else{
var prevDividerValue = null;
var dividerValue;
var divider;
for(i = startIdx; i < endIdx; i++){
rows[i]._data = this.items[i];
rows[i]._idx = i;
dividerValue = this.dividerFunction(this.items[i]);
if(dividerValue && dividerValue != prevDividerValue){
divider = this.applyTemplate(this.dividerTemplate, {
label: dividerValue,
item: this.items[i]
});
divider._isDivider = true;
frag.appendChild(divider);
prevDividerValue = dividerValue;
}
frag.appendChild(rows[i]);
}
}
return frag;
},
_replaceToken: function(value, key){
if(key.charAt(0) == '!'){ value = dojo.getObject(key.substr(1), false, _this); }
if(typeof value == "undefined"){ return ""; } // a debugging aide
if(value == null){ return ""; }
// Substitution keys beginning with ! will skip the transform step,
// in case a user wishes to insert unescaped markup, e.g. ${!foo}
return key.charAt(0) == "!" ? value :
// Safer substitution, see heading "Attribute values" in
// http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
value.toString().replace(/"/g,"&quot;"); //TODO: add &amp? use encodeXML method?
},
_checkLoadComplete: function(){
// summary:
// Checks if all templates have loaded
this._templateLoadCount--;
if(this._templateLoadCount < 1 && this.get("items")){
this.render();
}
},
_loadTemplate: function(url, thisAttr, callback){
// summary:
// Loads a template
if(!url){
callback();
return;
}
if(templateCache[url]){
this.set(thisAttr, templateCache[url]);
callback();
}else{
var _this = this;
dojo.xhrGet({
url: url,
sync: false,
handleAs: "text",
load: function(text){
templateCache[url] = dojo.trim(text);
_this.set(thisAttr, templateCache[url]);
callback();
}
});
}
},
_setFormattersAttr: function(formatters){
// summary:
// Sets the data items, and causes a rerender of the list
this.formatters = formatters;
},
_setItemsAttr: function(items){
// summary:
// Sets the data items, and causes a rerender of the list
this.items = items || [];
if(this._templateLoadCount < 1 && items){
this.render();
}
},
destroy: function(){
if(this.buttons){
dojo.forEach(this.buttons, function(button){
button.destroy();
});
this.buttons = null;
}
this.inherited(arguments);
}
});
})();
});