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