650 lines
17 KiB
JavaScript
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,"""); //TODO: add &? 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);
|
|
}
|
|
|
|
});
|
|
|
|
})();
|
|
});
|