Tree.js 52 KB

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