List.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. // wrapped by build app
  2. define("dojox/mobile/app/List", ["dijit","dojo","dojox","dojo/require!dojo/string,dijit/_WidgetBase"], function(dijit,dojo,dojox){
  3. dojo.provide("dojox.mobile.app.List");
  4. dojo.experimental("dojox.mobile.app.List");
  5. dojo.require("dojo.string");
  6. dojo.require("dijit._WidgetBase");
  7. (function(){
  8. var templateCache = {};
  9. dojo.declare("dojox.mobile.app.List", dijit._WidgetBase, {
  10. // summary:
  11. // A templated list widget. Given a simple array of data objects
  12. // and a HTML template, it renders a list of elements, with
  13. // support for a swipe delete action. An optional template
  14. // can be provided for when the list is empty.
  15. // items: Array
  16. // The array of data items that will be rendered.
  17. items: null,
  18. // itemTemplate: String
  19. // The URL to the HTML file containing the markup for each individual
  20. // data item.
  21. itemTemplate: "",
  22. // emptyTemplate: String
  23. // The URL to the HTML file containing the HTML to display if there
  24. // are no data items. This is optional.
  25. emptyTemplate: "",
  26. // dividerTemplate: String
  27. // The URL to the HTML file containing the markup for the dividers
  28. // between groups of list items
  29. dividerTemplate: "",
  30. // dividerFunction: Function
  31. // Function to create divider elements. This should return a divider
  32. // value for each item in the list
  33. dividerFunction: null,
  34. // labelDelete: String
  35. // The label to display for the Delete button
  36. labelDelete: "Delete",
  37. // labelCancel: String
  38. // The label to display for the Cancel button
  39. labelCancel: "Cancel",
  40. // controller: Object
  41. //
  42. controller: null,
  43. // autoDelete: Boolean
  44. autoDelete: true,
  45. // enableDelete: Boolean
  46. enableDelete: true,
  47. // enableHold: Boolean
  48. enableHold: true,
  49. // formatters: Object
  50. // A name/value map of functions used to format data for display
  51. formatters: null,
  52. // _templateLoadCount: Number
  53. // The number of templates remaining to load before the list renders.
  54. _templateLoadCount: 0,
  55. // _mouseDownPos: Object
  56. // The coordinates of where a mouseDown event was detected
  57. _mouseDownPos: null,
  58. baseClass: "list",
  59. constructor: function(){
  60. this._checkLoadComplete = dojo.hitch(this, this._checkLoadComplete);
  61. this._replaceToken = dojo.hitch(this, this._replaceToken);
  62. this._postDeleteAnim = dojo.hitch(this, this._postDeleteAnim);
  63. },
  64. postCreate: function(){
  65. var _this = this;
  66. if(this.emptyTemplate){
  67. this._templateLoadCount++;
  68. }
  69. if(this.itemTemplate){
  70. this._templateLoadCount++;
  71. }
  72. if(this.dividerTemplate){
  73. this._templateLoadCount++;
  74. }
  75. this.connect(this.domNode, "onmousedown", function(event){
  76. var touch = event;
  77. if(event.targetTouches && event.targetTouches.length > 0){
  78. touch = event.targetTouches[0];
  79. }
  80. // Find the node that was tapped/clicked
  81. var rowNode = _this._getRowNode(event.target);
  82. if(rowNode){
  83. // Add the rows data to the event so it can be picked up
  84. // by any listeners
  85. _this._setDataInfo(rowNode, event);
  86. // Select and highlight the row
  87. _this._selectRow(rowNode);
  88. // Record the position that was tapped
  89. _this._mouseDownPos = {
  90. x: touch.pageX,
  91. y: touch.pageY
  92. };
  93. _this._dragThreshold = null;
  94. }
  95. });
  96. this.connect(this.domNode, "onmouseup", function(event){
  97. // When the mouse/finger comes off the list,
  98. // call the onSelect function and deselect the row.
  99. if(event.targetTouches && event.targetTouches.length > 0){
  100. event = event.targetTouches[0];
  101. }
  102. var rowNode = _this._getRowNode(event.target);
  103. if(rowNode){
  104. _this._setDataInfo(rowNode, event);
  105. if(_this._selectedRow){
  106. _this.onSelect(rowNode._data, rowNode._idx, rowNode);
  107. }
  108. this._deselectRow();
  109. }
  110. });
  111. // If swipe-to-delete is enabled, listen for the mouse moving
  112. if(this.enableDelete){
  113. this.connect(this.domNode, "mousemove", function(event){
  114. dojo.stopEvent(event);
  115. if(!_this._selectedRow){
  116. return;
  117. }
  118. var rowNode = _this._getRowNode(event.target);
  119. // Still check for enableDelete in case it's changed after
  120. // this listener is added.
  121. if(_this.enableDelete && rowNode && !_this._deleting){
  122. _this.handleDrag(event);
  123. }
  124. });
  125. }
  126. // Put the data and index onto each onclick event.
  127. this.connect(this.domNode, "onclick", function(event){
  128. if(event.touches && event.touches.length > 0){
  129. event = event.touches[0];
  130. }
  131. var rowNode = _this._getRowNode(event.target, true);
  132. if(rowNode){
  133. _this._setDataInfo(rowNode, event);
  134. }
  135. });
  136. // If the mouse or finger moves off the selected row,
  137. // deselect it.
  138. this.connect(this.domNode, "mouseout", function(event){
  139. if(event.touches && event.touches.length > 0){
  140. event = event.touches[0];
  141. }
  142. if(event.target == _this._selectedRow){
  143. _this._deselectRow();
  144. }
  145. });
  146. // If no item template has been provided, it is an error.
  147. if(!this.itemTemplate){
  148. throw Error("An item template must be provided to " + this.declaredClass);
  149. }
  150. // Load the item template
  151. this._loadTemplate(this.itemTemplate, "itemTemplate", this._checkLoadComplete);
  152. if(this.emptyTemplate){
  153. // If the optional empty template has been provided, load it.
  154. this._loadTemplate(this.emptyTemplate, "emptyTemplate", this._checkLoadComplete);
  155. }
  156. if(this.dividerTemplate){
  157. this._loadTemplate(this.dividerTemplate, "dividerTemplate", this._checkLoadComplete);
  158. }
  159. },
  160. handleDrag: function(event){
  161. // summary:
  162. // Handles rows being swiped for deletion.
  163. var touch = event;
  164. if(event.targetTouches && event.targetTouches.length > 0){
  165. touch = event.targetTouches[0];
  166. }
  167. // Get the distance that the mouse or finger has moved since
  168. // beginning the swipe action.
  169. var diff = touch.pageX - this._mouseDownPos.x;
  170. var absDiff = Math.abs(diff);
  171. if(absDiff > 10 && !this._dragThreshold){
  172. // Make the user drag the row 60% of the width to remove it
  173. this._dragThreshold = dojo.marginBox(this._selectedRow).w * 0.6;
  174. if(!this.autoDelete){
  175. this.createDeleteButtons(this._selectedRow);
  176. }
  177. }
  178. this._selectedRow.style.left = (absDiff > 10 ? diff : 0) + "px";
  179. // If the user has dragged the row more than the threshold, slide
  180. // it off the screen in preparation for deletion.
  181. if(this._dragThreshold && this._dragThreshold < absDiff){
  182. this.preDelete(diff);
  183. }
  184. },
  185. handleDragCancel: function(){
  186. // summary:
  187. // Handle a drag action being cancelled, for whatever reason.
  188. // Reset handles, remove CSS classes etc.
  189. if(this._deleting){
  190. return;
  191. }
  192. dojo.removeClass(this._selectedRow, "hold");
  193. this._selectedRow.style.left = 0;
  194. this._mouseDownPos = null;
  195. this._dragThreshold = null;
  196. this._deleteBtns && dojo.style(this._deleteBtns, "display", "none");
  197. },
  198. preDelete: function(currentLeftPos){
  199. // summary:
  200. // Slides the row offscreen before it is deleted
  201. // TODO: do this with CSS3!
  202. var self = this;
  203. this._deleting = true;
  204. dojo.animateProperty({
  205. node: this._selectedRow,
  206. duration: 400,
  207. properties: {
  208. left: {
  209. end: currentLeftPos +
  210. ((currentLeftPos > 0 ? 1 : -1) * this._dragThreshold * 0.8)
  211. }
  212. },
  213. onEnd: dojo.hitch(this, function(){
  214. if(this.autoDelete){
  215. this.deleteRow(this._selectedRow);
  216. }
  217. })
  218. }).play();
  219. },
  220. deleteRow: function(row){
  221. // First make the row invisible
  222. // Put it back where it came from
  223. dojo.style(row, {
  224. visibility: "hidden",
  225. minHeight: "0px"
  226. });
  227. dojo.removeClass(row, "hold");
  228. this._deleteAnimConn =
  229. this.connect(row, "webkitAnimationEnd", this._postDeleteAnim);
  230. dojo.addClass(row, "collapsed");
  231. },
  232. _postDeleteAnim: function(event){
  233. // summary:
  234. // Completes the deletion of a row.
  235. if(this._deleteAnimConn){
  236. this.disconnect(this._deleteAnimConn);
  237. this._deleteAnimConn = null;
  238. }
  239. var row = this._selectedRow;
  240. var sibling = row.nextSibling;
  241. var prevSibling = row.previousSibling;
  242. // If the previous node is a divider and either this is
  243. // the last element in the list, or the next node is
  244. // also a divider, remove the divider for the deleted section.
  245. if(prevSibling && prevSibling._isDivider){
  246. if(!sibling || sibling._isDivider){
  247. prevSibling.parentNode.removeChild(prevSibling);
  248. }
  249. }
  250. row.parentNode.removeChild(row);
  251. this.onDelete(row._data, row._idx, this.items);
  252. // Decrement the index of each following row
  253. while(sibling){
  254. if(sibling._idx){
  255. sibling._idx--;
  256. }
  257. sibling = sibling.nextSibling;
  258. }
  259. dojo.destroy(row);
  260. // Fix up the 'first' and 'last' CSS classes on the rows
  261. dojo.query("> *:not(.buttons)", this.domNode).forEach(this.applyClass);
  262. this._deleting = false;
  263. this._deselectRow();
  264. },
  265. createDeleteButtons: function(aroundNode){
  266. // summary:
  267. // Creates the two buttons displayed when confirmation is
  268. // required before deletion of a row.
  269. // aroundNode:
  270. // The DOM node of the row about to be deleted.
  271. var mb = dojo.marginBox(aroundNode);
  272. var pos = dojo._abs(aroundNode, true);
  273. if(!this._deleteBtns){
  274. // Create the delete buttons.
  275. this._deleteBtns = dojo.create("div",{
  276. "class": "buttons"
  277. }, this.domNode);
  278. this.buttons = [];
  279. this.buttons.push(new dojox.mobile.Button({
  280. btnClass: "mblRedButton",
  281. label: this.labelDelete
  282. }));
  283. this.buttons.push(new dojox.mobile.Button({
  284. btnClass: "mblBlueButton",
  285. label: this.labelCancel
  286. }));
  287. dojo.place(this.buttons[0].domNode, this._deleteBtns);
  288. dojo.place(this.buttons[1].domNode, this._deleteBtns);
  289. dojo.addClass(this.buttons[0].domNode, "deleteBtn");
  290. dojo.addClass(this.buttons[1].domNode, "cancelBtn");
  291. this._handleButtonClick = dojo.hitch(this._handleButtonClick);
  292. this.connect(this._deleteBtns, "onclick", this._handleButtonClick);
  293. }
  294. dojo.removeClass(this._deleteBtns, "fade out fast");
  295. dojo.style(this._deleteBtns, {
  296. display: "",
  297. width: mb.w + "px",
  298. height: mb.h + "px",
  299. top: (aroundNode.offsetTop) + "px",
  300. left: "0px"
  301. });
  302. },
  303. onDelete: function(data, index, array){
  304. // summary:
  305. // Called when a row is deleted
  306. // data:
  307. // The data related to the row being deleted
  308. // index:
  309. // The index of the data in the total array
  310. // array:
  311. // The array of data used.
  312. array.splice(index, 1);
  313. // If the data is empty, rerender in case an emptyTemplate has
  314. // been provided
  315. if(array.length < 1){
  316. this.render();
  317. }
  318. },
  319. cancelDelete: function(){
  320. // summary:
  321. // Cancels the deletion of a row.
  322. this._deleting = false;
  323. this.handleDragCancel();
  324. },
  325. _handleButtonClick: function(event){
  326. // summary:
  327. // Handles the click of one of the deletion buttons, either to
  328. // delete the row or to cancel the deletion.
  329. if(event.touches && event.touches.length > 0){
  330. event = event.touches[0];
  331. }
  332. var node = event.target;
  333. if(dojo.hasClass(node, "deleteBtn")){
  334. this.deleteRow(this._selectedRow);
  335. }else if(dojo.hasClass(node, "cancelBtn")){
  336. this.cancelDelete();
  337. }else{
  338. return;
  339. }
  340. dojo.addClass(this._deleteBtns, "fade out");
  341. },
  342. applyClass: function(node, idx, array){
  343. // summary:
  344. // Applies the 'first' and 'last' CSS classes to the relevant
  345. // rows.
  346. dojo.removeClass(node, "first last");
  347. if(idx == 0){
  348. dojo.addClass(node, "first");
  349. }
  350. if(idx == array.length - 1){
  351. dojo.addClass(node, "last");
  352. }
  353. },
  354. _setDataInfo: function(rowNode, event){
  355. // summary:
  356. // Attaches the data item and index for each row to any event
  357. // that occurs on that row.
  358. event.item = rowNode._data;
  359. event.index = rowNode._idx;
  360. },
  361. onSelect: function(data, index, rowNode){
  362. // summary:
  363. // Dummy function that is called when a row is tapped
  364. },
  365. _selectRow: function(row){
  366. // summary:
  367. // Selects a row, applies the relevant CSS classes.
  368. if(this._deleting && this._selectedRow && row != this._selectedRow){
  369. this.cancelDelete();
  370. }
  371. if(!dojo.hasClass(row, "row")){
  372. return;
  373. }
  374. if(this.enableHold || this.enableDelete){
  375. dojo.addClass(row, "hold");
  376. }
  377. this._selectedRow = row;
  378. },
  379. _deselectRow: function(){
  380. // summary:
  381. // Deselects a row, and cancels any drag actions that were
  382. // occurring.
  383. if(!this._selectedRow || this._deleting){
  384. return;
  385. }
  386. this.handleDragCancel();
  387. dojo.removeClass(this._selectedRow, "hold");
  388. this._selectedRow = null;
  389. },
  390. _getRowNode: function(fromNode, ignoreNoClick){
  391. // summary:
  392. // Gets the DOM node of the row that is equal to or the parent
  393. // of the node passed to this function.
  394. while(fromNode && !fromNode._data && fromNode != this.domNode){
  395. if(!ignoreNoClick && dojo.hasClass(fromNode, "noclick")){
  396. return null;
  397. }
  398. fromNode = fromNode.parentNode;
  399. }
  400. return fromNode == this.domNode ? null : fromNode;
  401. },
  402. applyTemplate: function(template, data){
  403. return dojo._toDom(dojo.string.substitute(
  404. template, data, this._replaceToken, this.formatters || this));
  405. },
  406. render: function(){
  407. // summary:
  408. // Renders the list.
  409. // Delete all existing nodes, except the deletion buttons.
  410. dojo.query("> *:not(.buttons)", this.domNode).forEach(dojo.destroy);
  411. // If there is no data, and an empty template has been provided,
  412. // render it.
  413. if(this.items.length < 1 && this.emptyTemplate){
  414. dojo.place(dojo._toDom(this.emptyTemplate), this.domNode, "first");
  415. }else{
  416. this.domNode.appendChild(this._renderRange(0, this.items.length));
  417. }
  418. if(dojo.hasClass(this.domNode.parentNode, "mblRoundRect")){
  419. dojo.addClass(this.domNode.parentNode, "mblRoundRectList")
  420. }
  421. var divs = dojo.query("> .row", this.domNode);
  422. if(divs.length > 0){
  423. dojo.addClass(divs[0], "first");
  424. dojo.addClass(divs[divs.length - 1], "last");
  425. }
  426. },
  427. _renderRange: function(startIdx, endIdx){
  428. var rows = [];
  429. var row, i;
  430. var frag = document.createDocumentFragment();
  431. startIdx = Math.max(0, startIdx);
  432. endIdx = Math.min(endIdx, this.items.length);
  433. for(i = startIdx; i < endIdx; i++){
  434. // Create a document fragment containing the templated row
  435. row = this.applyTemplate(this.itemTemplate, this.items[i]);
  436. dojo.addClass(row, 'row');
  437. row._data = this.items[i];
  438. row._idx = i;
  439. rows.push(row);
  440. }
  441. if(!this.dividerFunction || !this.dividerTemplate){
  442. for(i = startIdx; i < endIdx; i++){
  443. rows[i]._data = this.items[i];
  444. rows[i]._idx = i;
  445. frag.appendChild(rows[i]);
  446. }
  447. }else{
  448. var prevDividerValue = null;
  449. var dividerValue;
  450. var divider;
  451. for(i = startIdx; i < endIdx; i++){
  452. rows[i]._data = this.items[i];
  453. rows[i]._idx = i;
  454. dividerValue = this.dividerFunction(this.items[i]);
  455. if(dividerValue && dividerValue != prevDividerValue){
  456. divider = this.applyTemplate(this.dividerTemplate, {
  457. label: dividerValue,
  458. item: this.items[i]
  459. });
  460. divider._isDivider = true;
  461. frag.appendChild(divider);
  462. prevDividerValue = dividerValue;
  463. }
  464. frag.appendChild(rows[i]);
  465. }
  466. }
  467. return frag;
  468. },
  469. _replaceToken: function(value, key){
  470. if(key.charAt(0) == '!'){ value = dojo.getObject(key.substr(1), false, _this); }
  471. if(typeof value == "undefined"){ return ""; } // a debugging aide
  472. if(value == null){ return ""; }
  473. // Substitution keys beginning with ! will skip the transform step,
  474. // in case a user wishes to insert unescaped markup, e.g. ${!foo}
  475. return key.charAt(0) == "!" ? value :
  476. // Safer substitution, see heading "Attribute values" in
  477. // http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
  478. value.toString().replace(/"/g,"&quot;"); //TODO: add &amp? use encodeXML method?
  479. },
  480. _checkLoadComplete: function(){
  481. // summary:
  482. // Checks if all templates have loaded
  483. this._templateLoadCount--;
  484. if(this._templateLoadCount < 1 && this.get("items")){
  485. this.render();
  486. }
  487. },
  488. _loadTemplate: function(url, thisAttr, callback){
  489. // summary:
  490. // Loads a template
  491. if(!url){
  492. callback();
  493. return;
  494. }
  495. if(templateCache[url]){
  496. this.set(thisAttr, templateCache[url]);
  497. callback();
  498. }else{
  499. var _this = this;
  500. dojo.xhrGet({
  501. url: url,
  502. sync: false,
  503. handleAs: "text",
  504. load: function(text){
  505. templateCache[url] = dojo.trim(text);
  506. _this.set(thisAttr, templateCache[url]);
  507. callback();
  508. }
  509. });
  510. }
  511. },
  512. _setFormattersAttr: function(formatters){
  513. // summary:
  514. // Sets the data items, and causes a rerender of the list
  515. this.formatters = formatters;
  516. },
  517. _setItemsAttr: function(items){
  518. // summary:
  519. // Sets the data items, and causes a rerender of the list
  520. this.items = items || [];
  521. if(this._templateLoadCount < 1 && items){
  522. this.render();
  523. }
  524. },
  525. destroy: function(){
  526. if(this.buttons){
  527. dojo.forEach(this.buttons, function(button){
  528. button.destroy();
  529. });
  530. this.buttons = null;
  531. }
  532. this.inherited(arguments);
  533. }
  534. });
  535. })();
  536. });