// 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); } }); })(); });