List.js 17 KB

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