RollingList.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209
  1. // wrapped by build app
  2. define("dojox/widget/RollingList", ["dijit","dojo","dojox","dojo/i18n!dijit/nls/common","dojo/require!dojo/window,dijit/layout/ContentPane,dijit/_Templated,dijit/_Contained,dijit/layout/_LayoutWidget,dijit/Menu,dijit/form/Button,dijit/focus,dijit/_base/focus,dojox/html/metrics,dojo/i18n"], function(dijit,dojo,dojox){
  3. dojo.provide("dojox.widget.RollingList");
  4. dojo.experimental("dojox.widget.RollingList");
  5. dojo.require("dojo.window");
  6. dojo.require("dijit.layout.ContentPane");
  7. dojo.require("dijit._Templated");
  8. dojo.require("dijit._Contained");
  9. dojo.require("dijit.layout._LayoutWidget");
  10. dojo.require("dijit.Menu");
  11. dojo.require("dijit.form.Button");
  12. dojo.require("dijit.focus"); // dijit.focus()
  13. dojo.require("dijit._base.focus"); // dijit.getFocus()
  14. dojo.require("dojox.html.metrics");
  15. dojo.require("dojo.i18n");
  16. dojo.requireLocalization("dijit", "common");
  17. dojo.declare("dojox.widget._RollingListPane",
  18. [dijit.layout.ContentPane, dijit._Templated, dijit._Contained], {
  19. // summary: a core pane that can be attached to a RollingList. All panes
  20. // should extend this one
  21. // templateString: string
  22. // our template
  23. templateString: '<div class="dojoxRollingListPane"><table><tbody><tr><td dojoAttachPoint="containerNode"></td></tr></tbody></div>',
  24. // parentWidget: dojox.widget.RollingList
  25. // Our rolling list widget
  26. parentWidget: null,
  27. // parentPane: dojox.widget._RollingListPane
  28. // The pane that immediately precedes ours
  29. parentPane: null,
  30. // store: store
  31. // the store we must use
  32. store: null,
  33. // items: item[]
  34. // an array of (possibly not-yet-loaded) items to display in this.
  35. // If this array is null, then the query and query options are used to
  36. // get the top-level items to use. This array is also used to watch and
  37. // see if the pane needs to be reloaded (store notifications are handled)
  38. // by the pane
  39. items: null,
  40. // query: object
  41. // a query to pass to the datastore. This is only used if items are null
  42. query: null,
  43. // queryOptions: object
  44. // query options to be passed to the datastore
  45. queryOptions: null,
  46. // focusByNode: boolean
  47. // set to false if the subclass will handle its own node focusing
  48. _focusByNode: true,
  49. // minWidth: integer
  50. // the width (in px) for this pane
  51. minWidth: 0,
  52. _setContentAndScroll: function(/*String|DomNode|Nodelist*/cont, /*Boolean?*/isFakeContent){
  53. // summary: sets the value of the content and scrolls it into view
  54. this._setContent(cont, isFakeContent);
  55. this.parentWidget.scrollIntoView(this);
  56. },
  57. _updateNodeWidth: function(n, min){
  58. // summary: updates the min width of the pane to be minPaneWidth
  59. n.style.width = "";
  60. var nWidth = dojo.marginBox(n).w;
  61. if(nWidth < min){
  62. dojo.marginBox(n, {w: min});
  63. }
  64. },
  65. _onMinWidthChange: function(v){
  66. // Called when the min width of a pane has changed
  67. this._updateNodeWidth(this.domNode, v);
  68. },
  69. _setMinWidthAttr: function(v){
  70. if(v !== this.minWidth){
  71. this.minWidth = v;
  72. this._onMinWidthChange(v);
  73. }
  74. },
  75. startup: function(){
  76. if(this._started){ return; }
  77. if(this.store && this.store.getFeatures()["dojo.data.api.Notification"]){
  78. window.setTimeout(dojo.hitch(this, function(){
  79. // Set connections after a slight timeout to avoid getting in the
  80. // condition where we are setting them while events are still
  81. // being fired
  82. this.connect(this.store, "onSet", "_onSetItem");
  83. this.connect(this.store, "onNew", "_onNewItem");
  84. this.connect(this.store, "onDelete", "_onDeleteItem");
  85. }), 1);
  86. }
  87. this.connect(this.focusNode||this.domNode, "onkeypress", "_focusKey");
  88. this.parentWidget._updateClass(this.domNode, "Pane");
  89. this.inherited(arguments);
  90. this._onMinWidthChange(this.minWidth);
  91. },
  92. _focusKey: function(/*Event*/e){
  93. // summary: called when a keypress happens on the widget
  94. if(e.charOrCode == dojo.keys.BACKSPACE){
  95. dojo.stopEvent(e);
  96. return;
  97. }else if(e.charOrCode == dojo.keys.LEFT_ARROW && this.parentPane){
  98. this.parentPane.focus();
  99. this.parentWidget.scrollIntoView(this.parentPane);
  100. }else if(e.charOrCode == dojo.keys.ENTER){
  101. this.parentWidget._onExecute();
  102. }
  103. },
  104. focus: function(/*boolean*/force){
  105. // summary: sets the focus to this current widget
  106. if(this.parentWidget._focusedPane != this){
  107. this.parentWidget._focusedPane = this;
  108. this.parentWidget.scrollIntoView(this);
  109. if(this._focusByNode && (!this.parentWidget._savedFocus || force)){
  110. try{(this.focusNode||this.domNode).focus();}catch(e){}
  111. }
  112. }
  113. },
  114. _onShow: function(){
  115. // summary: checks that the store is loaded
  116. if((this.store || this.items) && ((this.refreshOnShow && this.domNode) || (!this.isLoaded && this.domNode))){
  117. this.refresh();
  118. }
  119. },
  120. _load: function(){
  121. // summary: sets the "loading" message and then kicks off a query asyncronously
  122. this.isLoaded = false;
  123. if(this.items){
  124. this._setContentAndScroll(this.onLoadStart(), true);
  125. window.setTimeout(dojo.hitch(this, "_doQuery"), 1);
  126. }else{
  127. this._doQuery();
  128. }
  129. },
  130. _doLoadItems: function(/*item[]*/items, /*function*/callback){
  131. // summary: loads the given items, and then calls the callback when they
  132. // are finished.
  133. var _waitCount = 0, store = this.store;
  134. dojo.forEach(items, function(item){
  135. if(!store.isItemLoaded(item)){ _waitCount++; }
  136. });
  137. if(_waitCount === 0){
  138. callback();
  139. }else{
  140. var onItem = function(item){
  141. _waitCount--;
  142. if((_waitCount) === 0){
  143. callback();
  144. }
  145. };
  146. dojo.forEach(items, function(item){
  147. if(!store.isItemLoaded(item)){
  148. store.loadItem({item: item, onItem: onItem});
  149. }
  150. });
  151. }
  152. },
  153. _doQuery: function(){
  154. // summary: either runs the query or loads potentially not-yet-loaded items.
  155. if(!this.domNode){return;}
  156. var preload = this.parentWidget.preloadItems;
  157. preload = (preload === true || (this.items && this.items.length <= Number(preload)));
  158. if(this.items && preload){
  159. this._doLoadItems(this.items, dojo.hitch(this, "onItems"));
  160. }else if(this.items){
  161. this.onItems();
  162. }else{
  163. this._setContentAndScroll(this.onFetchStart(), true);
  164. this.store.fetch({query: this.query,
  165. onComplete: function(items){
  166. this.items = items;
  167. this.onItems();
  168. },
  169. onError: function(e){
  170. this._onError("Fetch", e);
  171. },
  172. scope: this});
  173. }
  174. },
  175. _hasItem: function(/* item */ item){
  176. // summary: returns whether or not the given item is handled by this
  177. // pane
  178. var items = this.items || [];
  179. for(var i = 0, myItem; (myItem = items[i]); i++){
  180. if(this.parentWidget._itemsMatch(myItem, item)){
  181. return true;
  182. }
  183. }
  184. return false;
  185. },
  186. _onSetItem: function(/* item */ item,
  187. /* attribute-name-string */ attribute,
  188. /* object | array */ oldValue,
  189. /* object | array */ newValue){
  190. // Summary: called when an item in the store has changed
  191. if(this._hasItem(item)){
  192. this.refresh();
  193. }
  194. },
  195. _onNewItem: function(/* item */ newItem, /*object?*/ parentInfo){
  196. // Summary: called when an item is added to the store
  197. var sel;
  198. if((!parentInfo && !this.parentPane) ||
  199. (parentInfo && this.parentPane && this.parentPane._hasItem(parentInfo.item) &&
  200. (sel = this.parentPane._getSelected()) && this.parentWidget._itemsMatch(sel.item, parentInfo.item))){
  201. this.items.push(newItem);
  202. this.refresh();
  203. }else if(parentInfo && this.parentPane && this._hasItem(parentInfo.item)){
  204. this.refresh();
  205. }
  206. },
  207. _onDeleteItem: function(/* item */ deletedItem){
  208. // Summary: called when an item is removed from the store
  209. if(this._hasItem(deletedItem)){
  210. this.items = dojo.filter(this.items, function(i){
  211. return (i != deletedItem);
  212. });
  213. this.refresh();
  214. }
  215. },
  216. onFetchStart: function(){
  217. // summary:
  218. // called before a fetch starts
  219. return this.loadingMessage;
  220. },
  221. onFetchError: function(/*Error*/ error){
  222. // summary:
  223. // called when a fetch error occurs.
  224. return this.errorMessage;
  225. },
  226. onLoadStart: function(){
  227. // summary:
  228. // called before a load starts
  229. return this.loadingMessage;
  230. },
  231. onLoadError: function(/*Error*/ error){
  232. // summary:
  233. // called when a load error occurs.
  234. return this.errorMessage;
  235. },
  236. onItems: function(){
  237. // summary:
  238. // called after a fetch or load - at this point, this.items should be
  239. // set and loaded. Override this function to "do your stuff"
  240. if(!this.onLoadDeferred){
  241. this.cancel();
  242. this.onLoadDeferred = new dojo.Deferred(dojo.hitch(this, "cancel"));
  243. }
  244. this._onLoadHandler();
  245. }
  246. });
  247. dojo.declare("dojox.widget._RollingListGroupPane",
  248. [dojox.widget._RollingListPane], {
  249. // summary: a pane that will handle groups (treats them as menu items)
  250. // templateString: string
  251. // our template
  252. templateString: '<div><div dojoAttachPoint="containerNode"></div>' +
  253. '<div dojoAttachPoint="menuContainer">' +
  254. '<div dojoAttachPoint="menuNode"></div>' +
  255. '</div></div>',
  256. // _menu: dijit.Menu
  257. // The menu that we will call addChild() on for adding items
  258. _menu: null,
  259. _setContent: function(/*String|DomNode|Nodelist*/cont){
  260. if(!this._menu){
  261. // Only set the content if we don't already have a menu
  262. this.inherited(arguments);
  263. }
  264. },
  265. _onMinWidthChange: function(v){
  266. // override and resize the menu instead
  267. if(!this._menu){ return; }
  268. var dWidth = dojo.marginBox(this.domNode).w;
  269. var mWidth = dojo.marginBox(this._menu.domNode).w;
  270. this._updateNodeWidth(this._menu.domNode, v - (dWidth - mWidth));
  271. },
  272. onItems: function(){
  273. // summary:
  274. // called after a fetch or load
  275. var selectItem, hadChildren = false;
  276. if(this._menu){
  277. selectItem = this._getSelected();
  278. this._menu.destroyRecursive();
  279. }
  280. this._menu = this._getMenu();
  281. var child, selectMenuItem;
  282. if(this.items.length){
  283. dojo.forEach(this.items, function(item){
  284. child = this.parentWidget._getMenuItemForItem(item, this);
  285. if(child){
  286. if(selectItem && this.parentWidget._itemsMatch(child.item, selectItem.item)){
  287. selectMenuItem = child;
  288. }
  289. this._menu.addChild(child);
  290. }
  291. }, this);
  292. }else{
  293. child = this.parentWidget._getMenuItemForItem(null, this);
  294. if(child){
  295. this._menu.addChild(child);
  296. }
  297. }
  298. if(selectMenuItem){
  299. this._setSelected(selectMenuItem);
  300. if((selectItem && !selectItem.children && selectMenuItem.children) ||
  301. (selectItem && selectItem.children && !selectMenuItem.children)){
  302. var itemPane = this.parentWidget._getPaneForItem(selectMenuItem.item, this, selectMenuItem.children);
  303. if(itemPane){
  304. this.parentWidget.addChild(itemPane, this.getIndexInParent() + 1);
  305. }else{
  306. this.parentWidget._removeAfter(this);
  307. this.parentWidget._onItemClick(null, this, selectMenuItem.item, selectMenuItem.children);
  308. }
  309. }
  310. }else if(selectItem){
  311. this.parentWidget._removeAfter(this);
  312. }
  313. this.containerNode.innerHTML = "";
  314. this.containerNode.appendChild(this._menu.domNode);
  315. this.parentWidget.scrollIntoView(this);
  316. this._checkScrollConnection(true);
  317. this.inherited(arguments);
  318. this._onMinWidthChange(this.minWidth);
  319. },
  320. _checkScrollConnection: function(doLoad){
  321. // summary: checks whether or not we need to connect to our onscroll
  322. // function
  323. var store = this.store
  324. if(this._scrollConn){
  325. this.disconnect(this._scrollConn);
  326. }
  327. delete this._scrollConn;
  328. if(!dojo.every(this.items, function(i){return store.isItemLoaded(i);})){
  329. if(doLoad){
  330. this._loadVisibleItems();
  331. }
  332. this._scrollConn = this.connect(this.domNode, "onscroll", "_onScrollPane");
  333. }
  334. },
  335. startup: function(){
  336. this.inherited(arguments);
  337. this.parentWidget._updateClass(this.domNode, "GroupPane");
  338. },
  339. focus: function(/*boolean*/force){
  340. // summary: sets the focus to this current widget
  341. if(this._menu){
  342. if(this._pendingFocus){
  343. this.disconnect(this._pendingFocus);
  344. }
  345. delete this._pendingFocus;
  346. // We focus the right widget - either the focusedChild, the
  347. // selected node, the first menu item, or the menu itself
  348. var focusWidget = this._menu.focusedChild;
  349. if(!focusWidget){
  350. var focusNode = dojo.query(".dojoxRollingListItemSelected", this.domNode)[0];
  351. if(focusNode){
  352. focusWidget = dijit.byNode(focusNode);
  353. }
  354. }
  355. if(!focusWidget){
  356. focusWidget = this._menu.getChildren()[0] || this._menu;
  357. }
  358. this._focusByNode = false;
  359. if(focusWidget.focusNode){
  360. if(!this.parentWidget._savedFocus || force){
  361. try{focusWidget.focusNode.focus();}catch(e){}
  362. }
  363. window.setTimeout(function(){
  364. try{
  365. dojo.window.scrollIntoView(focusWidget.focusNode);
  366. }catch(e){}
  367. }, 1);
  368. }else if(focusWidget.focus){
  369. if(!this.parentWidget._savedFocus || force){
  370. focusWidget.focus();
  371. }
  372. }else{
  373. this._focusByNode = true;
  374. }
  375. this.inherited(arguments);
  376. }else if(!this._pendingFocus){
  377. this._pendingFocus = this.connect(this, "onItems", "focus");
  378. }
  379. },
  380. _getMenu: function(){
  381. // summary: returns a widget to be used for the container widget.
  382. var self = this;
  383. var menu = new dijit.Menu({
  384. parentMenu: this.parentPane ? this.parentPane._menu : null,
  385. onCancel: function(/*Boolean*/ closeAll){
  386. if(self.parentPane){
  387. self.parentPane.focus(true);
  388. }
  389. },
  390. _moveToPopup: function(/*Event*/ evt){
  391. if(this.focusedChild && !this.focusedChild.disabled){
  392. this.focusedChild._onClick(evt);
  393. }
  394. }
  395. }, this.menuNode);
  396. this.connect(menu, "onItemClick", function(/*dijit.MenuItem*/ item, /*Event*/ evt){
  397. if(item.disabled){ return; }
  398. evt.alreadySelected = dojo.hasClass(item.domNode, "dojoxRollingListItemSelected");
  399. if(evt.alreadySelected &&
  400. ((evt.type == "keypress" && evt.charOrCode != dojo.keys.ENTER) ||
  401. (evt.type == "internal"))){
  402. var p = this.parentWidget.getChildren()[this.getIndexInParent() + 1];
  403. if(p){
  404. p.focus(true);
  405. this.parentWidget.scrollIntoView(p);
  406. }
  407. }else{
  408. this._setSelected(item, menu);
  409. this.parentWidget._onItemClick(evt, this, item.item, item.children);
  410. if(evt.type == "keypress" && evt.charOrCode == dojo.keys.ENTER){
  411. this.parentWidget._onExecute();
  412. }
  413. }
  414. });
  415. if(!menu._started){
  416. menu.startup();
  417. }
  418. return menu;
  419. },
  420. _onScrollPane: function(){
  421. // summary: called when the pane has been scrolled - it sets a timeout
  422. // so that we don't try and load our visible items too often during
  423. // a scroll
  424. if(this._visibleLoadPending){
  425. window.clearTimeout(this._visibleLoadPending);
  426. }
  427. this._visibleLoadPending = window.setTimeout(dojo.hitch(this, "_loadVisibleItems"), 500);
  428. },
  429. _loadVisibleItems: function(){
  430. // summary: loads the items that are currently visible in the pane
  431. delete this._visibleLoadPending
  432. var menu = this._menu;
  433. if(!menu){ return; }
  434. var children = menu.getChildren();
  435. if(!children || !children.length){ return; }
  436. var gpbme = function(n, m, pb){
  437. var s = dojo.getComputedStyle(n);
  438. var r = 0;
  439. if(m){ r += dojo._getMarginExtents(n, s).t; }
  440. if(pb){ r += dojo._getPadBorderExtents(n, s).t; }
  441. return r;
  442. };
  443. var topOffset = gpbme(this.domNode, false, true) +
  444. gpbme(this.containerNode, true, true) +
  445. gpbme(menu.domNode, true, true) +
  446. gpbme(children[0].domNode, true, false);
  447. var h = dojo.contentBox(this.domNode).h;
  448. var minOffset = this.domNode.scrollTop - topOffset - (h/2);
  449. var maxOffset = minOffset + (3*h/2);
  450. var menuItemsToLoad = dojo.filter(children, function(c){
  451. var cnt = c.domNode.offsetTop;
  452. var s = c.store;
  453. var i = c.item;
  454. return (cnt >= minOffset && cnt <= maxOffset && !s.isItemLoaded(i));
  455. })
  456. var itemsToLoad = dojo.map(menuItemsToLoad, function(c){
  457. return c.item;
  458. });
  459. var onItems = dojo.hitch(this, function(){
  460. var selectItem = this._getSelected();
  461. var selectMenuItem;
  462. dojo.forEach(itemsToLoad, function(item, idx){
  463. var newItem = this.parentWidget._getMenuItemForItem(item, this);
  464. var oItem = menuItemsToLoad[idx];
  465. var oIdx = oItem.getIndexInParent();
  466. menu.removeChild(oItem);
  467. if(newItem){
  468. if(selectItem && this.parentWidget._itemsMatch(newItem.item, selectItem.item)){
  469. selectMenuItem = newItem;
  470. }
  471. menu.addChild(newItem, oIdx);
  472. if(menu.focusedChild == oItem){
  473. menu.focusChild(newItem);
  474. }
  475. }
  476. oItem.destroy();
  477. }, this);
  478. this._checkScrollConnection(false);
  479. });
  480. this._doLoadItems(itemsToLoad, onItems);
  481. },
  482. _getSelected: function(/*dijit.Menu?*/ menu){
  483. // summary:
  484. // returns the selected menu item - or null if none are selected
  485. if(!menu){ menu = this._menu; }
  486. if(menu){
  487. var children = this._menu.getChildren();
  488. for(var i = 0, item; (item = children[i]); i++){
  489. if(dojo.hasClass(item.domNode, "dojoxRollingListItemSelected")){
  490. return item;
  491. }
  492. }
  493. }
  494. return null;
  495. },
  496. _setSelected: function(/*dijit.MenuItem?*/ item, /*dijit.Menu?*/ menu){
  497. // summary:
  498. // selectes the given item in the given menu (defaults to pane's menu)
  499. if(!menu){ menu = this._menu;}
  500. if(menu){
  501. dojo.forEach(menu.getChildren(), function(i){
  502. this.parentWidget._updateClass(i.domNode, "Item", {"Selected": (item && (i == item && !i.disabled))});
  503. }, this);
  504. }
  505. }
  506. });
  507. dojo.declare("dojox.widget.RollingList",
  508. [dijit._Widget, dijit._Templated, dijit._Container], {
  509. // summary: a rolling list that can be tied to a data store with children
  510. // templateString: String
  511. // The template to be used to construct the widget.
  512. templateString: dojo.cache("dojox.widget", "RollingList/RollingList.html", "<div class=\"dojoxRollingList ${className}\"\n\t><div class=\"dojoxRollingListContainer\" dojoAttachPoint=\"containerNode\" dojoAttachEvent=\"onkeypress:_onKey\"\n\t></div\n\t><div class=\"dojoxRollingListButtons\" dojoAttachPoint=\"buttonsNode\"\n ><button dojoType=\"dijit.form.Button\" dojoAttachPoint=\"okButton\"\n\t\t\t\tdojoAttachEvent=\"onClick:_onExecute\">${okButtonLabel}</button\n ><button dojoType=\"dijit.form.Button\" dojoAttachPoint=\"cancelButton\"\n\t\t\t\tdojoAttachEvent=\"onClick:_onCancel\">${cancelButtonLabel}</button\n\t></div\n></div>\n"),
  513. widgetsInTemplate: true,
  514. // className: string
  515. // an additional class (or space-separated classes) to add for our widget
  516. className: "",
  517. // store: store
  518. // the store we must use
  519. store: null,
  520. // query: object
  521. // a query to pass to the datastore. This is only used if items are null
  522. query: null,
  523. // queryOptions: object
  524. // query options to be passed to the datastore
  525. queryOptions: null,
  526. // childrenAttrs: String[]
  527. // one ore more attributes that holds children of a node
  528. childrenAttrs: ["children"],
  529. // parentAttr: string
  530. // the attribute to read for finding our parent item (if any)
  531. parentAttr: "",
  532. // value: item
  533. // The value that has been selected
  534. value: null,
  535. // executeOnDblClick: boolean
  536. // Set to true if you want to call onExecute when an item is
  537. // double-clicked, false if you want to call onExecute yourself. (mainly
  538. // used for popups to control how they want to be handled)
  539. executeOnDblClick: true,
  540. // preloadItems: boolean or int
  541. // if set to true, then onItems will be called only *after* all items have
  542. // been loaded (ie store.isLoaded will return true for all of them). If
  543. // false, then no preloading will occur. If set to an integer, preloading
  544. // will occur if the number of items is less than or equal to the value
  545. // of the integer. The onItems function will need to be aware of handling
  546. // items that may not be loaded
  547. preloadItems: false,
  548. // showButtons: boolean
  549. // if set to true, then buttons for "OK" and "Cancel" will be provided
  550. showButtons: false,
  551. // okButtonLabel: string
  552. // The string to use for the OK button - will use dijit's common "OK" string
  553. // if not set
  554. okButtonLabel: "",
  555. // cancelButtonLabel: string
  556. // The string to use for the Cancel button - will use dijit's common
  557. // "Cancel" string if not set
  558. cancelButtonLabel: "",
  559. // minPaneWidth: integer
  560. // the minimum pane width (in px) for all child panes. If they are narrower,
  561. // the width will be increased to this value.
  562. minPaneWidth: 0,
  563. postMixInProperties: function(){
  564. // summary: Mix in our labels, if they are not set
  565. this.inherited(arguments);
  566. var loc = dojo.i18n.getLocalization("dijit", "common");
  567. this.okButtonLabel = this.okButtonLabel || loc.buttonOk;
  568. this.cancelButtonLabel = this.cancelButtonLabel || loc.buttonCancel;
  569. },
  570. _setShowButtonsAttr: function(doShow){
  571. // summary: Sets the visibility of the buttons for the widget
  572. var needsLayout = false;
  573. if((this.showButtons != doShow && this._started) ||
  574. (this.showButtons == doShow && !this.started)){
  575. needsLayout = true;
  576. }
  577. dojo.toggleClass(this.domNode, "dojoxRollingListButtonsHidden", !doShow);
  578. this.showButtons = doShow;
  579. if(needsLayout){
  580. if(this._started){
  581. this.layout();
  582. }else{
  583. window.setTimeout(dojo.hitch(this, "layout"), 0);
  584. }
  585. }
  586. },
  587. _itemsMatch: function(/*item*/ item1, /*item*/ item2){
  588. // Summary: returns whether or not the two items match - checks ID if
  589. // they aren't the exact same object
  590. if(!item1 && !item2){
  591. return true;
  592. }else if(!item1 || !item2){
  593. return false;
  594. }
  595. return (item1 == item2 ||
  596. (this._isIdentity && this.store.getIdentity(item1) == this.store.getIdentity(item2)));
  597. },
  598. _removeAfter: function(/*Widget or int*/ idx){
  599. // summary: removes all widgets after the given widget (or index)
  600. if(typeof idx != "number"){
  601. idx = this.getIndexOfChild(idx);
  602. }
  603. if(idx >= 0){
  604. dojo.forEach(this.getChildren(), function(c, i){
  605. if(i > idx){
  606. this.removeChild(c);
  607. c.destroyRecursive();
  608. }
  609. }, this);
  610. }
  611. var children = this.getChildren(), child = children[children.length - 1];
  612. var selItem = null;
  613. while(child && !selItem){
  614. var val = child._getSelected ? child._getSelected() : null;
  615. if(val){
  616. selItem = val.item;
  617. }
  618. child = child.parentPane;
  619. }
  620. if(!this._setInProgress){
  621. this._setValue(selItem);
  622. }
  623. },
  624. addChild: function(/*dijit._Widget*/ widget, /*int?*/ insertIndex){
  625. // summary: adds a child to this rolling list - if passed an insertIndex,
  626. // then all children from that index on will be removed and destroyed
  627. // before adding the child.
  628. if(insertIndex > 0){
  629. this._removeAfter(insertIndex - 1);
  630. }
  631. this.inherited(arguments);
  632. if(!widget._started){
  633. widget.startup();
  634. }
  635. widget.attr("minWidth", this.minPaneWidth);
  636. this.layout();
  637. if(!this._savedFocus){
  638. widget.focus();
  639. }
  640. },
  641. _setMinPaneWidthAttr: function(value){
  642. // summary:
  643. // Sets the min pane width of all children
  644. if(value !== this.minPaneWidth){
  645. this.minPaneWidth = value;
  646. dojo.forEach(this.getChildren(), function(c){
  647. c.attr("minWidth", value);
  648. });
  649. }
  650. },
  651. _updateClass: function(/* Node */ node, /* String */ type, /* Object? */ options){
  652. // summary:
  653. // sets the state of the given node with the given type and options
  654. // options:
  655. // an object with key-value-pairs. The values are boolean, if true,
  656. // the key is added as a class, if false, it is removed.
  657. if(!this._declaredClasses){
  658. this._declaredClasses = ("dojoxRollingList " + this.className).split(" ");
  659. }
  660. dojo.forEach(this._declaredClasses, function(c){
  661. if(c){
  662. dojo.addClass(node, c + type);
  663. for(var k in options||{}){
  664. dojo.toggleClass(node, c + type + k, options[k]);
  665. }
  666. dojo.toggleClass(node, c + type + "FocusSelected",
  667. (dojo.hasClass(node, c + type + "Focus") && dojo.hasClass(node, c + type + "Selected")));
  668. dojo.toggleClass(node, c + type + "HoverSelected",
  669. (dojo.hasClass(node, c + type + "Hover") && dojo.hasClass(node, c + type + "Selected")));
  670. }
  671. });
  672. },
  673. scrollIntoView: function(/*dijit._Widget*/ childWidget){
  674. // summary: scrolls the given widget into view
  675. if(this._scrollingTimeout){
  676. window.clearTimeout(this._scrollingTimeout);
  677. }
  678. delete this._scrollingTimeout;
  679. this._scrollingTimeout = window.setTimeout(dojo.hitch(this, function(){
  680. if(childWidget.domNode){
  681. dojo.window.scrollIntoView(childWidget.domNode);
  682. }
  683. delete this._scrollingTimeout;
  684. return;
  685. }), 1);
  686. },
  687. resize: function(args){
  688. dijit.layout._LayoutWidget.prototype.resize.call(this, args);
  689. },
  690. layout: function(){
  691. var children = this.getChildren();
  692. if(this._contentBox){
  693. var bn = this.buttonsNode;
  694. var height = this._contentBox.h - dojo.marginBox(bn).h -
  695. dojox.html.metrics.getScrollbar().h;
  696. dojo.forEach(children, function(c){
  697. dojo.marginBox(c.domNode, {h: height});
  698. });
  699. }
  700. if(this._focusedPane){
  701. var foc = this._focusedPane;
  702. delete this._focusedPane;
  703. if(!this._savedFocus){
  704. foc.focus();
  705. }
  706. }else if(children && children.length){
  707. if(!this._savedFocus){
  708. children[0].focus();
  709. }
  710. }
  711. },
  712. _onChange: function(/*item*/ value){
  713. this.onChange(value);
  714. },
  715. _setValue: function(/* item */ value){
  716. // summary: internally sets the value and fires onchange
  717. delete this._setInProgress;
  718. if(!this._itemsMatch(this.value, value)){
  719. this.value = value;
  720. this._onChange(value);
  721. }
  722. },
  723. _setValueAttr: function(/* item */ value){
  724. // summary: sets the value of this widget to the given store item
  725. if(this._itemsMatch(this.value, value) && !value){ return; }
  726. if(this._setInProgress && this._setInProgress === value){ return; }
  727. this._setInProgress = value;
  728. if(!value || !this.store.isItem(value)){
  729. var pane = this.getChildren()[0];
  730. pane._setSelected(null);
  731. this._onItemClick(null, pane, null, null);
  732. return;
  733. }
  734. var fetchParentItems = dojo.hitch(this, function(/*item*/ item, /*function*/callback){
  735. // Summary: Fetchs the parent items for the given item
  736. var store = this.store, id;
  737. if(this.parentAttr && store.getFeatures()["dojo.data.api.Identity"] &&
  738. ((id = this.store.getValue(item, this.parentAttr)) || id === "")){
  739. // Fetch by parent attribute
  740. var cb = function(i){
  741. if(store.getIdentity(i) == store.getIdentity(item)){
  742. callback(null);
  743. }else{
  744. callback([i]);
  745. }
  746. };
  747. if(id === ""){
  748. callback(null);
  749. }else if(typeof id == "string"){
  750. store.fetchItemByIdentity({identity: id, onItem: cb});
  751. }else if(store.isItem(id)){
  752. cb(id);
  753. }
  754. }else{
  755. // Fetch by finding children
  756. var numCheck = this.childrenAttrs.length;
  757. var parents = [];
  758. dojo.forEach(this.childrenAttrs, function(attr){
  759. var q = {};
  760. q[attr] = item;
  761. store.fetch({query: q, scope: this,
  762. onComplete: function(items){
  763. if(this._setInProgress !== value){
  764. return;
  765. }
  766. parents = parents.concat(items);
  767. numCheck--;
  768. if(numCheck === 0){
  769. callback(parents);
  770. }
  771. }
  772. });
  773. }, this);
  774. }
  775. });
  776. var setFromChain = dojo.hitch(this, function(/*item[]*/itemChain, /*integer*/idx){
  777. // Summary: Sets the value of the widget at the given index in the chain - onchanges are not
  778. // fired here
  779. var set = itemChain[idx];
  780. var child = this.getChildren()[idx];
  781. var conn;
  782. if(set && child){
  783. var fx = dojo.hitch(this, function(){
  784. if(conn){
  785. this.disconnect(conn);
  786. }
  787. delete conn;
  788. if(this._setInProgress !== value){
  789. return;
  790. }
  791. var selOpt = dojo.filter(child._menu.getChildren(), function(i){
  792. return this._itemsMatch(i.item, set);
  793. }, this)[0];
  794. if(selOpt){
  795. idx++;
  796. child._menu.onItemClick(selOpt, {type: "internal",
  797. stopPropagation: function(){},
  798. preventDefault: function(){}});
  799. if(itemChain[idx]){
  800. setFromChain(itemChain, idx);
  801. }else{
  802. this._setValue(set);
  803. this.onItemClick(set, child, this.getChildItems(set));
  804. }
  805. }
  806. });
  807. if(!child.isLoaded){
  808. conn = this.connect(child, "onLoad", fx);
  809. }else{
  810. fx();
  811. }
  812. }else if(idx === 0){
  813. this.set("value", null);
  814. }
  815. });
  816. var parentChain = [];
  817. var onParents = dojo.hitch(this, function(/*item[]*/ parents){
  818. // Summary: recursively grabs the parents - only the first one is followed
  819. if(parents && parents.length){
  820. parentChain.push(parents[0]);
  821. fetchParentItems(parents[0], onParents);
  822. }else{
  823. if(!parents){
  824. parentChain.pop();
  825. }
  826. parentChain.reverse();
  827. setFromChain(parentChain, 0);
  828. }
  829. });
  830. // Only set the value in display if we are shown - if we are in a dropdown,
  831. // and are hidden, don't actually do the scrolling in the display (it can
  832. // mess up layouts)
  833. var ns = this.domNode.style;
  834. if(ns.display == "none" || ns.visibility == "hidden"){
  835. this._setValue(value);
  836. }else if(!this._itemsMatch(value, this._visibleItem)){
  837. onParents([value]);
  838. }
  839. },
  840. _onItemClick: function(/* Event */ evt, /* dijit._Contained */ pane, /* item */ item, /* item[]? */ children){
  841. // summary: internally called when a widget should pop up its child
  842. if(evt){
  843. var itemPane = this._getPaneForItem(item, pane, children);
  844. var alreadySelected = (evt.type == "click" && evt.alreadySelected);
  845. if(alreadySelected && itemPane){
  846. this._removeAfter(pane.getIndexInParent() + 1);
  847. var next = pane.getNextSibling();
  848. if(next && next._setSelected){
  849. next._setSelected(null);
  850. }
  851. this.scrollIntoView(next);
  852. }else if(itemPane){
  853. this.addChild(itemPane, pane.getIndexInParent() + 1);
  854. if(this._savedFocus){
  855. itemPane.focus(true);
  856. }
  857. }else{
  858. this._removeAfter(pane);
  859. this.scrollIntoView(pane);
  860. }
  861. }else if(pane){
  862. this._removeAfter(pane);
  863. this.scrollIntoView(pane);
  864. }
  865. if(!evt || evt.type != "internal"){
  866. this._setValue(item);
  867. this.onItemClick(item, pane, children);
  868. }
  869. this._visibleItem = item;
  870. },
  871. _getPaneForItem: function(/* item? */ item, /* dijit._Contained? */ parentPane, /* item[]? */ children){ // summary: gets the pane for the given item, and mixes in our needed parts
  872. // Returns the pane for the given item (null if the root pane) - after mixing in
  873. // its stuff.
  874. var ret = this.getPaneForItem(item, parentPane, children);
  875. ret.store = this.store;
  876. ret.parentWidget = this;
  877. ret.parentPane = parentPane||null;
  878. if(!item){
  879. ret.query = this.query;
  880. ret.queryOptions = this.queryOptions;
  881. }else if(children){
  882. ret.items = children;
  883. }else{
  884. ret.items = [item];
  885. }
  886. return ret;
  887. },
  888. _getMenuItemForItem: function(/*item*/ item, /* dijit._Contained */ parentPane){
  889. // summary: returns a widget for the given store item. The returned
  890. // item will be added to this widget's container widget. null will
  891. // be passed in for an "empty" item.
  892. var store = this.store;
  893. if(!item || !store || !store.isItem(item)){
  894. var i = new dijit.MenuItem({
  895. label: "---",
  896. disabled: true,
  897. iconClass: "dojoxEmpty",
  898. focus: function(){
  899. // Do nothing on focus of this guy...
  900. }
  901. });
  902. this._updateClass(i.domNode, "Item");
  903. return i;
  904. }else{
  905. var itemLoaded = store.isItemLoaded(item);
  906. var childItems = itemLoaded ? this.getChildItems(item) : undefined;
  907. var widgetItem;
  908. if(childItems){
  909. widgetItem = this.getMenuItemForItem(item, parentPane, childItems);
  910. widgetItem.children = childItems;
  911. this._updateClass(widgetItem.domNode, "Item", {"Expanding": true});
  912. if(!widgetItem._started){
  913. var c = widgetItem.connect(widgetItem, "startup", function(){
  914. this.disconnect(c);
  915. dojo.style(this.arrowWrapper, "display", "");
  916. });
  917. }else{
  918. dojo.style(widgetItem.arrowWrapper, "display", "");
  919. }
  920. }else{
  921. widgetItem = this.getMenuItemForItem(item, parentPane, null);
  922. if(itemLoaded){
  923. this._updateClass(widgetItem.domNode, "Item", {"Single": true});
  924. }else{
  925. this._updateClass(widgetItem.domNode, "Item", {"Unloaded": true});
  926. widgetItem.attr("disabled", true);
  927. }
  928. }
  929. widgetItem.store = this.store;
  930. widgetItem.item = item;
  931. if(!widgetItem.label){
  932. widgetItem.attr("label", this.store.getLabel(item).replace(/</,"&lt;"));
  933. }
  934. if(widgetItem.focusNode){
  935. var self = this;
  936. widgetItem.focus = function(){
  937. // Don't set our class
  938. if(!this.disabled){try{this.focusNode.focus();}catch(e){}}
  939. };
  940. widgetItem.connect(widgetItem.focusNode, "onmouseenter", function(){
  941. if(!this.disabled){
  942. self._updateClass(this.domNode, "Item", {"Hover": true});
  943. }
  944. });
  945. widgetItem.connect(widgetItem.focusNode, "onmouseleave", function(){
  946. if(!this.disabled){
  947. self._updateClass(this.domNode, "Item", {"Hover": false});
  948. }
  949. });
  950. widgetItem.connect(widgetItem.focusNode, "blur", function(){
  951. self._updateClass(this.domNode, "Item", {"Focus": false, "Hover": false});
  952. });
  953. widgetItem.connect(widgetItem.focusNode, "focus", function(){
  954. self._updateClass(this.domNode, "Item", {"Focus": true});
  955. self._focusedPane = parentPane;
  956. });
  957. if(this.executeOnDblClick){
  958. widgetItem.connect(widgetItem.focusNode, "ondblclick", function(){
  959. self._onExecute();
  960. });
  961. }
  962. }
  963. return widgetItem;
  964. }
  965. },
  966. _setStore: function(/* dojo.data.api.Read */ store){
  967. // summary: sets the store for this widget */
  968. if(store === this.store && this._started){ return; }
  969. this.store = store;
  970. this._isIdentity = store.getFeatures()["dojo.data.api.Identity"];
  971. var rootPane = this._getPaneForItem();
  972. this.addChild(rootPane, 0);
  973. },
  974. _onKey: function(/*Event*/ e){
  975. // summary: called when a keypress event happens on this widget
  976. if(e.charOrCode == dojo.keys.BACKSPACE){
  977. dojo.stopEvent(e);
  978. return;
  979. }else if(e.charOrCode == dojo.keys.ESCAPE && this._savedFocus){
  980. try{dijit.focus(this._savedFocus);}catch(e){}
  981. dojo.stopEvent(e);
  982. return;
  983. }else if(e.charOrCode == dojo.keys.LEFT_ARROW ||
  984. e.charOrCode == dojo.keys.RIGHT_ARROW){
  985. dojo.stopEvent(e);
  986. return;
  987. }
  988. },
  989. _resetValue: function(){
  990. // Summary: function called when the value is reset.
  991. this.set("value", this._lastExecutedValue);
  992. },
  993. _onCancel: function(){
  994. // Summary: function called when the cancel button is clicked. It
  995. // resets its value to whatever was last executed and then cancels
  996. this._resetValue();
  997. this.onCancel();
  998. },
  999. _onExecute: function(){
  1000. // Summary: function called when the OK button is clicked or when an
  1001. // item is selected (double-clicked or "enter" pressed on it)
  1002. this._lastExecutedValue = this.get("value");
  1003. this.onExecute();
  1004. },
  1005. focus: function(){
  1006. // summary: sets the focus state of this widget
  1007. var wasSaved = this._savedFocus;
  1008. this._savedFocus = dijit.getFocus(this);
  1009. if(!this._savedFocus.node){
  1010. delete this._savedFocus;
  1011. }
  1012. if(!this._focusedPane){
  1013. var child = this.getChildren()[0];
  1014. if(child && !wasSaved){
  1015. child.focus(true);
  1016. }
  1017. }else{
  1018. this._savedFocus = dijit.getFocus(this);
  1019. var foc = this._focusedPane;
  1020. delete this._focusedPane;
  1021. if(!wasSaved){
  1022. foc.focus(true);
  1023. }
  1024. }
  1025. },
  1026. handleKey:function(/*Event*/e){
  1027. // summary: handle the key for the given event - called by dropdown
  1028. // widgets
  1029. if(e.charOrCode == dojo.keys.DOWN_ARROW){
  1030. delete this._savedFocus;
  1031. this.focus();
  1032. return false;
  1033. }else if(e.charOrCode == dojo.keys.ESCAPE){
  1034. this._onCancel();
  1035. return false;
  1036. }
  1037. return true;
  1038. },
  1039. _updateChildClasses: function(){
  1040. // summary: Called when a child is added or removed - so that we can
  1041. // update the classes for styling the "current" one differently than
  1042. // the others
  1043. var children = this.getChildren();
  1044. var length = children.length;
  1045. dojo.forEach(children, function(c, idx){
  1046. dojo.toggleClass(c.domNode, "dojoxRollingListPaneCurrentChild", (idx == (length - 1)));
  1047. dojo.toggleClass(c.domNode, "dojoxRollingListPaneCurrentSelected", (idx == (length - 2)));
  1048. });
  1049. },
  1050. startup: function(){
  1051. if(this._started){ return; }
  1052. if(!this.getParent || !this.getParent()){
  1053. this.resize();
  1054. this.connect(dojo.global, "onresize", "resize");
  1055. }
  1056. this.connect(this, "addChild", "_updateChildClasses");
  1057. this.connect(this, "removeChild", "_updateChildClasses");
  1058. this._setStore(this.store);
  1059. this.set("showButtons", this.showButtons);
  1060. this.inherited(arguments);
  1061. this._lastExecutedValue = this.get("value");
  1062. },
  1063. getChildItems: function(/*item*/ item){
  1064. // summary: Returns the child items for the given store item
  1065. var childItems, store = this.store;
  1066. dojo.forEach(this.childrenAttrs, function(attr){
  1067. var vals = store.getValues(item, attr);
  1068. if(vals && vals.length){
  1069. childItems = (childItems||[]).concat(vals);
  1070. }
  1071. });
  1072. return childItems;
  1073. },
  1074. getMenuItemForItem: function(/*item*/ item, /* dijit._Contained */ parentPane, /* item[]? */ children){
  1075. // summary: user overridable function to return a widget for the given item
  1076. // and its children.
  1077. return new dijit.MenuItem({});
  1078. },
  1079. getPaneForItem: function(/* item? */ item, /* dijit._Contained? */ parentPane, /* item[]? */ children){
  1080. // summary: user-overridable function to return a pane that corresponds
  1081. // to the given item in the store. It can return null to not add a new pane
  1082. // (ie, you are planning on doing something else with it in onItemClick)
  1083. //
  1084. // Item is undefined for the root pane, children is undefined for non-group panes
  1085. if(!item || children){
  1086. return new dojox.widget._RollingListGroupPane({});
  1087. }else{
  1088. return null;
  1089. }
  1090. },
  1091. onItemClick: function(/* item */ item, /* dijit._Contained */ pane, /* item[]? */ children){
  1092. // summary: called when an item is clicked - it receives the store item
  1093. },
  1094. onExecute: function(){
  1095. // summary: exists so that popups don't disappear too soon
  1096. },
  1097. onCancel: function(){
  1098. // summary: exists so that we can close ourselves if we wish
  1099. },
  1100. onChange: function(/* item */ value){
  1101. // summary: called when the value of this widget has changed
  1102. }
  1103. });
  1104. });