Tree.js.uncompressed.js 59 KB

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