diff options
| author | Tristan Zur <tzur@web.web.ccwn.org> | 2014-03-27 22:27:47 +0100 |
|---|---|---|
| committer | Tristan Zur <tzur@web.web.ccwn.org> | 2014-03-27 22:27:47 +0100 |
| commit | b62676ca5d3d6f6ba3f019ea3f99722e165a98d8 (patch) | |
| tree | 86722cb80f07d4569f90088eeaea2fc2f6e2ef94 /js/dojo/dojox/mobile/app/List.js | |
Diffstat (limited to 'js/dojo/dojox/mobile/app/List.js')
| -rw-r--r-- | js/dojo/dojox/mobile/app/List.js | 649 |
1 files changed, 649 insertions, 0 deletions
diff --git a/js/dojo/dojox/mobile/app/List.js b/js/dojo/dojox/mobile/app/List.js new file mode 100644 index 0000000..21c2c64 --- /dev/null +++ b/js/dojo/dojox/mobile/app/List.js @@ -0,0 +1,649 @@ +//>>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); + } + + }); + +})(); +}); |
