List.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/on", "dojo/has", "./util/misc", "dojo/has!touch?./TouchScroll", "xstyle/has-class", "put-selector/put", "dojo/_base/sniff", "xstyle/css!./css/dgrid.css"],
  2. function(kernel, declare, listen, has, miscUtil, TouchScroll, hasClass, put){
  3. // Add user agent/feature CSS classes
  4. hasClass("mozilla", "opera", "webkit", "ie", "ie-6", "ie-6-7", "quirks", "no-quirks", "touch");
  5. var oddClass = "dgrid-row-odd",
  6. evenClass = "dgrid-row-even",
  7. scrollbarWidth, scrollbarHeight;
  8. function byId(id){
  9. return document.getElementById(id);
  10. }
  11. function getScrollbarSize(node, dimension){
  12. // Used by has tests for scrollbar width/height
  13. var body = document.body,
  14. size;
  15. put(body, node, ".dgrid-scrollbar-measure");
  16. size = node["offset" + dimension] - node["client" + dimension];
  17. put(node, "!dgrid-scrollbar-measure");
  18. body.removeChild(node);
  19. return size;
  20. }
  21. has.add("dom-scrollbar-width", function(global, doc, element){
  22. return getScrollbarSize(element, "Width");
  23. });
  24. has.add("dom-scrollbar-height", function(global, doc, element){
  25. return getScrollbarSize(element, "Height");
  26. });
  27. // var and function for autogenerating ID when one isn't provided
  28. var autogen = 0;
  29. function generateId(){
  30. return "dgrid_" + autogen++;
  31. }
  32. // common functions for class and className setters/getters
  33. // (these are run in instance context)
  34. var spaceRx = / +/g;
  35. function setClass(cls){
  36. // Format input appropriately for use with put...
  37. var putClass = cls ? "." + cls.replace(spaceRx, ".") : "";
  38. // Remove any old classes, and add new ones.
  39. if(this._class){
  40. putClass = "!" + this._class.replace(spaceRx, "!") + putClass;
  41. }
  42. put(this.domNode, putClass);
  43. // Store for later retrieval/removal.
  44. this._class = cls;
  45. }
  46. function getClass(){
  47. return this._class;
  48. }
  49. // window resize event handler, run in context of List instance
  50. var winResizeHandler = has("ie") < 7 && !has("quirks") ? function(){
  51. // IE6 triggers window.resize on any element resize;
  52. // avoid useless calls (and infinite loop if height: auto).
  53. // The measurement logic here is based on dojo/window logic.
  54. var root, w, h, dims;
  55. if(!this._started){ return; } // no sense calling resize yet
  56. root = document.documentElement;
  57. w = root.clientWidth;
  58. h = root.clientHeight;
  59. dims = this._prevWinDims || [];
  60. if(dims[0] !== w || dims[1] !== h){
  61. this.resize();
  62. this._prevWinDims = [w, h];
  63. }
  64. } :
  65. function(){
  66. if(this._started){ this.resize(); }
  67. };
  68. // Desktop versions of functions, deferred to when there is no touch support,
  69. // or when the useTouchScroll instance property is set to false
  70. function desktopGetScrollPosition(){
  71. return {
  72. x: this.bodyNode.scrollLeft,
  73. y: this.bodyNode.scrollTop
  74. };
  75. }
  76. function desktopScrollTo(options){
  77. if(typeof options.x !== "undefined"){
  78. this.bodyNode.scrollLeft = options.x;
  79. }
  80. if(typeof options.y !== "undefined"){
  81. this.bodyNode.scrollTop = options.y;
  82. }
  83. }
  84. return declare(has("touch") ? TouchScroll : null, {
  85. tabableHeader: false,
  86. // showHeader: Boolean
  87. // Whether to render header (sub)rows.
  88. showHeader: false,
  89. // showFooter: Boolean
  90. // Whether to render footer area. Extensions which display content
  91. // in the footer area should set this to true.
  92. showFooter: false,
  93. // maintainOddEven: Boolean
  94. // Whether to maintain the odd/even classes when new rows are inserted.
  95. // This can be disabled to improve insertion performance if odd/even styling is not employed.
  96. maintainOddEven: true,
  97. // cleanAddedRules: Boolean
  98. // Whether to track rules added via the addCssRule method to be removed
  99. // when the list is destroyed. Note this is effective at the time of
  100. // the call to addCssRule, not at the time of destruction.
  101. cleanAddedRules: true,
  102. // useTouchScroll: Boolean
  103. // If touch support is available, this determines whether to
  104. // incorporate logic from the TouchScroll module (at the expense of
  105. // normal desktop/mouse or native mobile scrolling functionality).
  106. useTouchScroll: true,
  107. // cleanEmptyObservers: Boolean
  108. // Whether to clean up observers for empty result sets.
  109. cleanEmptyObservers: true,
  110. // highlightDuration: Integer
  111. // The amount of time (in milliseconds) that a row should remain
  112. // highlighted after it has been updated.
  113. highlightDuration: 250,
  114. postscript: function(params, srcNodeRef){
  115. // perform setup and invoke create in postScript to allow descendants to
  116. // perform logic before create/postCreate happen (a la dijit/_WidgetBase)
  117. var grid = this;
  118. (this._Row = function(id, object, element){
  119. this.id = id;
  120. this.data = object;
  121. this.element = element;
  122. }).prototype.remove = function(){
  123. grid.removeRow(this.element);
  124. };
  125. if(srcNodeRef){
  126. // normalize srcNodeRef and store on instance during create process.
  127. // Doing this in postscript is a bit earlier than dijit would do it,
  128. // but allows subclasses to access it pre-normalized during create.
  129. this.srcNodeRef = srcNodeRef =
  130. srcNodeRef.nodeType ? srcNodeRef : byId(srcNodeRef);
  131. }
  132. this.create(params, srcNodeRef);
  133. },
  134. listType: "list",
  135. create: function(params, srcNodeRef){
  136. var domNode = this.domNode = srcNodeRef || put("div"),
  137. cls;
  138. if(params){
  139. this.params = params;
  140. declare.safeMixin(this, params);
  141. // Check for initial class or className in params or on domNode
  142. cls = params["class"] || params.className || domNode.className;
  143. // handle sort param - TODO: revise @ 0.4 when _sort -> sort
  144. this._sort = params.sort || [];
  145. delete this.sort; // ensure back-compat method isn't shadowed
  146. }else{
  147. this._sort = [];
  148. }
  149. // ensure arrays and hashes are initialized
  150. this.observers = [];
  151. this._numObservers = 0;
  152. this._listeners = [];
  153. this._rowIdToObject = {};
  154. this.postMixInProperties && this.postMixInProperties();
  155. // Apply id to widget and domNode,
  156. // from incoming node, widget params, or autogenerated.
  157. this.id = domNode.id = domNode.id || this.id || generateId();
  158. // Perform initial rendering, and apply classes if any were specified.
  159. this.buildRendering();
  160. if(cls){ setClass.call(this, cls); }
  161. this.postCreate();
  162. // remove srcNodeRef instance property post-create
  163. delete this.srcNodeRef;
  164. // to preserve "it just works" behavior, call startup if we're visible
  165. if(this.domNode.offsetHeight){
  166. this.startup();
  167. }
  168. },
  169. buildRendering: function(){
  170. var domNode = this.domNode,
  171. self = this,
  172. headerNode, spacerNode, bodyNode, footerNode, isRTL;
  173. // Detect RTL on html/body nodes; taken from dojo/dom-geometry
  174. isRTL = this.isRTL = (document.body.dir || document.documentElement.dir ||
  175. document.body.style.direction).toLowerCase() == "rtl";
  176. // Clear out className (any pre-applied classes will be re-applied via the
  177. // class / className setter), then apply standard classes/attributes
  178. domNode.className = "";
  179. put(domNode, "[role=grid].ui-widget.dgrid.dgrid-" + this.listType);
  180. // Place header node (initially hidden if showHeader is false).
  181. headerNode = this.headerNode = put(domNode,
  182. "div.dgrid-header.dgrid-header-row.ui-widget-header" +
  183. (this.showHeader ? "" : ".dgrid-header-hidden"));
  184. if(has("quirks") || has("ie") < 8){
  185. spacerNode = put(domNode, "div.dgrid-spacer");
  186. }
  187. bodyNode = this.bodyNode = put(domNode, "div.dgrid-scroller");
  188. // firefox 4 until at least 10 adds overflow: auto elements to the tab index by default for some
  189. // reason; force them to be not tabbable
  190. bodyNode.tabIndex = -1;
  191. this.headerScrollNode = put(domNode, "div.dgrid-header-scroll.dgrid-scrollbar-width.ui-widget-header");
  192. // Place footer node (initially hidden if showFooter is false).
  193. footerNode = this.footerNode = put("div.dgrid-footer" +
  194. (this.showFooter ? "" : ".dgrid-footer-hidden"));
  195. put(domNode, footerNode);
  196. if(isRTL){
  197. domNode.className += " dgrid-rtl" + (has("webkit") ? "" : " dgrid-rtl-nonwebkit");
  198. }
  199. listen(bodyNode, "scroll", function(event){
  200. if(self.showHeader){
  201. // keep the header aligned with the body
  202. headerNode.scrollLeft = event.scrollLeft || bodyNode.scrollLeft;
  203. }
  204. // re-fire, since browsers are not consistent about propagation here
  205. event.stopPropagation();
  206. listen.emit(domNode, "scroll", {scrollTarget: bodyNode});
  207. });
  208. this.configStructure();
  209. this.renderHeader();
  210. this.contentNode = this.touchNode = put(this.bodyNode, "div.dgrid-content.ui-widget-content");
  211. // add window resize handler, with reference for later removal if needed
  212. this._listeners.push(this._resizeHandle = listen(window, "resize",
  213. miscUtil.throttleDelayed(winResizeHandler, this)));
  214. },
  215. postCreate: has("touch") ? function(){
  216. if(this.useTouchScroll){
  217. this.inherited(arguments);
  218. }
  219. } : function(){},
  220. startup: function(){
  221. // summary:
  222. // Called automatically after postCreate if the component is already
  223. // visible; otherwise, should be called manually once placed.
  224. if(this._started){ return; } // prevent double-triggering
  225. this.inherited(arguments);
  226. this._started = true;
  227. this.resize();
  228. // apply sort (and refresh) now that we're ready to render
  229. this.set("sort", this._sort);
  230. },
  231. configStructure: function(){
  232. // does nothing in List, this is more of a hook for the Grid
  233. },
  234. resize: function(){
  235. var
  236. bodyNode = this.bodyNode,
  237. headerNode = this.headerNode,
  238. footerNode = this.footerNode,
  239. headerHeight = headerNode.offsetHeight,
  240. footerHeight = this.showFooter ? footerNode.offsetHeight : 0,
  241. quirks = has("quirks") || has("ie") < 7;
  242. this.headerScrollNode.style.height = bodyNode.style.marginTop = headerHeight + "px";
  243. bodyNode.style.marginBottom = footerHeight + "px";
  244. if(quirks){
  245. // in IE6 and quirks mode, the "bottom" CSS property is ignored.
  246. // We guard against negative values in case of issues with external CSS.
  247. bodyNode.style.height = ""; // reset first
  248. bodyNode.style.height =
  249. Math.max((this.domNode.offsetHeight - headerHeight - footerHeight), 0) + "px";
  250. if (footerHeight) {
  251. // Work around additional glitch where IE 6 / quirks fails to update
  252. // the position of the bottom-aligned footer; this jogs its memory.
  253. footerNode.style.bottom = '1px';
  254. setTimeout(function(){ footerNode.style.bottom = ''; }, 0);
  255. }
  256. }
  257. if(!scrollbarWidth){
  258. // Measure the browser's scrollbar width using a DIV we'll delete right away
  259. scrollbarWidth = has("dom-scrollbar-width");
  260. scrollbarHeight = has("dom-scrollbar-height");
  261. // Avoid issues with certain widgets inside in IE7, and
  262. // ColumnSet scroll issues with all supported IE versions
  263. if(has("ie")){
  264. scrollbarWidth++;
  265. scrollbarHeight++;
  266. }
  267. // add rules that can be used where scrollbar width/height is needed
  268. miscUtil.addCssRule(".dgrid-scrollbar-width", "width: " + scrollbarWidth + "px");
  269. miscUtil.addCssRule(".dgrid-scrollbar-height", "height: " + scrollbarHeight + "px");
  270. if(scrollbarWidth != 17 && !quirks){
  271. // for modern browsers, we can perform a one-time operation which adds
  272. // a rule to account for scrollbar width in all grid headers.
  273. miscUtil.addCssRule(".dgrid-header", "right: " + scrollbarWidth + "px");
  274. // add another for RTL grids
  275. miscUtil.addCssRule(".dgrid-rtl-nonwebkit .dgrid-header", "left: " + scrollbarWidth + "px");
  276. }
  277. }
  278. if(quirks){
  279. // old IE doesn't support left + right + width:auto; set width directly
  280. headerNode.style.width = bodyNode.clientWidth + "px";
  281. setTimeout(function(){
  282. // sync up (after the browser catches up with the new width)
  283. headerNode.scrollLeft = bodyNode.scrollLeft;
  284. }, 0);
  285. }
  286. },
  287. addCssRule: function(selector, css){
  288. // summary:
  289. // Version of util/misc.addCssRule which tracks added rules and removes
  290. // them when the List is destroyed.
  291. var rule = miscUtil.addCssRule(selector, css);
  292. if(this.cleanAddedRules){
  293. // Although this isn't a listener, it shares the same remove contract
  294. this._listeners.push(rule);
  295. }
  296. return rule;
  297. },
  298. on: function(eventType, listener){
  299. // delegate events to the domNode
  300. var signal = listen(this.domNode, eventType, listener);
  301. if(!has("dom-addeventlistener")){
  302. this._listeners.push(signal);
  303. }
  304. return signal;
  305. },
  306. cleanup: function(){
  307. // summary:
  308. // Clears out all rows currently in the list.
  309. var observers = this.observers,
  310. i;
  311. for(i in this._rowIdToObject){
  312. if(this._rowIdToObject[i] != this.columns){
  313. var rowElement = byId(i);
  314. if(rowElement){
  315. this.removeRow(rowElement, true);
  316. }
  317. }
  318. }
  319. // remove any store observers
  320. for(i = 0;i < observers.length; i++){
  321. var observer = observers[i];
  322. observer && observer.cancel();
  323. }
  324. this.observers = [];
  325. this._numObservers = 0;
  326. this.preload = null;
  327. },
  328. destroy: function(){
  329. // summary:
  330. // Destroys this grid
  331. // Remove any event listeners and other such removables
  332. if(this._listeners){ // Guard against accidental subsequent calls to destroy
  333. for(var i = this._listeners.length; i--;){
  334. this._listeners[i].remove();
  335. }
  336. delete this._listeners;
  337. }
  338. this.cleanup();
  339. // destroy DOM
  340. put(this.domNode, "!");
  341. this.inherited(arguments);
  342. },
  343. refresh: function(){
  344. // summary:
  345. // refreshes the contents of the grid
  346. this.cleanup();
  347. this._rowIdToObject = {};
  348. this._autoId = 0;
  349. // make sure all the content has been removed so it can be recreated
  350. this.contentNode.innerHTML = "";
  351. // Ensure scroll position always resets (especially for TouchScroll).
  352. this.scrollTo({ x: 0, y: 0 });
  353. },
  354. newRow: function(object, parentNode, beforeNode, i, options){
  355. if(parentNode){
  356. var row = this.insertRow(object, parentNode, beforeNode, i, options);
  357. put(row, ".ui-state-highlight");
  358. setTimeout(function(){
  359. put(row, "!ui-state-highlight");
  360. }, this.highlightDuration);
  361. return row;
  362. }
  363. },
  364. adjustRowIndices: function(firstRow){
  365. // this traverses through rows to maintain odd/even classes on the rows when indexes shift;
  366. var next = firstRow;
  367. var rowIndex = next.rowIndex;
  368. if(rowIndex > -1){ // make sure we have a real number in case this is called on a non-row
  369. do{
  370. // Skip non-numeric, non-rows
  371. if(next.rowIndex > -1){
  372. if(this.maintainOddEven){
  373. if((next.className + ' ').indexOf("dgrid-row ") > -1){
  374. put(next, '.' + (rowIndex % 2 == 1 ? oddClass : evenClass) + '!' + (rowIndex % 2 == 0 ? oddClass : evenClass));
  375. }
  376. }
  377. next.rowIndex = rowIndex++;
  378. }
  379. }while((next = next.nextSibling) && next.rowIndex != rowIndex);
  380. }
  381. },
  382. renderArray: function(results, beforeNode, options){
  383. // summary:
  384. // This renders an array or collection of objects as rows in the grid, before the
  385. // given node. This will listen for changes in the collection if an observe method
  386. // is available (as it should be if it comes from an Observable data store).
  387. options = options || {};
  388. var self = this,
  389. start = options.start || 0,
  390. observers = this.observers,
  391. rows, container, observerIndex;
  392. if(!beforeNode){
  393. this._lastCollection = results;
  394. }
  395. if(results.observe){
  396. // observe the results for changes
  397. self._numObservers++;
  398. observerIndex = observers.push(results.observe(function(object, from, to){
  399. var row, firstRow, nextNode, parentNode;
  400. function advanceNext() {
  401. nextNode = (nextNode.connected || nextNode).nextSibling;
  402. }
  403. // a change in the data took place
  404. if(from > -1 && rows[from]){
  405. // remove from old slot
  406. row = rows.splice(from, 1)[0];
  407. // check to make the sure the node is still there before we try to remove it, (in case it was moved to a different place in the DOM)
  408. if(row.parentNode == container){
  409. firstRow = row.nextSibling;
  410. if(firstRow){ // it's possible for this to have been already removed if it is in overlapping query results
  411. if(from != to){ // if from and to are identical, it is an in-place update and we don't want to alter the rowIndex at all
  412. firstRow.rowIndex--; // adjust the rowIndex so adjustRowIndices has the right starting point
  413. }
  414. }
  415. self.removeRow(row); // now remove
  416. }
  417. // the removal of rows could cause us to need to page in more items
  418. if(self._processScroll){
  419. self._processScroll();
  420. }
  421. }
  422. if(to > -1){
  423. // Add to new slot (either before an existing row, or at the end)
  424. // First determine the DOM node that this should be placed before.
  425. if(rows.length){
  426. nextNode = rows[to];
  427. if(!nextNode){
  428. nextNode = rows[to - 1];
  429. if(nextNode){
  430. // Make sure to skip connected nodes, so we don't accidentally
  431. // insert a row in between a parent and its children.
  432. advanceNext();
  433. }
  434. }
  435. }else{
  436. // There are no rows. Allow for subclasses to insert new rows somewhere other than
  437. // at the end of the parent node.
  438. nextNode = self._getFirstRowSibling && self._getFirstRowSibling(container);
  439. }
  440. // Make sure we don't trip over a stale reference to a
  441. // node that was removed, or try to place a node before
  442. // itself (due to overlapped queries)
  443. if(row && nextNode && row.id === nextNode.id){
  444. advanceNext();
  445. }
  446. if(nextNode && !nextNode.parentNode){
  447. nextNode = byId(nextNode.id);
  448. }
  449. parentNode = (beforeNode && beforeNode.parentNode) ||
  450. (nextNode && nextNode.parentNode) || self.contentNode;
  451. row = self.newRow(object, parentNode, nextNode, options.start + to, options);
  452. if(row){
  453. row.observerIndex = observerIndex;
  454. rows.splice(to, 0, row);
  455. if(!firstRow || to < from){
  456. // the inserted row is first, so we update firstRow to point to it
  457. var previous = row.previousSibling;
  458. // if we are not in sync with the previous row, roll the firstRow back one so adjustRowIndices can sync everything back up.
  459. firstRow = !previous || previous.rowIndex + 1 == row.rowIndex || row.rowIndex == 0 ?
  460. row : previous;
  461. }
  462. }
  463. options.count++;
  464. }
  465. from != to && firstRow && self.adjustRowIndices(firstRow);
  466. self._onNotification(rows, object, from, to);
  467. }, true)) - 1;
  468. }
  469. var rowsFragment = document.createDocumentFragment(),
  470. lastRow;
  471. function mapEach(object){
  472. lastRow = self.insertRow(object, rowsFragment, null, start++, options);
  473. lastRow.observerIndex = observerIndex;
  474. return lastRow;
  475. }
  476. function whenError(error){
  477. if(typeof observerIndex !== "undefined"){
  478. observers[observerIndex].cancel();
  479. observers[observerIndex] = 0;
  480. self._numObservers--;
  481. }
  482. if(error){
  483. throw error;
  484. }
  485. }
  486. function whenDone(resolvedRows){
  487. container = beforeNode ? beforeNode.parentNode : self.contentNode;
  488. if(container && container.parentNode &&
  489. (container !== self.contentNode || resolvedRows.length)){
  490. container.insertBefore(rowsFragment, beforeNode || null);
  491. lastRow = resolvedRows[resolvedRows.length - 1];
  492. lastRow && self.adjustRowIndices(lastRow);
  493. }else if(observers[observerIndex] && self.cleanEmptyObservers){
  494. // Remove the observer and don't bother inserting;
  495. // rows are already out of view or there were none to track
  496. whenError();
  497. }
  498. return (rows = resolvedRows);
  499. }
  500. // now render the results
  501. if(results.map){
  502. rows = results.map(mapEach, console.error);
  503. if(rows.then){
  504. return rows.then(whenDone, whenError);
  505. }
  506. }else{
  507. rows = [];
  508. for(var i = 0, l = results.length; i < l; i++){
  509. rows[i] = mapEach(results[i]);
  510. }
  511. }
  512. return whenDone(rows);
  513. },
  514. _onNotification: function(rows, object, from, to){
  515. // summary:
  516. // Protected method called whenever a store notification is observed.
  517. // Intended to be extended as necessary by mixins/extensions.
  518. },
  519. renderHeader: function(){
  520. // no-op in a plain list
  521. },
  522. _autoId: 0,
  523. insertRow: function(object, parent, beforeNode, i, options){
  524. // summary:
  525. // Creates a single row in the grid.
  526. // Include parentId within row identifier if one was specified in options.
  527. // (This is used by tree to allow the same object to appear under
  528. // multiple parents.)
  529. var parentId = options.parentId,
  530. id = this.id + "-row-" + (parentId ? parentId + "-" : "") +
  531. ((this.store && this.store.getIdentity) ?
  532. this.store.getIdentity(object) : this._autoId++),
  533. row = byId(id),
  534. previousRow = row && row.previousSibling;
  535. if(row){// if it existed elsewhere in the DOM, we will remove it, so we can recreate it
  536. this.removeRow(row);
  537. }
  538. row = this.renderRow(object, options);
  539. row.className = (row.className || "") + " ui-state-default dgrid-row " + (i % 2 == 1 ? oddClass : evenClass);
  540. // get the row id for easy retrieval
  541. this._rowIdToObject[row.id = id] = object;
  542. parent.insertBefore(row, beforeNode || null);
  543. if(previousRow){
  544. // in this case, we are pulling the row from another location in the grid, and we need to readjust the rowIndices from the point it was removed
  545. this.adjustRowIndices(previousRow);
  546. }
  547. row.rowIndex = i;
  548. return row;
  549. },
  550. renderRow: function(value, options){
  551. // summary:
  552. // Responsible for returning the DOM for a single row in the grid.
  553. return put("div", "" + value);
  554. },
  555. removeRow: function(rowElement, justCleanup){
  556. // summary:
  557. // Simply deletes the node in a plain List.
  558. // Column plugins may aspect this to implement their own cleanup routines.
  559. // rowElement: Object|DOMNode
  560. // Object or element representing the row to be removed.
  561. // justCleanup: Boolean
  562. // If true, the row element will not be removed from the DOM; this can
  563. // be used by extensions/plugins in cases where the DOM will be
  564. // massively cleaned up at a later point in time.
  565. rowElement = rowElement.element || rowElement;
  566. delete this._rowIdToObject[rowElement.id];
  567. if(!justCleanup){
  568. put(rowElement, "!");
  569. }
  570. },
  571. row: function(target){
  572. // summary:
  573. // Get the row object by id, object, node, or event
  574. var id;
  575. if(target instanceof this._Row){ return target; } // no-op; already a row
  576. if(target.target && target.target.nodeType){
  577. // event
  578. target = target.target;
  579. }
  580. if(target.nodeType){
  581. var object;
  582. do{
  583. var rowId = target.id;
  584. if((object = this._rowIdToObject[rowId])){
  585. return new this._Row(rowId.substring(this.id.length + 5), object, target);
  586. }
  587. target = target.parentNode;
  588. }while(target && target != this.domNode);
  589. return;
  590. }
  591. if(typeof target == "object"){
  592. // assume target represents a store item
  593. id = this.store.getIdentity(target);
  594. }else{
  595. // assume target is a row ID
  596. id = target;
  597. target = this._rowIdToObject[this.id + "-row-" + id];
  598. }
  599. return new this._Row(id, target, byId(this.id + "-row-" + id));
  600. },
  601. cell: function(target){
  602. // this doesn't do much in a plain list
  603. return {
  604. row: this.row(target)
  605. };
  606. },
  607. _move: function(item, steps, targetClass, visible){
  608. var nextSibling, current, element;
  609. // Start at the element indicated by the provided row or cell object.
  610. element = current = item.element;
  611. steps = steps || 1;
  612. do{
  613. // Outer loop: move in the appropriate direction.
  614. if((nextSibling = current[steps < 0 ? "previousSibling" : "nextSibling"])){
  615. do{
  616. // Inner loop: advance, and dig into children if applicable.
  617. current = nextSibling;
  618. if(current && (current.className + " ").indexOf(targetClass + " ") > -1){
  619. // Element with the appropriate class name; count step, stop digging.
  620. element = current;
  621. steps += steps < 0 ? 1 : -1;
  622. break;
  623. }
  624. // If the next sibling isn't a match, drill down to search, unless
  625. // visible is true and children are hidden.
  626. }while((nextSibling = (!visible || !current.hidden) && current[steps < 0 ? "lastChild" : "firstChild"]));
  627. }else{
  628. current = current.parentNode;
  629. if(current === this.bodyNode || current === this.headerNode){
  630. // Break out if we step out of the navigation area entirely.
  631. break;
  632. }
  633. }
  634. }while(steps);
  635. // Return the final element we arrived at, which might still be the
  636. // starting element if we couldn't navigate further in that direction.
  637. return element;
  638. },
  639. up: function(row, steps, visible){
  640. // summary:
  641. // Returns the row that is the given number of steps (1 by default)
  642. // above the row represented by the given object.
  643. // row:
  644. // The row to navigate upward from.
  645. // steps:
  646. // Number of steps to navigate up from the given row; default is 1.
  647. // visible:
  648. // If true, rows that are currently hidden (i.e. children of
  649. // collapsed tree rows) will not be counted in the traversal.
  650. // returns:
  651. // A row object representing the appropriate row. If the top of the
  652. // list is reached before the given number of steps, the first row will
  653. // be returned.
  654. if(!row.element){ row = this.row(row); }
  655. return this.row(this._move(row, -(steps || 1), "dgrid-row", visible));
  656. },
  657. down: function(row, steps, visible){
  658. // summary:
  659. // Returns the row that is the given number of steps (1 by default)
  660. // below the row represented by the given object.
  661. // row:
  662. // The row to navigate downward from.
  663. // steps:
  664. // Number of steps to navigate down from the given row; default is 1.
  665. // visible:
  666. // If true, rows that are currently hidden (i.e. children of
  667. // collapsed tree rows) will not be counted in the traversal.
  668. // returns:
  669. // A row object representing the appropriate row. If the bottom of the
  670. // list is reached before the given number of steps, the last row will
  671. // be returned.
  672. if(!row.element){ row = this.row(row); }
  673. return this.row(this._move(row, steps || 1, "dgrid-row", visible));
  674. },
  675. scrollTo: has("touch") ? function(options){
  676. // If TouchScroll is the superclass, defer to its implementation.
  677. return this.useTouchScroll ? this.inherited(arguments) :
  678. desktopScrollTo.call(this, options);
  679. } : desktopScrollTo,
  680. getScrollPosition: has("touch") ? function(){
  681. // If TouchScroll is the superclass, defer to its implementation.
  682. return this.useTouchScroll ? this.inherited(arguments) :
  683. desktopGetScrollPosition.call(this);
  684. } : desktopGetScrollPosition,
  685. get: function(/*String*/ name /*, ... */){
  686. // summary:
  687. // Get a property on a List instance.
  688. // name:
  689. // The property to get.
  690. // returns:
  691. // The property value on this List instance.
  692. // description:
  693. // Get a named property on a List object. The property may
  694. // potentially be retrieved via a getter method in subclasses. In the base class
  695. // this just retrieves the object's property.
  696. var fn = "_get" + name.charAt(0).toUpperCase() + name.slice(1);
  697. if(typeof this[fn] === "function"){
  698. return this[fn].apply(this, [].slice.call(arguments, 1));
  699. }
  700. // Alert users that try to use Dijit-style getter/setters so they don’t get confused
  701. // if they try to use them and it does not work
  702. if(!has("dojo-built") && typeof this[fn + "Attr"] === "function"){
  703. console.warn("dgrid: Use " + fn + " instead of " + fn + "Attr for getting " + name);
  704. }
  705. return this[name];
  706. },
  707. set: function(/*String*/ name, /*Object*/ value /*, ... */){
  708. // summary:
  709. // Set a property on a List instance
  710. // name:
  711. // The property to set.
  712. // value:
  713. // The value to set in the property.
  714. // returns:
  715. // The function returns this List instance.
  716. // description:
  717. // Sets named properties on a List object.
  718. // A programmatic setter may be defined in subclasses.
  719. //
  720. // set() may also be called with a hash of name/value pairs, ex:
  721. // | myObj.set({
  722. // | foo: "Howdy",
  723. // | bar: 3
  724. // | })
  725. // This is equivalent to calling set(foo, "Howdy") and set(bar, 3)
  726. if(typeof name === "object"){
  727. for(var k in name){
  728. this.set(k, name[k]);
  729. }
  730. }else{
  731. var fn = "_set" + name.charAt(0).toUpperCase() + name.slice(1);
  732. if(typeof this[fn] === "function"){
  733. this[fn].apply(this, [].slice.call(arguments, 1));
  734. }else{
  735. // Alert users that try to use Dijit-style getter/setters so they don’t get confused
  736. // if they try to use them and it does not work
  737. if(!has("dojo-built") && typeof this[fn + "Attr"] === "function"){
  738. console.warn("dgrid: Use " + fn + " instead of " + fn + "Attr for setting " + name);
  739. }
  740. this[name] = value;
  741. }
  742. }
  743. return this;
  744. },
  745. // Accept both class and className programmatically to set domNode class.
  746. _getClass: getClass,
  747. _setClass: setClass,
  748. _getClassName: getClass,
  749. _setClassName: setClass,
  750. _setSort: function(property, descending){
  751. // summary:
  752. // Sort the content
  753. // property: String|Array
  754. // String specifying field to sort by, or actual array of objects
  755. // with attribute and descending properties
  756. // descending: boolean
  757. // In the case where property is a string, this argument
  758. // specifies whether to sort ascending (false) or descending (true)
  759. this._sort = typeof property != "string" ? property :
  760. [{attribute: property, descending: descending}];
  761. this.refresh();
  762. if(this._lastCollection){
  763. if(property.length){
  764. // if an array was passed in, flatten to just first sort attribute
  765. // for default array sort logic
  766. if(typeof property != "string"){
  767. descending = property[0].descending;
  768. property = property[0].attribute;
  769. }
  770. this._lastCollection.sort(function(a,b){
  771. var aVal = a[property], bVal = b[property];
  772. // fall back undefined values to "" for more consistent behavior
  773. if(aVal === undefined){ aVal = ""; }
  774. if(bVal === undefined){ bVal = ""; }
  775. return aVal == bVal ? 0 : (aVal > bVal == !descending ? 1 : -1);
  776. });
  777. }
  778. this.renderArray(this._lastCollection);
  779. }
  780. },
  781. // TODO: remove the following two (and rename _sort to sort) in 0.4
  782. sort: function(property, descending){
  783. kernel.deprecated("sort(...)", 'use set("sort", ...) instead', "dgrid 0.4");
  784. this.set("sort", property, descending);
  785. },
  786. _getSort: function(){
  787. return this._sort;
  788. },
  789. _setShowHeader: function(show){
  790. // this is in List rather than just in Grid, primarily for two reasons:
  791. // (1) just in case someone *does* want to show a header in a List
  792. // (2) helps address IE < 8 header display issue in List
  793. var headerNode = this.headerNode;
  794. this.showHeader = show;
  795. // add/remove class which has styles for "hiding" header
  796. put(headerNode, (show ? "!" : ".") + "dgrid-header-hidden");
  797. this.renderHeader();
  798. this.resize(); // resize to account for (dis)appearance of header
  799. if(show){
  800. // Update scroll position of header to make sure it's in sync.
  801. headerNode.scrollLeft = this.getScrollPosition().x;
  802. }
  803. },
  804. setShowHeader: function(show){
  805. kernel.deprecated("setShowHeader(...)", 'use set("showHeader", ...) instead', "dgrid 0.4");
  806. this.set("showHeader", show);
  807. },
  808. _setShowFooter: function(show){
  809. this.showFooter = show;
  810. // add/remove class which has styles for hiding footer
  811. put(this.footerNode, (show ? "!" : ".") + "dgrid-footer-hidden");
  812. this.resize(); // to account for (dis)appearance of footer
  813. }
  814. });
  815. });