Tree.js 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634
  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["dijit.Tree"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.Tree"] = true;
  8. dojo.provide("dijit.Tree");
  9. dojo.require("dojo.fx");
  10. dojo.require("dojo.DeferredList");
  11. dojo.require("dijit._Widget");
  12. dojo.require("dijit._Templated");
  13. dojo.require("dijit._Container");
  14. dojo.require("dijit._Contained");
  15. dojo.require("dijit._CssStateMixin");
  16. dojo.require("dojo.cookie");
  17. dojo.require("dijit.tree.TreeStoreModel");
  18. dojo.require("dijit.tree.ForestStoreModel");
  19. dojo.require("dijit.tree._dndSelector");
  20. dojo.declare(
  21. "dijit._TreeNode",
  22. [dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained, dijit._CssStateMixin],
  23. {
  24. // summary:
  25. // Single node within a tree. This class is used internally
  26. // by Tree and should not be accessed directly.
  27. // tags:
  28. // private
  29. // item: [const] dojo.data.Item
  30. // the dojo.data entry this tree represents
  31. item: null,
  32. // isTreeNode: [protected] Boolean
  33. // Indicates that this is a TreeNode. Used by `dijit.Tree` only,
  34. // should not be accessed directly.
  35. isTreeNode: true,
  36. // label: String
  37. // Text of this tree node
  38. label: "",
  39. // isExpandable: [private] Boolean
  40. // This node has children, so show the expando node (+ sign)
  41. isExpandable: null,
  42. // isExpanded: [readonly] Boolean
  43. // This node is currently expanded (ie, opened)
  44. isExpanded: false,
  45. // state: [private] String
  46. // Dynamic loading-related stuff.
  47. // When an empty folder node appears, it is "UNCHECKED" first,
  48. // then after dojo.data query it becomes "LOADING" and, finally "LOADED"
  49. state: "UNCHECKED",
  50. templateString: dojo.cache("dijit", "templates/TreeNode.html", "<div class=\"dijitTreeNode\" role=\"presentation\"\n\t><div dojoAttachPoint=\"rowNode\" class=\"dijitTreeRow\" role=\"presentation\" dojoAttachEvent=\"onmouseenter:_onMouseEnter, onmouseleave:_onMouseLeave, onclick:_onClick, ondblclick:_onDblClick\"\n\t\t><img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"expandoNode\" class=\"dijitTreeExpando\" role=\"presentation\"\n\t\t/><span dojoAttachPoint=\"expandoNodeText\" class=\"dijitExpandoText\" role=\"presentation\"\n\t\t></span\n\t\t><span dojoAttachPoint=\"contentNode\"\n\t\t\tclass=\"dijitTreeContent\" role=\"presentation\">\n\t\t\t<img src=\"${_blankGif}\" alt=\"\" dojoAttachPoint=\"iconNode\" class=\"dijitIcon dijitTreeIcon\" role=\"presentation\"\n\t\t\t/><span dojoAttachPoint=\"labelNode\" class=\"dijitTreeLabel\" role=\"treeitem\" tabindex=\"-1\" aria-selected=\"false\" dojoAttachEvent=\"onfocus:_onLabelFocus\"></span>\n\t\t</span\n\t></div>\n\t<div dojoAttachPoint=\"containerNode\" class=\"dijitTreeContainer\" role=\"presentation\" style=\"display: none;\"></div>\n</div>\n"),
  51. baseClass: "dijitTreeNode",
  52. // For hover effect for tree node, and focus effect for label
  53. cssStateNodes: {
  54. rowNode: "dijitTreeRow",
  55. labelNode: "dijitTreeLabel"
  56. },
  57. attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, {
  58. label: {node: "labelNode", type: "innerText"},
  59. tooltip: {node: "rowNode", type: "attribute", attribute: "title"}
  60. }),
  61. buildRendering: function(){
  62. this.inherited(arguments);
  63. // set expand icon for leaf
  64. this._setExpando();
  65. // set icon and label class based on item
  66. this._updateItemClasses(this.item);
  67. if(this.isExpandable){
  68. dijit.setWaiState(this.labelNode, "expanded", this.isExpanded);
  69. }
  70. //aria-selected should be false on all selectable elements.
  71. this.setSelected(false);
  72. },
  73. _setIndentAttr: function(indent){
  74. // summary:
  75. // Tell this node how many levels it should be indented
  76. // description:
  77. // 0 for top level nodes, 1 for their children, 2 for their
  78. // grandchildren, etc.
  79. // Math.max() is to prevent negative padding on hidden root node (when indent == -1)
  80. var pixels = (Math.max(indent, 0) * this.tree._nodePixelIndent) + "px";
  81. dojo.style(this.domNode, "backgroundPosition", pixels + " 0px");
  82. dojo.style(this.rowNode, this.isLeftToRight() ? "paddingLeft" : "paddingRight", pixels);
  83. dojo.forEach(this.getChildren(), function(child){
  84. child.set("indent", indent+1);
  85. });
  86. this._set("indent", indent);
  87. },
  88. markProcessing: function(){
  89. // summary:
  90. // Visually denote that tree is loading data, etc.
  91. // tags:
  92. // private
  93. this.state = "LOADING";
  94. this._setExpando(true);
  95. },
  96. unmarkProcessing: function(){
  97. // summary:
  98. // Clear markup from markProcessing() call
  99. // tags:
  100. // private
  101. this._setExpando(false);
  102. },
  103. _updateItemClasses: function(item){
  104. // summary:
  105. // Set appropriate CSS classes for icon and label dom node
  106. // (used to allow for item updates to change respective CSS)
  107. // tags:
  108. // private
  109. var tree = this.tree, model = tree.model;
  110. if(tree._v10Compat && item === model.root){
  111. // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0)
  112. item = null;
  113. }
  114. this._applyClassAndStyle(item, "icon", "Icon");
  115. this._applyClassAndStyle(item, "label", "Label");
  116. this._applyClassAndStyle(item, "row", "Row");
  117. },
  118. _applyClassAndStyle: function(item, lower, upper){
  119. // summary:
  120. // Set the appropriate CSS classes and styles for labels, icons and rows.
  121. //
  122. // item:
  123. // The data item.
  124. //
  125. // lower:
  126. // The lower case attribute to use, e.g. 'icon', 'label' or 'row'.
  127. //
  128. // upper:
  129. // The upper case attribute to use, e.g. 'Icon', 'Label' or 'Row'.
  130. //
  131. // tags:
  132. // private
  133. var clsName = "_" + lower + "Class";
  134. var nodeName = lower + "Node";
  135. var oldCls = this[clsName];
  136. this[clsName] = this.tree["get" + upper + "Class"](item, this.isExpanded);
  137. dojo.replaceClass(this[nodeName], this[clsName] || "", oldCls || "");
  138. dojo.style(this[nodeName], this.tree["get" + upper + "Style"](item, this.isExpanded) || {});
  139. },
  140. _updateLayout: function(){
  141. // summary:
  142. // Set appropriate CSS classes for this.domNode
  143. // tags:
  144. // private
  145. var parent = this.getParent();
  146. if(!parent || parent.rowNode.style.display == "none"){
  147. /* if we are hiding the root node then make every first level child look like a root node */
  148. dojo.addClass(this.domNode, "dijitTreeIsRoot");
  149. }else{
  150. dojo.toggleClass(this.domNode, "dijitTreeIsLast", !this.getNextSibling());
  151. }
  152. },
  153. _setExpando: function(/*Boolean*/ processing){
  154. // summary:
  155. // Set the right image for the expando node
  156. // tags:
  157. // private
  158. var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened",
  159. "dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"],
  160. _a11yStates = ["*","-","+","*"],
  161. idx = processing ? 0 : (this.isExpandable ? (this.isExpanded ? 1 : 2) : 3);
  162. // apply the appropriate class to the expando node
  163. dojo.replaceClass(this.expandoNode, styles[idx], styles);
  164. // provide a non-image based indicator for images-off mode
  165. this.expandoNodeText.innerHTML = _a11yStates[idx];
  166. },
  167. expand: function(){
  168. // summary:
  169. // Show my children
  170. // returns:
  171. // Deferred that fires when expansion is complete
  172. // If there's already an expand in progress or we are already expanded, just return
  173. if(this._expandDeferred){
  174. return this._expandDeferred; // dojo.Deferred
  175. }
  176. // cancel in progress collapse operation
  177. this._wipeOut && this._wipeOut.stop();
  178. // All the state information for when a node is expanded, maybe this should be
  179. // set when the animation completes instead
  180. this.isExpanded = true;
  181. dijit.setWaiState(this.labelNode, "expanded", "true");
  182. if(this.tree.showRoot || this !== this.tree.rootNode){
  183. dijit.setWaiRole(this.containerNode, "group");
  184. }
  185. dojo.addClass(this.contentNode,'dijitTreeContentExpanded');
  186. this._setExpando();
  187. this._updateItemClasses(this.item);
  188. if(this == this.tree.rootNode){
  189. dijit.setWaiState(this.tree.domNode, "expanded", "true");
  190. }
  191. var def,
  192. wipeIn = dojo.fx.wipeIn({
  193. node: this.containerNode, duration: dijit.defaultDuration,
  194. onEnd: function(){
  195. def.callback(true);
  196. }
  197. });
  198. // Deferred that fires when expand is complete
  199. def = (this._expandDeferred = new dojo.Deferred(function(){
  200. // Canceller
  201. wipeIn.stop();
  202. }));
  203. wipeIn.play();
  204. return def; // dojo.Deferred
  205. },
  206. collapse: function(){
  207. // summary:
  208. // Collapse this node (if it's expanded)
  209. if(!this.isExpanded){ return; }
  210. // cancel in progress expand operation
  211. if(this._expandDeferred){
  212. this._expandDeferred.cancel();
  213. delete this._expandDeferred;
  214. }
  215. this.isExpanded = false;
  216. dijit.setWaiState(this.labelNode, "expanded", "false");
  217. if(this == this.tree.rootNode){
  218. dijit.setWaiState(this.tree.domNode, "expanded", "false");
  219. }
  220. dojo.removeClass(this.contentNode,'dijitTreeContentExpanded');
  221. this._setExpando();
  222. this._updateItemClasses(this.item);
  223. if(!this._wipeOut){
  224. this._wipeOut = dojo.fx.wipeOut({
  225. node: this.containerNode, duration: dijit.defaultDuration
  226. });
  227. }
  228. this._wipeOut.play();
  229. },
  230. // indent: Integer
  231. // Levels from this node to the root node
  232. indent: 0,
  233. setChildItems: function(/* Object[] */ items){
  234. // summary:
  235. // Sets the child items of this node, removing/adding nodes
  236. // from current children to match specified items[] array.
  237. // Also, if this.persist == true, expands any children that were previously
  238. // opened.
  239. // returns:
  240. // Deferred object that fires after all previously opened children
  241. // have been expanded again (or fires instantly if there are no such children).
  242. var tree = this.tree,
  243. model = tree.model,
  244. defs = []; // list of deferreds that need to fire before I am complete
  245. // Orphan all my existing children.
  246. // If items contains some of the same items as before then we will reattach them.
  247. // Don't call this.removeChild() because that will collapse the tree etc.
  248. dojo.forEach(this.getChildren(), function(child){
  249. dijit._Container.prototype.removeChild.call(this, child);
  250. }, this);
  251. this.state = "LOADED";
  252. if(items && items.length > 0){
  253. this.isExpandable = true;
  254. // Create _TreeNode widget for each specified tree node, unless one already
  255. // exists and isn't being used (presumably it's from a DnD move and was recently
  256. // released
  257. dojo.forEach(items, function(item){
  258. var id = model.getIdentity(item),
  259. existingNodes = tree._itemNodesMap[id],
  260. node;
  261. if(existingNodes){
  262. for(var i=0;i<existingNodes.length;i++){
  263. if(existingNodes[i] && !existingNodes[i].getParent()){
  264. node = existingNodes[i];
  265. node.set('indent', this.indent+1);
  266. break;
  267. }
  268. }
  269. }
  270. if(!node){
  271. node = this.tree._createTreeNode({
  272. item: item,
  273. tree: tree,
  274. isExpandable: model.mayHaveChildren(item),
  275. label: tree.getLabel(item),
  276. tooltip: tree.getTooltip(item),
  277. dir: tree.dir,
  278. lang: tree.lang,
  279. indent: this.indent + 1
  280. });
  281. if(existingNodes){
  282. existingNodes.push(node);
  283. }else{
  284. tree._itemNodesMap[id] = [node];
  285. }
  286. }
  287. this.addChild(node);
  288. // If node was previously opened then open it again now (this may trigger
  289. // more data store accesses, recursively)
  290. if(this.tree.autoExpand || this.tree._state(item)){
  291. defs.push(tree._expandNode(node));
  292. }
  293. }, this);
  294. // note that updateLayout() needs to be called on each child after
  295. // _all_ the children exist
  296. dojo.forEach(this.getChildren(), function(child, idx){
  297. child._updateLayout();
  298. });
  299. }else{
  300. this.isExpandable=false;
  301. }
  302. if(this._setExpando){
  303. // change expando to/from dot or + icon, as appropriate
  304. this._setExpando(false);
  305. }
  306. // Set leaf icon or folder icon, as appropriate
  307. this._updateItemClasses(this.item);
  308. // On initial tree show, make the selected TreeNode as either the root node of the tree,
  309. // or the first child, if the root node is hidden
  310. if(this == tree.rootNode){
  311. var fc = this.tree.showRoot ? this : this.getChildren()[0];
  312. if(fc){
  313. fc.setFocusable(true);
  314. tree.lastFocused = fc;
  315. }else{
  316. // fallback: no nodes in tree so focus on Tree <div> itself
  317. tree.domNode.setAttribute("tabIndex", "0");
  318. }
  319. }
  320. return new dojo.DeferredList(defs); // dojo.Deferred
  321. },
  322. getTreePath: function(){
  323. var node = this;
  324. var path = [];
  325. while(node && node !== this.tree.rootNode){
  326. path.unshift(node.item);
  327. node = node.getParent();
  328. }
  329. path.unshift(this.tree.rootNode.item);
  330. return path;
  331. },
  332. getIdentity: function() {
  333. return this.tree.model.getIdentity(this.item);
  334. },
  335. removeChild: function(/* treeNode */ node){
  336. this.inherited(arguments);
  337. var children = this.getChildren();
  338. if(children.length == 0){
  339. this.isExpandable = false;
  340. this.collapse();
  341. }
  342. dojo.forEach(children, function(child){
  343. child._updateLayout();
  344. });
  345. },
  346. makeExpandable: function(){
  347. // summary:
  348. // if this node wasn't already showing the expando node,
  349. // turn it into one and call _setExpando()
  350. // TODO: hmm this isn't called from anywhere, maybe should remove it for 2.0
  351. this.isExpandable = true;
  352. this._setExpando(false);
  353. },
  354. _onLabelFocus: function(evt){
  355. // summary:
  356. // Called when this row is focused (possibly programatically)
  357. // Note that we aren't using _onFocus() builtin to dijit
  358. // because it's called when focus is moved to a descendant TreeNode.
  359. // tags:
  360. // private
  361. this.tree._onNodeFocus(this);
  362. },
  363. setSelected: function(/*Boolean*/ selected){
  364. // summary:
  365. // A Tree has a (single) currently selected node.
  366. // Mark that this node is/isn't that currently selected node.
  367. // description:
  368. // In particular, setting a node as selected involves setting tabIndex
  369. // so that when user tabs to the tree, focus will go to that node (only).
  370. dijit.setWaiState(this.labelNode, "selected", selected);
  371. dojo.toggleClass(this.rowNode, "dijitTreeRowSelected", selected);
  372. },
  373. setFocusable: function(/*Boolean*/ selected){
  374. // summary:
  375. // A Tree has a (single) node that's focusable.
  376. // Mark that this node is/isn't that currently focsuable node.
  377. // description:
  378. // In particular, setting a node as selected involves setting tabIndex
  379. // so that when user tabs to the tree, focus will go to that node (only).
  380. this.labelNode.setAttribute("tabIndex", selected ? "0" : "-1");
  381. },
  382. _onClick: function(evt){
  383. // summary:
  384. // Handler for onclick event on a node
  385. // tags:
  386. // private
  387. this.tree._onClick(this, evt);
  388. },
  389. _onDblClick: function(evt){
  390. // summary:
  391. // Handler for ondblclick event on a node
  392. // tags:
  393. // private
  394. this.tree._onDblClick(this, evt);
  395. },
  396. _onMouseEnter: function(evt){
  397. // summary:
  398. // Handler for onmouseenter event on a node
  399. // tags:
  400. // private
  401. this.tree._onNodeMouseEnter(this, evt);
  402. },
  403. _onMouseLeave: function(evt){
  404. // summary:
  405. // Handler for onmouseenter event on a node
  406. // tags:
  407. // private
  408. this.tree._onNodeMouseLeave(this, evt);
  409. }
  410. });
  411. dojo.declare(
  412. "dijit.Tree",
  413. [dijit._Widget, dijit._Templated],
  414. {
  415. // summary:
  416. // This widget displays hierarchical data from a store.
  417. // store: [deprecated] String||dojo.data.Store
  418. // Deprecated. Use "model" parameter instead.
  419. // The store to get data to display in the tree.
  420. store: null,
  421. // model: dijit.Tree.model
  422. // Interface to read tree data, get notifications of changes to tree data,
  423. // and for handling drop operations (i.e drag and drop onto the tree)
  424. model: null,
  425. // query: [deprecated] anything
  426. // Deprecated. User should specify query to the model directly instead.
  427. // Specifies datastore query to return the root item or top items for the tree.
  428. query: null,
  429. // label: [deprecated] String
  430. // Deprecated. Use dijit.tree.ForestStoreModel directly instead.
  431. // Used in conjunction with query parameter.
  432. // If a query is specified (rather than a root node id), and a label is also specified,
  433. // then a fake root node is created and displayed, with this label.
  434. label: "",
  435. // showRoot: [const] Boolean
  436. // Should the root node be displayed, or hidden?
  437. showRoot: true,
  438. // childrenAttr: [deprecated] String[]
  439. // Deprecated. This information should be specified in the model.
  440. // One ore more attributes that holds children of a tree node
  441. childrenAttr: ["children"],
  442. // paths: String[][] or Item[][]
  443. // Full paths from rootNode to selected nodes expressed as array of items or array of ids.
  444. // Since setting the paths may be asynchronous (because ofwaiting on dojo.data), set("paths", ...)
  445. // returns a Deferred to indicate when the set is complete.
  446. paths: [],
  447. // path: String[] or Item[]
  448. // Backward compatible singular variant of paths.
  449. path: [],
  450. // selectedItems: [readonly] Item[]
  451. // The currently selected items in this tree.
  452. // This property can only be set (via set('selectedItems', ...)) when that item is already
  453. // visible in the tree. (I.e. the tree has already been expanded to show that node.)
  454. // Should generally use `paths` attribute to set the selected items instead.
  455. selectedItems: null,
  456. // selectedItem: [readonly] Item
  457. // Backward compatible singular variant of selectedItems.
  458. selectedItem: null,
  459. // openOnClick: Boolean
  460. // If true, clicking a folder node's label will open it, rather than calling onClick()
  461. openOnClick: false,
  462. // openOnDblClick: Boolean
  463. // If true, double-clicking a folder node's label will open it, rather than calling onDblClick()
  464. openOnDblClick: false,
  465. templateString: dojo.cache("dijit", "templates/Tree.html", "<div class=\"dijitTree dijitTreeContainer\" role=\"tree\"\n\tdojoAttachEvent=\"onkeypress:_onKeyPress\">\n\t<div class=\"dijitInline dijitTreeIndent\" style=\"position: absolute; top: -9999px\" dojoAttachPoint=\"indentDetector\"></div>\n</div>\n"),
  466. // persist: Boolean
  467. // Enables/disables use of cookies for state saving.
  468. persist: true,
  469. // autoExpand: Boolean
  470. // Fully expand the tree on load. Overrides `persist`.
  471. autoExpand: false,
  472. // dndController: [protected] String
  473. // Class name to use as as the dnd controller. Specifying this class enables DnD.
  474. // Generally you should specify this as "dijit.tree.dndSource".
  475. // Default of "dijit.tree._dndSelector" handles selection only (no actual DnD).
  476. dndController: "dijit.tree._dndSelector",
  477. // parameters to pull off of the tree and pass on to the dndController as its params
  478. dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance", "dragThreshold", "betweenThreshold"],
  479. //declare the above items so they can be pulled from the tree's markup
  480. // onDndDrop: [protected] Function
  481. // Parameter to dndController, see `dijit.tree.dndSource.onDndDrop`.
  482. // Generally this doesn't need to be set.
  483. onDndDrop: null,
  484. /*=====
  485. itemCreator: function(nodes, target, source){
  486. // summary:
  487. // Returns objects passed to `Tree.model.newItem()` based on DnD nodes
  488. // dropped onto the tree. Developer must override this method to enable
  489. // dropping from external sources onto this Tree, unless the Tree.model's items
  490. // happen to look like {id: 123, name: "Apple" } with no other attributes.
  491. // description:
  492. // For each node in nodes[], which came from source, create a hash of name/value
  493. // pairs to be passed to Tree.model.newItem(). Returns array of those hashes.
  494. // nodes: DomNode[]
  495. // The DOMNodes dragged from the source container
  496. // target: DomNode
  497. // The target TreeNode.rowNode
  498. // source: dojo.dnd.Source
  499. // The source container the nodes were dragged from, perhaps another Tree or a plain dojo.dnd.Source
  500. // returns: Object[]
  501. // Array of name/value hashes for each new item to be added to the Tree, like:
  502. // | [
  503. // | { id: 123, label: "apple", foo: "bar" },
  504. // | { id: 456, label: "pear", zaz: "bam" }
  505. // | ]
  506. // tags:
  507. // extension
  508. return [{}];
  509. },
  510. =====*/
  511. itemCreator: null,
  512. // onDndCancel: [protected] Function
  513. // Parameter to dndController, see `dijit.tree.dndSource.onDndCancel`.
  514. // Generally this doesn't need to be set.
  515. onDndCancel: null,
  516. /*=====
  517. checkAcceptance: function(source, nodes){
  518. // summary:
  519. // Checks if the Tree itself can accept nodes from this source
  520. // source: dijit.tree._dndSource
  521. // The source which provides items
  522. // nodes: DOMNode[]
  523. // Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
  524. // source is a dijit.Tree.
  525. // tags:
  526. // extension
  527. return true; // Boolean
  528. },
  529. =====*/
  530. checkAcceptance: null,
  531. /*=====
  532. checkItemAcceptance: function(target, source, position){
  533. // summary:
  534. // Stub function to be overridden if one wants to check for the ability to drop at the node/item level
  535. // description:
  536. // In the base case, this is called to check if target can become a child of source.
  537. // When betweenThreshold is set, position="before" or "after" means that we
  538. // are asking if the source node can be dropped before/after the target node.
  539. // target: DOMNode
  540. // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
  541. // Use dijit.getEnclosingWidget(target) to get the TreeNode.
  542. // source: dijit.tree.dndSource
  543. // The (set of) nodes we are dropping
  544. // position: String
  545. // "over", "before", or "after"
  546. // tags:
  547. // extension
  548. return true; // Boolean
  549. },
  550. =====*/
  551. checkItemAcceptance: null,
  552. // dragThreshold: Integer
  553. // Number of pixels mouse moves before it's considered the start of a drag operation
  554. dragThreshold: 5,
  555. // betweenThreshold: Integer
  556. // Set to a positive value to allow drag and drop "between" nodes.
  557. //
  558. // If during DnD mouse is over a (target) node but less than betweenThreshold
  559. // pixels from the bottom edge, dropping the the dragged node will make it
  560. // the next sibling of the target node, rather than the child.
  561. //
  562. // Similarly, if mouse is over a target node but less that betweenThreshold
  563. // pixels from the top edge, dropping the dragged node will make it
  564. // the target node's previous sibling rather than the target node's child.
  565. betweenThreshold: 0,
  566. // _nodePixelIndent: Integer
  567. // Number of pixels to indent tree nodes (relative to parent node).
  568. // Default is 19 but can be overridden by setting CSS class dijitTreeIndent
  569. // and calling resize() or startup() on tree after it's in the DOM.
  570. _nodePixelIndent: 19,
  571. _publish: function(/*String*/ topicName, /*Object*/ message){
  572. // summary:
  573. // Publish a message for this widget/topic
  574. dojo.publish(this.id, [dojo.mixin({tree: this, event: topicName}, message || {})]);
  575. },
  576. postMixInProperties: function(){
  577. this.tree = this;
  578. if(this.autoExpand){
  579. // There's little point in saving opened/closed state of nodes for a Tree
  580. // that initially opens all it's nodes.
  581. this.persist = false;
  582. }
  583. this._itemNodesMap={};
  584. if(!this.cookieName){
  585. this.cookieName = this.id + "SaveStateCookie";
  586. }
  587. this._loadDeferred = new dojo.Deferred();
  588. this.inherited(arguments);
  589. },
  590. postCreate: function(){
  591. this._initState();
  592. // Create glue between store and Tree, if not specified directly by user
  593. if(!this.model){
  594. this._store2model();
  595. }
  596. // monitor changes to items
  597. this.connect(this.model, "onChange", "_onItemChange");
  598. this.connect(this.model, "onChildrenChange", "_onItemChildrenChange");
  599. this.connect(this.model, "onDelete", "_onItemDelete");
  600. this._load();
  601. this.inherited(arguments);
  602. if(this.dndController){
  603. if(dojo.isString(this.dndController)){
  604. this.dndController = dojo.getObject(this.dndController);
  605. }
  606. var params={};
  607. for(var i=0; i<this.dndParams.length;i++){
  608. if(this[this.dndParams[i]]){
  609. params[this.dndParams[i]] = this[this.dndParams[i]];
  610. }
  611. }
  612. this.dndController = new this.dndController(this, params);
  613. }
  614. },
  615. _store2model: function(){
  616. // summary:
  617. // User specified a store&query rather than model, so create model from store/query
  618. this._v10Compat = true;
  619. dojo.deprecated("Tree: from version 2.0, should specify a model object rather than a store/query");
  620. var modelParams = {
  621. id: this.id + "_ForestStoreModel",
  622. store: this.store,
  623. query: this.query,
  624. childrenAttrs: this.childrenAttr
  625. };
  626. // Only override the model's mayHaveChildren() method if the user has specified an override
  627. if(this.params.mayHaveChildren){
  628. modelParams.mayHaveChildren = dojo.hitch(this, "mayHaveChildren");
  629. }
  630. if(this.params.getItemChildren){
  631. modelParams.getChildren = dojo.hitch(this, function(item, onComplete, onError){
  632. this.getItemChildren((this._v10Compat && item === this.model.root) ? null : item, onComplete, onError);
  633. });
  634. }
  635. this.model = new dijit.tree.ForestStoreModel(modelParams);
  636. // For backwards compatibility, the visibility of the root node is controlled by
  637. // whether or not the user has specified a label
  638. this.showRoot = Boolean(this.label);
  639. },
  640. onLoad: function(){
  641. // summary:
  642. // Called when tree finishes loading and expanding.
  643. // description:
  644. // If persist == true the loading may encompass many levels of fetches
  645. // from the data store, each asynchronous. Waits for all to finish.
  646. // tags:
  647. // callback
  648. },
  649. _load: function(){
  650. // summary:
  651. // Initial load of the tree.
  652. // Load root node (possibly hidden) and it's children.
  653. this.model.getRoot(
  654. dojo.hitch(this, function(item){
  655. var rn = (this.rootNode = this.tree._createTreeNode({
  656. item: item,
  657. tree: this,
  658. isExpandable: true,
  659. label: this.label || this.getLabel(item),
  660. indent: this.showRoot ? 0 : -1
  661. }));
  662. if(!this.showRoot){
  663. rn.rowNode.style.display="none";
  664. // if root is not visible, move tree role to the invisible
  665. // root node's containerNode, see #12135
  666. dijit.setWaiRole(this.domNode, 'presentation');
  667. dijit.setWaiRole(rn.labelNode, 'presentation');
  668. dijit.setWaiRole(rn.containerNode, 'tree');
  669. }
  670. this.domNode.appendChild(rn.domNode);
  671. var identity = this.model.getIdentity(item);
  672. if(this._itemNodesMap[identity]){
  673. this._itemNodesMap[identity].push(rn);
  674. }else{
  675. this._itemNodesMap[identity] = [rn];
  676. }
  677. rn._updateLayout(); // sets "dijitTreeIsRoot" CSS classname
  678. // load top level children and then fire onLoad() event
  679. this._expandNode(rn).addCallback(dojo.hitch(this, function(){
  680. this._loadDeferred.callback(true);
  681. this.onLoad();
  682. }));
  683. }),
  684. function(err){
  685. console.error(this, ": error loading root: ", err);
  686. }
  687. );
  688. },
  689. getNodesByItem: function(/*dojo.data.Item or id*/ item){
  690. // summary:
  691. // Returns all tree nodes that refer to an item
  692. // returns:
  693. // Array of tree nodes that refer to passed item
  694. if(!item){ return []; }
  695. var identity = dojo.isString(item) ? item : this.model.getIdentity(item);
  696. // return a copy so widget don't get messed up by changes to returned array
  697. return [].concat(this._itemNodesMap[identity]);
  698. },
  699. _setSelectedItemAttr: function(/*dojo.data.Item or id*/ item){
  700. this.set('selectedItems', [item]);
  701. },
  702. _setSelectedItemsAttr: function(/*dojo.data.Items or ids*/ items){
  703. // summary:
  704. // Select tree nodes related to passed items.
  705. // WARNING: if model use multi-parented items or desired tree node isn't already loaded
  706. // behavior is undefined. Use set('paths', ...) instead.
  707. var tree = this;
  708. this._loadDeferred.addCallback( dojo.hitch(this, function(){
  709. var identities = dojo.map(items, function(item){
  710. return (!item || dojo.isString(item)) ? item : tree.model.getIdentity(item);
  711. });
  712. var nodes = [];
  713. dojo.forEach(identities, function(id){
  714. nodes = nodes.concat(tree._itemNodesMap[id] || []);
  715. });
  716. this.set('selectedNodes', nodes);
  717. }));
  718. },
  719. _setPathAttr: function(/*Item[] || String[]*/ path){
  720. // summary:
  721. // Singular variant of _setPathsAttr
  722. if(path.length) {
  723. return this.set("paths", [path]);
  724. } else {
  725. //Empty list is interpreted as "select nothing"
  726. return this.set("paths", []);
  727. }
  728. },
  729. _setPathsAttr: function(/*Item[][] || String[][]*/ paths){
  730. // summary:
  731. // Select the tree nodes identified by passed paths.
  732. // paths:
  733. // Array of arrays of items or item id's
  734. // returns:
  735. // Deferred to indicate when the set is complete
  736. var tree = this;
  737. // We may need to wait for some nodes to expand, so setting
  738. // each path will involve a Deferred. We bring those deferreds
  739. // together witha DeferredList.
  740. return new dojo.DeferredList(dojo.map(paths, function(path){
  741. var d = new dojo.Deferred();
  742. // normalize path to use identity
  743. path = dojo.map(path, function(item){
  744. return dojo.isString(item) ? item : tree.model.getIdentity(item);
  745. });
  746. if(path.length){
  747. // Wait for the tree to load, if it hasn't already.
  748. tree._loadDeferred.addCallback(function(){ selectPath(path, [tree.rootNode], d); });
  749. }else{
  750. d.errback("Empty path");
  751. }
  752. return d;
  753. })).addCallback(setNodes);
  754. function selectPath(path, nodes, def){
  755. // Traverse path; the next path component should be among "nodes".
  756. var nextPath = path.shift();
  757. var nextNode = dojo.filter(nodes, function(node){
  758. return node.getIdentity() == nextPath;
  759. })[0];
  760. if(!!nextNode){
  761. if(path.length){
  762. tree._expandNode(nextNode).addCallback(function(){ selectPath(path, nextNode.getChildren(), def); });
  763. }else{
  764. //Successfully reached the end of this path
  765. def.callback(nextNode);
  766. }
  767. } else {
  768. def.errback("Could not expand path at " + nextPath);
  769. }
  770. }
  771. function setNodes(newNodes){
  772. //After all expansion is finished, set the selection to
  773. //the set of nodes successfully found.
  774. tree.set("selectedNodes", dojo.map(
  775. dojo.filter(newNodes,function(x){return x[0];}),
  776. function(x){return x[1];}));
  777. }
  778. },
  779. _setSelectedNodeAttr: function(node){
  780. this.set('selectedNodes', [node]);
  781. },
  782. _setSelectedNodesAttr: function(nodes){
  783. this._loadDeferred.addCallback( dojo.hitch(this, function(){
  784. this.dndController.setSelection(nodes);
  785. }));
  786. },
  787. ////////////// Data store related functions //////////////////////
  788. // These just get passed to the model; they are here for back-compat
  789. mayHaveChildren: function(/*dojo.data.Item*/ item){
  790. // summary:
  791. // Deprecated. This should be specified on the model itself.
  792. //
  793. // Overridable function to tell if an item has or may have children.
  794. // Controls whether or not +/- expando icon is shown.
  795. // (For efficiency reasons we may not want to check if an element actually
  796. // has children until user clicks the expando node)
  797. // tags:
  798. // deprecated
  799. },
  800. getItemChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete){
  801. // summary:
  802. // Deprecated. This should be specified on the model itself.
  803. //
  804. // Overridable function that return array of child items of given parent item,
  805. // or if parentItem==null then return top items in tree
  806. // tags:
  807. // deprecated
  808. },
  809. ///////////////////////////////////////////////////////
  810. // Functions for converting an item to a TreeNode
  811. getLabel: function(/*dojo.data.Item*/ item){
  812. // summary:
  813. // Overridable function to get the label for a tree node (given the item)
  814. // tags:
  815. // extension
  816. return this.model.getLabel(item); // String
  817. },
  818. getIconClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  819. // summary:
  820. // Overridable function to return CSS class name to display icon
  821. // tags:
  822. // extension
  823. return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
  824. },
  825. getLabelClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  826. // summary:
  827. // Overridable function to return CSS class name to display label
  828. // tags:
  829. // extension
  830. },
  831. getRowClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  832. // summary:
  833. // Overridable function to return CSS class name to display row
  834. // tags:
  835. // extension
  836. },
  837. getIconStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  838. // summary:
  839. // Overridable function to return CSS styles to display icon
  840. // returns:
  841. // Object suitable for input to dojo.style() like {backgroundImage: "url(...)"}
  842. // tags:
  843. // extension
  844. },
  845. getLabelStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  846. // summary:
  847. // Overridable function to return CSS styles to display label
  848. // returns:
  849. // Object suitable for input to dojo.style() like {color: "red", background: "green"}
  850. // tags:
  851. // extension
  852. },
  853. getRowStyle: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
  854. // summary:
  855. // Overridable function to return CSS styles to display row
  856. // returns:
  857. // Object suitable for input to dojo.style() like {background-color: "#bbb"}
  858. // tags:
  859. // extension
  860. },
  861. getTooltip: function(/*dojo.data.Item*/ item){
  862. // summary:
  863. // Overridable function to get the tooltip for a tree node (given the item)
  864. // tags:
  865. // extension
  866. return ""; // String
  867. },
  868. /////////// Keyboard and Mouse handlers ////////////////////
  869. _onKeyPress: function(/*Event*/ e){
  870. // summary:
  871. // Translates keypress events into commands for the controller
  872. if(e.altKey){ return; }
  873. var dk = dojo.keys;
  874. var treeNode = dijit.getEnclosingWidget(e.target);
  875. if(!treeNode){ return; }
  876. var key = e.charOrCode;
  877. if(typeof key == "string" && key != " "){ // handle printables (letter navigation)
  878. // Check for key navigation.
  879. if(!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey){
  880. this._onLetterKeyNav( { node: treeNode, key: key.toLowerCase() } );
  881. dojo.stopEvent(e);
  882. }
  883. }else{ // handle non-printables (arrow keys)
  884. // clear record of recent printables (being saved for multi-char letter navigation),
  885. // because "a", down-arrow, "b" shouldn't search for "ab"
  886. if(this._curSearch){
  887. clearTimeout(this._curSearch.timer);
  888. delete this._curSearch;
  889. }
  890. var map = this._keyHandlerMap;
  891. if(!map){
  892. // setup table mapping keys to events
  893. map = {};
  894. map[dk.ENTER]="_onEnterKey";
  895. //On WebKit based browsers, the combination ctrl-enter
  896. //does not get passed through. To allow accessible
  897. //multi-select on those browsers, the space key is
  898. //also used for selection.
  899. map[dk.SPACE]= map[" "] = "_onEnterKey";
  900. map[this.isLeftToRight() ? dk.LEFT_ARROW : dk.RIGHT_ARROW]="_onLeftArrow";
  901. map[this.isLeftToRight() ? dk.RIGHT_ARROW : dk.LEFT_ARROW]="_onRightArrow";
  902. map[dk.UP_ARROW]="_onUpArrow";
  903. map[dk.DOWN_ARROW]="_onDownArrow";
  904. map[dk.HOME]="_onHomeKey";
  905. map[dk.END]="_onEndKey";
  906. this._keyHandlerMap = map;
  907. }
  908. if(this._keyHandlerMap[key]){
  909. this[this._keyHandlerMap[key]]( { node: treeNode, item: treeNode.item, evt: e } );
  910. dojo.stopEvent(e);
  911. }
  912. }
  913. },
  914. _onEnterKey: function(/*Object*/ message){
  915. this._publish("execute", { item: message.item, node: message.node } );
  916. this.dndController.userSelect(message.node, dojo.isCopyKey( message.evt ), message.evt.shiftKey);
  917. this.onClick(message.item, message.node, message.evt);
  918. },
  919. _onDownArrow: function(/*Object*/ message){
  920. // summary:
  921. // down arrow pressed; get next visible node, set focus there
  922. var node = this._getNextNode(message.node);
  923. if(node && node.isTreeNode){
  924. this.focusNode(node);
  925. }
  926. },
  927. _onUpArrow: function(/*Object*/ message){
  928. // summary:
  929. // Up arrow pressed; move to previous visible node
  930. var node = message.node;
  931. // if younger siblings
  932. var previousSibling = node.getPreviousSibling();
  933. if(previousSibling){
  934. node = previousSibling;
  935. // if the previous node is expanded, dive in deep
  936. while(node.isExpandable && node.isExpanded && node.hasChildren()){
  937. // move to the last child
  938. var children = node.getChildren();
  939. node = children[children.length-1];
  940. }
  941. }else{
  942. // if this is the first child, return the parent
  943. // unless the parent is the root of a tree with a hidden root
  944. var parent = node.getParent();
  945. if(!(!this.showRoot && parent === this.rootNode)){
  946. node = parent;
  947. }
  948. }
  949. if(node && node.isTreeNode){
  950. this.focusNode(node);
  951. }
  952. },
  953. _onRightArrow: function(/*Object*/ message){
  954. // summary:
  955. // Right arrow pressed; go to child node
  956. var node = message.node;
  957. // if not expanded, expand, else move to 1st child
  958. if(node.isExpandable && !node.isExpanded){
  959. this._expandNode(node);
  960. }else if(node.hasChildren()){
  961. node = node.getChildren()[0];
  962. if(node && node.isTreeNode){
  963. this.focusNode(node);
  964. }
  965. }
  966. },
  967. _onLeftArrow: function(/*Object*/ message){
  968. // summary:
  969. // Left arrow pressed.
  970. // If not collapsed, collapse, else move to parent.
  971. var node = message.node;
  972. if(node.isExpandable && node.isExpanded){
  973. this._collapseNode(node);
  974. }else{
  975. var parent = node.getParent();
  976. if(parent && parent.isTreeNode && !(!this.showRoot && parent === this.rootNode)){
  977. this.focusNode(parent);
  978. }
  979. }
  980. },
  981. _onHomeKey: function(){
  982. // summary:
  983. // Home key pressed; get first visible node, and set focus there
  984. var node = this._getRootOrFirstNode();
  985. if(node){
  986. this.focusNode(node);
  987. }
  988. },
  989. _onEndKey: function(/*Object*/ message){
  990. // summary:
  991. // End key pressed; go to last visible node.
  992. var node = this.rootNode;
  993. while(node.isExpanded){
  994. var c = node.getChildren();
  995. node = c[c.length - 1];
  996. }
  997. if(node && node.isTreeNode){
  998. this.focusNode(node);
  999. }
  1000. },
  1001. // multiCharSearchDuration: Number
  1002. // If multiple characters are typed where each keystroke happens within
  1003. // multiCharSearchDuration of the previous keystroke,
  1004. // search for nodes matching all the keystrokes.
  1005. //
  1006. // For example, typing "ab" will search for entries starting with
  1007. // "ab" unless the delay between "a" and "b" is greater than multiCharSearchDuration.
  1008. multiCharSearchDuration: 250,
  1009. _onLetterKeyNav: function(message){
  1010. // summary:
  1011. // Called when user presses a prinatable key; search for node starting with recently typed letters.
  1012. // message: Object
  1013. // Like { node: TreeNode, key: 'a' } where key is the key the user pressed.
  1014. // Branch depending on whether this key starts a new search, or modifies an existing search
  1015. var cs = this._curSearch;
  1016. if(cs){
  1017. // We are continuing a search. Ex: user has pressed 'a', and now has pressed
  1018. // 'b', so we want to search for nodes starting w/"ab".
  1019. cs.pattern = cs.pattern + message.key;
  1020. clearTimeout(cs.timer);
  1021. }else{
  1022. // We are starting a new search
  1023. cs = this._curSearch = {
  1024. pattern: message.key,
  1025. startNode: message.node
  1026. };
  1027. }
  1028. // set/reset timer to forget recent keystrokes
  1029. var self = this;
  1030. cs.timer = setTimeout(function(){
  1031. delete self._curSearch;
  1032. }, this.multiCharSearchDuration);
  1033. // Navigate to TreeNode matching keystrokes [entered so far].
  1034. var node = cs.startNode;
  1035. do{
  1036. node = this._getNextNode(node);
  1037. //check for last node, jump to first node if necessary
  1038. if(!node){
  1039. node = this._getRootOrFirstNode();
  1040. }
  1041. }while(node !== cs.startNode && (node.label.toLowerCase().substr(0, cs.pattern.length) != cs.pattern));
  1042. if(node && node.isTreeNode){
  1043. // no need to set focus if back where we started
  1044. if(node !== cs.startNode){
  1045. this.focusNode(node);
  1046. }
  1047. }
  1048. },
  1049. isExpandoNode: function(node, widget){
  1050. // summary:
  1051. // check whether a dom node is the expandoNode for a particular TreeNode widget
  1052. return dojo.isDescendant(node, widget.expandoNode);
  1053. },
  1054. _onClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){
  1055. // summary:
  1056. // Translates click events into commands for the controller to process
  1057. var domElement = e.target,
  1058. isExpandoClick = this.isExpandoNode(domElement, nodeWidget);
  1059. if( (this.openOnClick && nodeWidget.isExpandable) || isExpandoClick ){
  1060. // expando node was clicked, or label of a folder node was clicked; open it
  1061. if(nodeWidget.isExpandable){
  1062. this._onExpandoClick({node:nodeWidget});
  1063. }
  1064. }else{
  1065. this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } );
  1066. this.onClick(nodeWidget.item, nodeWidget, e);
  1067. this.focusNode(nodeWidget);
  1068. }
  1069. dojo.stopEvent(e);
  1070. },
  1071. _onDblClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){
  1072. // summary:
  1073. // Translates double-click events into commands for the controller to process
  1074. var domElement = e.target,
  1075. isExpandoClick = (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText);
  1076. if( (this.openOnDblClick && nodeWidget.isExpandable) ||isExpandoClick ){
  1077. // expando node was clicked, or label of a folder node was clicked; open it
  1078. if(nodeWidget.isExpandable){
  1079. this._onExpandoClick({node:nodeWidget});
  1080. }
  1081. }else{
  1082. this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } );
  1083. this.onDblClick(nodeWidget.item, nodeWidget, e);
  1084. this.focusNode(nodeWidget);
  1085. }
  1086. dojo.stopEvent(e);
  1087. },
  1088. _onExpandoClick: function(/*Object*/ message){
  1089. // summary:
  1090. // User clicked the +/- icon; expand or collapse my children.
  1091. var node = message.node;
  1092. // If we are collapsing, we might be hiding the currently focused node.
  1093. // Also, clicking the expando node might have erased focus from the current node.
  1094. // For simplicity's sake just focus on the node with the expando.
  1095. this.focusNode(node);
  1096. if(node.isExpanded){
  1097. this._collapseNode(node);
  1098. }else{
  1099. this._expandNode(node);
  1100. }
  1101. },
  1102. onClick: function(/* dojo.data */ item, /*TreeNode*/ node, /*Event*/ evt){
  1103. // summary:
  1104. // Callback when a tree node is clicked
  1105. // tags:
  1106. // callback
  1107. },
  1108. onDblClick: function(/* dojo.data */ item, /*TreeNode*/ node, /*Event*/ evt){
  1109. // summary:
  1110. // Callback when a tree node is double-clicked
  1111. // tags:
  1112. // callback
  1113. },
  1114. onOpen: function(/* dojo.data */ item, /*TreeNode*/ node){
  1115. // summary:
  1116. // Callback when a node is opened
  1117. // tags:
  1118. // callback
  1119. },
  1120. onClose: function(/* dojo.data */ item, /*TreeNode*/ node){
  1121. // summary:
  1122. // Callback when a node is closed
  1123. // tags:
  1124. // callback
  1125. },
  1126. _getNextNode: function(node){
  1127. // summary:
  1128. // Get next visible node
  1129. if(node.isExpandable && node.isExpanded && node.hasChildren()){
  1130. // if this is an expanded node, get the first child
  1131. return node.getChildren()[0]; // _TreeNode
  1132. }else{
  1133. // find a parent node with a sibling
  1134. while(node && node.isTreeNode){
  1135. var returnNode = node.getNextSibling();
  1136. if(returnNode){
  1137. return returnNode; // _TreeNode
  1138. }
  1139. node = node.getParent();
  1140. }
  1141. return null;
  1142. }
  1143. },
  1144. _getRootOrFirstNode: function(){
  1145. // summary:
  1146. // Get first visible node
  1147. return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0];
  1148. },
  1149. _collapseNode: function(/*_TreeNode*/ node){
  1150. // summary:
  1151. // Called when the user has requested to collapse the node
  1152. if(node._expandNodeDeferred){
  1153. delete node._expandNodeDeferred;
  1154. }
  1155. if(node.isExpandable){
  1156. if(node.state == "LOADING"){
  1157. // ignore clicks while we are in the process of loading data
  1158. return;
  1159. }
  1160. node.collapse();
  1161. this.onClose(node.item, node);
  1162. if(node.item){
  1163. this._state(node.item,false);
  1164. this._saveState();
  1165. }
  1166. }
  1167. },
  1168. _expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){
  1169. // summary:
  1170. // Called when the user has requested to expand the node
  1171. // recursive:
  1172. // Internal flag used when _expandNode() calls itself, don't set.
  1173. // returns:
  1174. // Deferred that fires when the node is loaded and opened and (if persist=true) all it's descendants
  1175. // that were previously opened too
  1176. if(node._expandNodeDeferred && !recursive){
  1177. // there's already an expand in progress (or completed), so just return
  1178. return node._expandNodeDeferred; // dojo.Deferred
  1179. }
  1180. var model = this.model,
  1181. item = node.item,
  1182. _this = this;
  1183. switch(node.state){
  1184. case "UNCHECKED":
  1185. // need to load all the children, and then expand
  1186. node.markProcessing();
  1187. // Setup deferred to signal when the load and expand are finished.
  1188. // Save that deferred in this._expandDeferred as a flag that operation is in progress.
  1189. var def = (node._expandNodeDeferred = new dojo.Deferred());
  1190. // Get the children
  1191. model.getChildren(
  1192. item,
  1193. function(items){
  1194. node.unmarkProcessing();
  1195. // Display the children and also start expanding any children that were previously expanded
  1196. // (if this.persist == true). The returned Deferred will fire when those expansions finish.
  1197. var scid = node.setChildItems(items);
  1198. // Call _expandNode() again but this time it will just to do the animation (default branch).
  1199. // The returned Deferred will fire when the animation completes.
  1200. // TODO: seems like I can avoid recursion and just use a deferred to sequence the events?
  1201. var ed = _this._expandNode(node, true);
  1202. // After the above two tasks (setChildItems() and recursive _expandNode()) finish,
  1203. // signal that I am done.
  1204. scid.addCallback(function(){
  1205. ed.addCallback(function(){
  1206. def.callback();
  1207. })
  1208. });
  1209. },
  1210. function(err){
  1211. console.error(_this, ": error loading root children: ", err);
  1212. }
  1213. );
  1214. break;
  1215. default: // "LOADED"
  1216. // data is already loaded; just expand node
  1217. def = (node._expandNodeDeferred = node.expand());
  1218. this.onOpen(node.item, node);
  1219. if(item){
  1220. this._state(item, true);
  1221. this._saveState();
  1222. }
  1223. }
  1224. return def; // dojo.Deferred
  1225. },
  1226. ////////////////// Miscellaneous functions ////////////////
  1227. focusNode: function(/* _tree.Node */ node){
  1228. // summary:
  1229. // Focus on the specified node (which must be visible)
  1230. // tags:
  1231. // protected
  1232. // set focus so that the label will be voiced using screen readers
  1233. dijit.focus(node.labelNode);
  1234. },
  1235. _onNodeFocus: function(/*dijit._Widget*/ node){
  1236. // summary:
  1237. // Called when a TreeNode gets focus, either by user clicking
  1238. // it, or programatically by arrow key handling code.
  1239. // description:
  1240. // It marks that the current node is the selected one, and the previously
  1241. // selected node no longer is.
  1242. if(node && node != this.lastFocused){
  1243. if(this.lastFocused && !this.lastFocused._destroyed){
  1244. // mark that the previously focsable node is no longer focusable
  1245. this.lastFocused.setFocusable(false);
  1246. }
  1247. // mark that the new node is the currently selected one
  1248. node.setFocusable(true);
  1249. this.lastFocused = node;
  1250. }
  1251. },
  1252. _onNodeMouseEnter: function(/*dijit._Widget*/ node){
  1253. // summary:
  1254. // Called when mouse is over a node (onmouseenter event),
  1255. // this is monitored by the DND code
  1256. },
  1257. _onNodeMouseLeave: function(/*dijit._Widget*/ node){
  1258. // summary:
  1259. // Called when mouse leaves a node (onmouseleave event),
  1260. // this is monitored by the DND code
  1261. },
  1262. //////////////// Events from the model //////////////////////////
  1263. _onItemChange: function(/*Item*/ item){
  1264. // summary:
  1265. // Processes notification of a change to an item's scalar values like label
  1266. var model = this.model,
  1267. identity = model.getIdentity(item),
  1268. nodes = this._itemNodesMap[identity];
  1269. if(nodes){
  1270. var label = this.getLabel(item),
  1271. tooltip = this.getTooltip(item);
  1272. dojo.forEach(nodes, function(node){
  1273. node.set({
  1274. item: item, // theoretically could be new JS Object representing same item
  1275. label: label,
  1276. tooltip: tooltip
  1277. });
  1278. node._updateItemClasses(item);
  1279. });
  1280. }
  1281. },
  1282. _onItemChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
  1283. // summary:
  1284. // Processes notification of a change to an item's children
  1285. var model = this.model,
  1286. identity = model.getIdentity(parent),
  1287. parentNodes = this._itemNodesMap[identity];
  1288. if(parentNodes){
  1289. dojo.forEach(parentNodes,function(parentNode){
  1290. parentNode.setChildItems(newChildrenList);
  1291. });
  1292. }
  1293. },
  1294. _onItemDelete: function(/*Item*/ item){
  1295. // summary:
  1296. // Processes notification of a deletion of an item
  1297. var model = this.model,
  1298. identity = model.getIdentity(item),
  1299. nodes = this._itemNodesMap[identity];
  1300. if(nodes){
  1301. dojo.forEach(nodes,function(node){
  1302. // Remove node from set of selected nodes (if it's selected)
  1303. this.dndController.removeTreeNode(node);
  1304. var parent = node.getParent();
  1305. if(parent){
  1306. // if node has not already been orphaned from a _onSetItem(parent, "children", ..) call...
  1307. parent.removeChild(node);
  1308. }
  1309. node.destroyRecursive();
  1310. }, this);
  1311. delete this._itemNodesMap[identity];
  1312. }
  1313. },
  1314. /////////////// Miscellaneous funcs
  1315. _initState: function(){
  1316. // summary:
  1317. // Load in which nodes should be opened automatically
  1318. if(this.persist){
  1319. var cookie = dojo.cookie(this.cookieName);
  1320. this._openedItemIds = {};
  1321. if(cookie){
  1322. dojo.forEach(cookie.split(','), function(item){
  1323. this._openedItemIds[item] = true;
  1324. }, this);
  1325. }
  1326. }
  1327. },
  1328. _state: function(item,expanded){
  1329. // summary:
  1330. // Query or set expanded state for an item,
  1331. if(!this.persist){
  1332. return false;
  1333. }
  1334. var id=this.model.getIdentity(item);
  1335. if(arguments.length === 1){
  1336. return this._openedItemIds[id];
  1337. }
  1338. if(expanded){
  1339. this._openedItemIds[id] = true;
  1340. }else{
  1341. delete this._openedItemIds[id];
  1342. }
  1343. },
  1344. _saveState: function(){
  1345. // summary:
  1346. // Create and save a cookie with the currently expanded nodes identifiers
  1347. if(!this.persist){
  1348. return;
  1349. }
  1350. var ary = [];
  1351. for(var id in this._openedItemIds){
  1352. ary.push(id);
  1353. }
  1354. dojo.cookie(this.cookieName, ary.join(","), {expires:365});
  1355. },
  1356. destroy: function(){
  1357. if(this._curSearch){
  1358. clearTimeout(this._curSearch.timer);
  1359. delete this._curSearch;
  1360. }
  1361. if(this.rootNode){
  1362. this.rootNode.destroyRecursive();
  1363. }
  1364. if(this.dndController && !dojo.isString(this.dndController)){
  1365. this.dndController.destroy();
  1366. }
  1367. this.rootNode = null;
  1368. this.inherited(arguments);
  1369. },
  1370. destroyRecursive: function(){
  1371. // A tree is treated as a leaf, not as a node with children (like a grid),
  1372. // but defining destroyRecursive for back-compat.
  1373. this.destroy();
  1374. },
  1375. resize: function(changeSize){
  1376. if(changeSize){
  1377. dojo.marginBox(this.domNode, changeSize);
  1378. }
  1379. // The only JS sizing involved w/tree is the indentation, which is specified
  1380. // in CSS and read in through this dummy indentDetector node (tree must be
  1381. // visible and attached to the DOM to read this)
  1382. this._nodePixelIndent = dojo._getMarginSize(this.tree.indentDetector).w;
  1383. if(this.tree.rootNode){
  1384. // If tree has already loaded, then reset indent for all the nodes
  1385. this.tree.rootNode.set('indent', this.showRoot ? 0 : -1);
  1386. }
  1387. },
  1388. _createTreeNode: function(/*Object*/ args){
  1389. // summary:
  1390. // creates a TreeNode
  1391. // description:
  1392. // Developers can override this method to define their own TreeNode class;
  1393. // However it will probably be removed in a future release in favor of a way
  1394. // of just specifying a widget for the label, rather than one that contains
  1395. // the children too.
  1396. return new dijit._TreeNode(args);
  1397. }
  1398. });
  1399. // For back-compat. TODO: remove in 2.0
  1400. }