jquery.fancytree.js 112 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670
  1. /*!
  2. * jquery.fancytree.js
  3. * Dynamic tree view control, with support for lazy loading of branches.
  4. * https://github.com/mar10/fancytree/
  5. *
  6. * Copyright (c) 2008-2013, Martin Wendt (http://wwWendt.de)
  7. * Released under the MIT license
  8. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  9. *
  10. * @version DEVELOPMENT
  11. * @date DEVELOPMENT
  12. */
  13. // Start of local namespace
  14. ;(function($, window, document, undefined) {
  15. // relax some jslint checks:
  16. /*globals alert */
  17. "use strict";
  18. // prevent duplicate loading
  19. if ( $.ui.fancytree && $.ui.fancytree.version ) {
  20. $.ui.fancytree.warn("Fancytree: ignored duplicate include");
  21. return;
  22. }
  23. /* *****************************************************************************
  24. * Private functions and variables
  25. */
  26. function _raiseNotImplemented(msg){
  27. msg = msg || "";
  28. $.error("Not implemented: " + msg);
  29. }
  30. function _assert(cond, msg){
  31. // TODO: see qunit.js extractStacktrace()
  32. msg = ": " + msg || "";
  33. if(!cond){
  34. $.error("Assertion failed" + msg);
  35. }
  36. }
  37. function consoleApply(method, args){
  38. var i, s,
  39. fn = window.console ? window.console[method] : null;
  40. if(fn){
  41. if(fn.apply){
  42. fn.apply(window.console, args);
  43. }else{
  44. // IE?
  45. s = "";
  46. for( i=0; i<args.length; i++){
  47. s += args[i];
  48. }
  49. fn(s);
  50. }
  51. }
  52. }
  53. /** Return true if dotted version string is equal or higher than requested version.
  54. *
  55. * See http://jsfiddle.net/mar10/FjSAN/
  56. */
  57. function isVersionAtLeast(dottedVersion, major, minor, patch){
  58. var i, v, t,
  59. verParts = $.map($.trim(dottedVersion).split("."), function(e){ return parseInt(e, 10); }),
  60. testParts = $.map(Array.prototype.slice.call(arguments, 1), function(e){ return parseInt(e, 10); });
  61. for( i = 0; i < testParts.length; i++ ){
  62. v = verParts[i] || 0;
  63. t = testParts[i] || 0;
  64. if( v !== t ){
  65. return ( v > t );
  66. }
  67. }
  68. return true;
  69. }
  70. /** Return a wrapper that calls sub.fn() and exposes base.fn() as _super(). */
  71. function _makeVirtualFunction(methodName, base, sub){
  72. var _super = base[methodName],
  73. func = sub[methodName];
  74. // if(rexTestSuper && !rexTestSuper.test(func)){
  75. // // sub.methodName() doesn't call _super(), so no wrapper required
  76. // return func;
  77. // }
  78. return function(){
  79. try{
  80. base._super = function(){
  81. return _super.apply(base, arguments);
  82. };
  83. // sub._base = base;
  84. return func.apply(base, arguments);
  85. }finally{
  86. base._super = null;
  87. }
  88. };
  89. }
  90. /**
  91. * Subclass `base` by creating proxy functions
  92. */
  93. function _subclassObject(tree, base, extension, extName){
  94. // $.ui.fancytree.debug("_subclassObject", base, extension, extName);
  95. for(var attrName in extension){
  96. if(typeof extension[attrName] === "function"){
  97. if(typeof tree[attrName] === "function"){
  98. // override existing method
  99. tree[attrName] = _makeVirtualFunction(attrName, tree, extension);
  100. }else if(attrName.charAt(0) === "_"){
  101. // Create private methods in tree.EXTENSION namespace
  102. tree[extName][attrName] = $.proxy(extension[attrName], tree);
  103. }else{
  104. $.error("Could not override tree." + attrName + ". Use prefix '_' to create tree." + extName + "._" + attrName);
  105. }
  106. }else{
  107. // Create member variables in tree.EXTENSION namespace
  108. if(attrName !== "options"){
  109. tree[extName][attrName] = base[attrName];
  110. }
  111. }
  112. }
  113. }
  114. function _getResolvedPromise(context, argArray){
  115. if(context === undefined){
  116. return $.Deferred(function(){this.resolve();}).promise();
  117. }else{
  118. return $.Deferred(function(){this.resolveWith(context, argArray);}).promise();
  119. }
  120. }
  121. function _getRejectedPromise(context, argArray){
  122. if(context === undefined){
  123. return $.Deferred(function(){this.reject();}).promise();
  124. }else{
  125. return $.Deferred(function(){this.rejectWith(context, argArray);}).promise();
  126. }
  127. }
  128. function _makeResolveFunc(deferred, context){
  129. return function(){
  130. deferred.resolveWith(context);
  131. };
  132. }
  133. // TODO: use currying
  134. function _makeNodeTitleMatcher(s){
  135. s = s.toLowerCase();
  136. return function(node){
  137. return node.title.toLowerCase().indexOf(s) >= 0;
  138. };
  139. }
  140. var i,
  141. FT = null, // initialized below
  142. //Boolean attributes that can be set with equivalent class names in the LI tags
  143. CLASS_ATTRS = "active expanded focus folder lazy selected unselectable".split(" "),
  144. CLASS_ATTR_MAP = {},
  145. // Top-level Fancytree node attributes, that can be set by dict
  146. NODE_ATTRS = "expanded extraClasses folder hideCheckbox key lazy selected title tooltip unselectable".split(" "),
  147. NODE_ATTR_MAP = {},
  148. // Attribute names that should NOT be added to node.data
  149. NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true};
  150. for(i=0; i<CLASS_ATTRS.length; i++){ CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; }
  151. for(i=0; i<NODE_ATTRS.length; i++){ NODE_ATTR_MAP[NODE_ATTRS[i]] = true; }
  152. /* *****************************************************************************
  153. * FancytreeNode
  154. */
  155. /**
  156. * Creates a new node
  157. * @class Represents the hierarchical data model and operations.
  158. * @name FancytreeNode
  159. * @constructor
  160. * @param {FancytreeNode} parent
  161. * @param {NodeData} data
  162. *
  163. * @property {Fancytree} tree
  164. * @property {FancytreeNode} parent Parent node
  165. * @property {String} key
  166. * @property {String} title
  167. * @property {object} data Contains all extra data that was passed on node creation
  168. * @property {FancytreeNode[] | null | undefined} children list of child nodes
  169. * @property {Boolean} isStatusNode
  170. * @property {Boolean} expanded
  171. * @property {Boolean} folder
  172. * @property {Boolean} href
  173. * @property {String} extraClasses
  174. * @property {Boolean} lazy
  175. * @property {Boolean} nolink OBSOLETE
  176. * @property {Boolean} selected
  177. * @property {String} target
  178. * @property {String} tooltip
  179. */
  180. function FancytreeNode(parent, obj){
  181. var i, l, name, cl;
  182. this.parent = parent;
  183. this.tree = parent.tree;
  184. this.ul = null;
  185. this.li = null; // <li id='key' ftnode=this> tag
  186. this.isStatusNode = false;
  187. this.data = {};
  188. // TODO: merge this code with node.toDict()
  189. // copy attributes from obj object
  190. for(i=0, l=NODE_ATTRS.length; i<l; i++){
  191. name = NODE_ATTRS[i];
  192. this[name] = obj[name];
  193. }
  194. // node.data += obj.data
  195. if(obj.data){
  196. $.extend(this.data, obj.data);
  197. }
  198. // copy all other attributes to this.data.NAME
  199. for(name in obj){
  200. if(!NODE_ATTR_MAP[name] && !$.isFunction(obj[name]) && !NONE_NODE_DATA_MAP[name]){
  201. // node.data.NAME = obj.NAME
  202. this.data[name] = obj[name];
  203. }
  204. }
  205. // Fix missing key
  206. if( this.key == null ){ // test for null OR undefined
  207. this.key = "_" + (FT._nextNodeKey++);
  208. }
  209. // Fix tree.activeNode
  210. // TODO: not elegant: we use obj.active as marker to set tree.activeNode
  211. // when loading from a dictionary.
  212. if(obj.active){
  213. _assert(this.tree.activeNode === null, "only one active node allowed");
  214. this.tree.activeNode = this;
  215. }
  216. // TODO: handle obj.focus = true
  217. // Create child nodes
  218. this.children = null;
  219. cl = obj.children;
  220. if(cl && cl.length){
  221. this._setChildren(cl);
  222. }
  223. }
  224. FancytreeNode.prototype = /**@lends FancytreeNode*/{
  225. /* Return the direct child FancytreeNode with a given key, index. */
  226. _findDirectChild: function(ptr){
  227. var i, l,
  228. cl = this.children;
  229. if(cl){
  230. if(typeof ptr === "string"){
  231. for(i=0, l=cl.length; i<l; i++){
  232. if(cl[i].key === ptr){
  233. return cl[i];
  234. }
  235. }
  236. }else if(typeof ptr === "number"){
  237. return this.children[ptr];
  238. }else if(ptr.parent === this){
  239. return ptr;
  240. }
  241. }
  242. return null;
  243. },
  244. // TODO: activate()
  245. // TODO: activateSilently()
  246. /* Internal helper called in recursive addChildren sequence.*/
  247. _setChildren: function(children){
  248. _assert(children && (!this.children || this.children.length === 0), "only init supported");
  249. this.children = [];
  250. for(var i=0, l=children.length; i<l; i++){
  251. this.children.push(new FancytreeNode(this, children[i]));
  252. }
  253. },
  254. /**
  255. * Append (or insert) a list of child nodes.
  256. *
  257. * @param {NodeData[]} children array of child node definitions (also single child accepted)
  258. * @param {FancytreeNode | String | Integer} [insertBefore] child node (or key or index of such).
  259. * If omitted, the new children are appended.
  260. * @returns {FancytreeNode} first child added
  261. *
  262. * @see applyPatch to modify existing child nodes.
  263. * @see FanctreeNode.applyPatch to modify existing child nodes.
  264. * @see FanctreeNode#applyPatch to modify existing child nodes.
  265. * @see applyPatch
  266. * @see FanctreeNode.applyPatch
  267. * @see FanctreeNode#applyPatch
  268. */
  269. addChildren: function(children, insertBefore){
  270. var i, l, pos,
  271. firstNode = null,
  272. nodeList = [];
  273. if($.isPlainObject(children) ){
  274. children = [children];
  275. }
  276. if(!this.children){
  277. this.children = [];
  278. }
  279. for(i=0, l=children.length; i<l; i++){
  280. nodeList.push(new FancytreeNode(this, children[i]));
  281. }
  282. firstNode = nodeList[0];
  283. if(insertBefore == null){
  284. this.children = this.children.concat(nodeList);
  285. }else{
  286. insertBefore = this._findDirectChild(insertBefore);
  287. pos = $.inArray(insertBefore, this.children);
  288. _assert(pos >= 0, "insertBefore must be an existing child");
  289. // insert nodeList after children[pos]
  290. this.children.splice.apply(this.children, [pos, 0].concat(nodeList));
  291. }
  292. if(!this.parent || this.parent.ul){
  293. // render if the parent was rendered (or this is a root node)
  294. this.render();
  295. }
  296. if( this.tree.options.selectMode === 3 ){
  297. this.fixSelection3FromEndNodes();
  298. }
  299. return firstNode;
  300. },
  301. /**
  302. * Append or prepend a node, or append a child node.
  303. *
  304. * @param {NodeData} node node definition
  305. * @param {String} [mode] 'before', 'after', or 'child'
  306. * @returns {FancytreeNode} new node
  307. */
  308. addNode: function(node, mode){
  309. if(mode === undefined || mode === "over"){
  310. mode = "child";
  311. }
  312. switch(mode){
  313. case "after":
  314. return this.getParent().addChildren(node, this.getNextSibling());
  315. case "before":
  316. return this.getParent().addChildren(node, this);
  317. case "child":
  318. case "over":
  319. return this.addChildren(node);
  320. }
  321. _assert(false, "Invalid mode: " + mode);
  322. },
  323. /**
  324. *
  325. * @param {NodePatch} patch
  326. * @returns {$.Promise}
  327. * @see {@link applyPatch} to modify existing child nodes.
  328. * @see FancytreeNode#addChildren
  329. */
  330. applyPatch: function(patch) {
  331. // patch [key, null] means 'remove'
  332. if(patch === null){
  333. this.remove();
  334. return _getResolvedPromise(this);
  335. }
  336. // TODO: make sure that root node is not collapsed or modified
  337. // copy (most) attributes to node.ATTR or node.data.ATTR
  338. var name, promise, v,
  339. IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global
  340. for(name in patch){
  341. v = patch[name];
  342. if( !IGNORE_MAP[name] && !$.isFunction(v)){
  343. if(NODE_ATTR_MAP[name]){
  344. this[name] = v;
  345. }else{
  346. this.data[name] = v;
  347. }
  348. }
  349. }
  350. // Remove and/or create children
  351. if(patch.hasOwnProperty("children")){
  352. this.removeChildren();
  353. if(patch.children){ // only if not null and not empty list
  354. // TODO: addChildren instead?
  355. this._setChildren(patch.children);
  356. }
  357. // TODO: how can we APPEND or INSERT child nodes?
  358. }
  359. if(this.isVisible()){
  360. this.renderTitle();
  361. this.renderStatus();
  362. }
  363. // Expand collapse (final step, since this may be async)
  364. if(patch.hasOwnProperty("expanded")){
  365. promise = this.setExpanded(patch.expanded);
  366. }else{
  367. promise = _getResolvedPromise(this);
  368. }
  369. return promise;
  370. },
  371. /**
  372. * @returns {$.Promise}
  373. */
  374. collapseSiblings: function() {
  375. return this.tree._callHook("nodeCollapseSiblings", this);
  376. },
  377. /** Copy this node as sibling or child of `node`.
  378. *
  379. * @param {FancytreeNode} node source node
  380. * @param {String} mode 'before' | 'after' | 'child'
  381. * @param {Function} [map] callback function(NodeData) that could modify the new node
  382. * @returns {FancytreeNode} new
  383. */
  384. copyTo: function(node, mode, map) {
  385. return node.addNode(this.toDict(true, map), mode);
  386. },
  387. /** Count direct and indirect children.
  388. *
  389. * @param {Boolean} [deep=true] pass 'false' to only count direct children
  390. * @returns {int} number of child nodes
  391. */
  392. countChildren: function(deep) {
  393. var cl = this.children, i, l, n;
  394. if( !cl ){
  395. return 0;
  396. }
  397. n = cl.length;
  398. if(deep !== false){
  399. for(i=0, l=n; i<l; i++){
  400. n += cl[i].countChildren();
  401. }
  402. }
  403. return n;
  404. },
  405. // TODO: deactivate()
  406. /** Write to browser console if debugLevel >= 2 (prepending node info)
  407. *
  408. * @param {*} msg string or object or array of such
  409. */
  410. debug: function(msg){
  411. if( this.tree.options.debugLevel >= 2 ) {
  412. Array.prototype.unshift.call(arguments, this.toString());
  413. consoleApply("debug", arguments);
  414. }
  415. },
  416. /** Remove all children of a lazy node and collapse.*/
  417. discard: function(){
  418. if(this.lazy && $.isArray(this.children)){
  419. this.removeChildren();
  420. return this.setExpanded(false);
  421. }
  422. },
  423. // TODO: expand(flag)
  424. /**Find all nodes that contain `match` in the title.
  425. *
  426. * @param {String | function(node)} match string to search for, of a function that
  427. * returns `true` if a node is matched.
  428. * @returns {FancytreeNode[]} array of nodes (may be empty)
  429. * @see FancytreeNode#findAll
  430. */
  431. findAll: function(match) {
  432. match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
  433. var res = [];
  434. this.visit(function(n){
  435. if(match(n)){
  436. res.push(n);
  437. }
  438. });
  439. return res;
  440. },
  441. /**Find first node that contains `match` in the title (not including self).
  442. *
  443. * @param {String | function(node)} match string to search for, of a function that
  444. * returns `true` if a node is matched.
  445. * @returns {FancytreeNode} matching node or null
  446. * @example
  447. * <b>fat</b> text
  448. */
  449. findFirst: function(match) {
  450. match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
  451. var res = null;
  452. this.visit(function(n){
  453. if(match(n)){
  454. res = n;
  455. return false;
  456. }
  457. });
  458. return res;
  459. },
  460. /* Apply selection state (internal use only) */
  461. _changeSelectStatusAttrs: function (state) {
  462. var changed = false;
  463. switch(state){
  464. case false:
  465. changed = ( this.selected || this.partsel );
  466. this.selected = false;
  467. this.partsel = false;
  468. break;
  469. case true:
  470. changed = ( !this.selected || !this.partsel );
  471. this.selected = true;
  472. this.partsel = true;
  473. break;
  474. case undefined:
  475. changed = ( this.selected || !this.partsel );
  476. this.selected = false;
  477. this.partsel = true;
  478. break;
  479. default:
  480. _assert(false, "invalid state: " + state);
  481. }
  482. this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed);
  483. if( changed ){
  484. this.renderStatus();
  485. }
  486. return changed;
  487. },
  488. /**
  489. * Fix selection status, after this node was (de)selected in multi-hier mode.
  490. * This includes (de)selecting all children.
  491. */
  492. fixSelection3AfterClick: function() {
  493. var flag = this.isSelected();
  494. // this.debug("fixSelection3AfterClick()");
  495. this.visit(function(node){
  496. node._changeSelectStatusAttrs(flag);
  497. });
  498. this.fixSelection3FromEndNodes();
  499. },
  500. /**
  501. * Fix selection status for multi-hier mode.
  502. * Only end-nodes are considered to update the descendants branch and parents.
  503. * Should be called after this node has loaded new children or after
  504. * children have been modified using the API.
  505. */
  506. fixSelection3FromEndNodes: function() {
  507. // this.debug("fixSelection3FromEndNodes()");
  508. _assert(this.tree.options.selectMode === 3, "expected selectMode 3");
  509. // Visit all end nodes and adjust their parent's `selected` and `partsel`
  510. // attributes. Return selection state true, false, or undefined.
  511. function _walk(node){
  512. var i, l, child, s, state, allSelected,someSelected,
  513. children = node.children;
  514. if( children ){
  515. // check all children recursively
  516. allSelected = true;
  517. someSelected = false;
  518. for( i=0, l=children.length; i<l; i++ ){
  519. child = children[i];
  520. // the selection state of a node is not relevant; we need the end-nodes
  521. s = _walk(child);
  522. if( s !== false ) {
  523. someSelected = true;
  524. }
  525. if( s !== true ) {
  526. allSelected = false;
  527. }
  528. }
  529. state = allSelected ? true : (someSelected ? undefined : false);
  530. }else{
  531. // This is an end-node: simply report the status
  532. // state = ( node.unselectable ) ? undefined : !!node.selected;
  533. state = !!node.selected;
  534. }
  535. node._changeSelectStatusAttrs(state);
  536. return state;
  537. }
  538. _walk(this);
  539. // Update parent's state
  540. this.visitParents(function(node){
  541. var i, l, child, state,
  542. children = node.children,
  543. allSelected = true,
  544. someSelected = false;
  545. for( i=0, l=children.length; i<l; i++ ){
  546. child = children[i];
  547. // When fixing the parents, we trust the sibling status (i.e.
  548. // we don't recurse)
  549. if( child.selected || child.partsel ) {
  550. someSelected = true;
  551. }
  552. if( !child.unselectable && !child.selected ) {
  553. allSelected = false;
  554. }
  555. }
  556. state = allSelected ? true : (someSelected ? undefined : false);
  557. node._changeSelectStatusAttrs(state);
  558. });
  559. },
  560. // TODO: focus()
  561. /**
  562. * Update node data. If dict contains 'children', then also replace
  563. * the hole sub tree.
  564. * @param {NodeData} dict
  565. *
  566. * @see FancytreeNode#addChildren
  567. * @see FancytreeNode#applyPatch
  568. */
  569. fromDict: function(dict) {
  570. // copy all other attributes to this.data.xxx
  571. for(var name in dict){
  572. if(NODE_ATTR_MAP[name]){
  573. // node.NAME = dict.NAME
  574. this[name] = dict[name];
  575. }else if(name === "data"){
  576. // node.data += dict.data
  577. $.extend(this.data, dict.data);
  578. }else if(!$.isFunction(dict[name]) && !NONE_NODE_DATA_MAP[name]){
  579. // node.data.NAME = dict.NAME
  580. this.data[name] = dict[name];
  581. }
  582. }
  583. if(dict.children){
  584. // recursively set children and render
  585. this.removeChildren();
  586. this.addChildren(dict.children);
  587. }else{
  588. this.renderTitle();
  589. }
  590. /*
  591. var children = dict.children;
  592. if(children === undefined){
  593. this.data = $.extend(this.data, dict);
  594. this.render();
  595. return;
  596. }
  597. dict = $.extend({}, dict);
  598. dict.children = undefined;
  599. this.data = $.extend(this.data, dict);
  600. this.removeChildren();
  601. this.addChild(children);
  602. */
  603. },
  604. /** @returns {FancytreeNode[] | undefined} list of child nodes (undefined for unexpanded lazy nodes).*/
  605. getChildren: function() {
  606. if(this.hasChildren() === undefined){ // TODO: only required for lazy nodes?
  607. return undefined; // Lazy node: unloaded, currently loading, or load error
  608. }
  609. return this.children;
  610. },
  611. /** @returns {FancytreeNode | null}*/
  612. getFirstChild: function() {
  613. return this.children ? this.children[0] : null;
  614. },
  615. /** @returns {int} 0-based child index.*/
  616. getIndex: function() {
  617. // return this.parent.children.indexOf(this);
  618. return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7
  619. },
  620. /**@returns {String} hierarchical child index (1-based: '3.2.4').*/
  621. getIndexHier: function(separator) {
  622. separator = separator || ".";
  623. var res = [];
  624. $.each(this.getParentList(false, true), function(i, o){
  625. res.push(o.getIndex() + 1);
  626. });
  627. return res.join(separator);
  628. },
  629. /**
  630. * @param {Boolean} [excludeSelf=false]
  631. * @returns {String} parent keys separated by options.keyPathSeparator
  632. */
  633. getKeyPath: function(excludeSelf) {
  634. var path = [],
  635. sep = this.tree.options.keyPathSeparator;
  636. this.visitParents(function(n){
  637. if(n.parent){
  638. path.unshift(n.key);
  639. }
  640. }, !excludeSelf);
  641. return sep + path.join(sep);
  642. },
  643. /**@returns {FancytreeNode | null} last child of this node.*/
  644. getLastChild: function() {
  645. return this.children ? this.children[this.children.length - 1] : null;
  646. },
  647. /** @returns {int} node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, .... */
  648. getLevel: function() {
  649. var level = 0,
  650. dtn = this.parent;
  651. while( dtn ) {
  652. level++;
  653. dtn = dtn.parent;
  654. }
  655. return level;
  656. },
  657. /** @returns {FancytreeNode | null} */
  658. getNextSibling: function() {
  659. // TODO: use indexOf, if available: (not in IE6)
  660. if( this.parent ){
  661. var i, l,
  662. ac = this.parent.children;
  663. for(i=0, l=ac.length-1; i<l; i++){ // up to length-2, so next(last) = null
  664. if( ac[i] === this ){
  665. return ac[i+1];
  666. }
  667. }
  668. }
  669. return null;
  670. },
  671. /** @returns {FancytreeNode | null} returns null for the system root node*/
  672. getParent: function() {
  673. // TODO: return null for top-level nodes?
  674. return this.parent;
  675. },
  676. /**
  677. * @param {Boolean} [includeRoot=false]
  678. * @param {Boolean} [includeSelf=false]
  679. * @returns {FancytreeNode[]}
  680. */
  681. getParentList: function(includeRoot, includeSelf) {
  682. var l = [],
  683. dtn = includeSelf ? this : this.parent;
  684. while( dtn ) {
  685. if( includeRoot || dtn.parent ){
  686. l.unshift(dtn);
  687. }
  688. dtn = dtn.parent;
  689. }
  690. return l;
  691. },
  692. /** @returns {FancytreeNode | null} */
  693. getPrevSibling: function() {
  694. if( this.parent ){
  695. var i, l,
  696. ac = this.parent.children;
  697. for(i=1, l=ac.length; i<l; i++){ // start with 1, so prev(first) = null
  698. if( ac[i] === this ){
  699. return ac[i-1];
  700. }
  701. }
  702. }
  703. return null;
  704. },
  705. /** @returns {boolean | undefined} Check if node has children (returns undefined, if not sure). */
  706. hasChildren: function() {
  707. if(this.lazy){
  708. if(this.children === null || this.children === undefined){
  709. // Not yet loaded
  710. return undefined;
  711. }else if(this.children.length === 0){
  712. // Loaded, but response was empty
  713. return false;
  714. }else if(this.children.length === 1 && this.children[0].isStatusNode ){
  715. // Currently loading or load error
  716. return undefined;
  717. }
  718. return true;
  719. }
  720. return !!this.children;
  721. },
  722. /**@returns {Boolean} true, if node has keyboard focus*/
  723. hasFocus: function() {
  724. return (this.tree.hasFocus() && this.tree.focusNode === this);
  725. },
  726. /**@returns {Boolean} true, if node is active*/
  727. isActive: function() {
  728. return (this.tree.activeNode === this);
  729. },
  730. /**
  731. * @param {FancytreeNode} otherNode
  732. * @returns {Boolean} true, if node is a direct child of otherNode
  733. */
  734. isChildOf: function(otherNode) {
  735. return (this.parent && this.parent === otherNode);
  736. },
  737. /**
  738. * @param {FancytreeNode} otherNode
  739. * @returns {Boolean} true, if node is a sub node of otherNode
  740. */
  741. isDescendantOf: function(otherNode) {
  742. if(!otherNode || otherNode.tree !== this.tree){
  743. return false;
  744. }
  745. var p = this.parent;
  746. while( p ) {
  747. if( p === otherNode ){
  748. return true;
  749. }
  750. p = p.parent;
  751. }
  752. return false;
  753. },
  754. /** @returns {Boolean} true, if node is expanded*/
  755. isExpanded: function() {
  756. return !!this.expanded;
  757. },
  758. /** @returns {Boolean}*/
  759. isFirstSibling: function() {
  760. var p = this.parent;
  761. return !p || p.children[0] === this;
  762. },
  763. /** @returns {Boolean}*/
  764. isFolder: function() {
  765. return !!this.folder;
  766. },
  767. /** @returns {Boolean}*/
  768. isLastSibling: function() {
  769. var p = this.parent;
  770. return !p || p.children[p.children.length-1] === this;
  771. },
  772. /** @returns {Boolean} true, if node is lazy (even if data was already loaded)*/
  773. isLazy: function() {
  774. return !!this.lazy;
  775. },
  776. /** @returns {Boolean} true, if children are currently beeing loaded*/
  777. isLoading: function() {
  778. _raiseNotImplemented(); // TODO: implement
  779. },
  780. /**@returns {Boolean} true, if node is the (invisible) system root node*/
  781. isRoot: function() {
  782. return (this.tree.rootNode === this);
  783. },
  784. /** @returns {Boolean} true, if node is selected (e.g. has a checkmark set)*/
  785. isSelected: function() {
  786. return !!this.selected;
  787. },
  788. // TODO: use _isStatusNode as class attribute name
  789. // isStatusNode: function() {
  790. // return (this.data.isStatusNode === true);
  791. // },
  792. /** Return true, if all parents are expanded. */
  793. isVisible: function() {
  794. var i, l,
  795. parents = this.getParentList(false, false);
  796. for(i=0, l=parents.length; i<l; i++){
  797. if( ! parents[i].expanded ){ return false; }
  798. }
  799. return true;
  800. },
  801. /** Expand all parents and optionally scroll into visible area as neccessary (async).
  802. *
  803. */
  804. makeVisible: function() {
  805. // TODO: implement scolling (http://www.w3.org/TR/wai-aria-practices/#visualfocus)
  806. // TODO: return $.promise
  807. var i, l,
  808. parents = this.getParentList(false, false);
  809. for(i=0, l=parents.length; i<l; i++){
  810. parents[i].setExpanded(true);
  811. }
  812. },
  813. /** Move this node to targetNode.
  814. * @param {FancytreeNode} targetNode
  815. * @param {String} mode
  816. * 'child': append this node as last child of targetNode.
  817. * This is the default. To be compatble with the D'n'd
  818. * hitMode, we also accept 'over'.
  819. * 'before': add this node as sibling before targetNode.
  820. * 'after': add this node as sibling after targetNode.
  821. * @param [map] optional callback(FancytreeNode) to allow modifcations
  822. */
  823. moveTo: function(targetNode, mode, map) {
  824. if(mode === undefined || mode === "over"){
  825. mode = "child";
  826. }
  827. var pos,
  828. prevParent = this.parent,
  829. targetParent = (mode === "child") ? targetNode : targetNode.parent;
  830. if(this === targetNode){
  831. return;
  832. }else if( !this.parent ){
  833. throw "Cannot move system root";
  834. }else if( targetParent.isDescendantOf(this) ){
  835. throw "Cannot move a node to it's own descendant";
  836. }
  837. // Unlink this node from current parent
  838. if( this.parent.children.length === 1 ) {
  839. this.parent.children = this.parent.lazy ? [] : null;
  840. this.parent.expanded = false;
  841. } else {
  842. pos = $.inArray(this, this.parent.children);
  843. _assert(pos >= 0);
  844. this.parent.children.splice(pos, 1);
  845. }
  846. // Remove from source DOM parent
  847. // if(this.parent.ul){
  848. // this.parent.ul.removeChild(this.li);
  849. // }
  850. // Insert this node to target parent's child list
  851. this.parent = targetParent;
  852. if( targetParent.hasChildren() ) {
  853. switch(mode) {
  854. case "child":
  855. // Append to existing target children
  856. targetParent.children.push(this);
  857. break;
  858. case "before":
  859. // Insert this node before target node
  860. pos = $.inArray(targetNode, targetParent.children);
  861. _assert(pos >= 0);
  862. targetParent.children.splice(pos, 0, this);
  863. break;
  864. case "after":
  865. // Insert this node after target node
  866. pos = $.inArray(targetNode, targetParent.children);
  867. _assert(pos >= 0);
  868. targetParent.children.splice(pos+1, 0, this);
  869. break;
  870. default:
  871. throw "Invalid mode " + mode;
  872. }
  873. } else {
  874. targetParent.children = [ this ];
  875. }
  876. // Parent has no <ul> tag yet:
  877. // if( !targetParent.ul ) {
  878. // // This is the parent's first child: create UL tag
  879. // // (Hidden, because it will be
  880. // targetParent.ul = document.createElement("ul");
  881. // targetParent.ul.style.display = "none";
  882. // targetParent.li.appendChild(targetParent.ul);
  883. // }
  884. // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded))
  885. // if(this.li){
  886. // targetParent.ul.appendChild(this.li);
  887. // }^
  888. // Let caller modify the nodes
  889. if( map ){
  890. targetNode.visit(map, true);
  891. }
  892. // Handle cross-tree moves
  893. if( this.tree !== targetNode.tree ) {
  894. // Fix node.tree for all source nodes
  895. // _assert(false, "Cross-tree move is not yet implemented.");
  896. this.warn("Cross-tree moveTo is experimantal!");
  897. this.visit(function(n){
  898. // TODO: fix selection state and activation, ...
  899. n.tree = targetNode.tree;
  900. }, true);
  901. }
  902. // Update HTML markup
  903. if( !prevParent.isDescendantOf(targetParent)) {
  904. prevParent.render();
  905. }
  906. if( !targetParent.isDescendantOf(prevParent) && targetParent !== prevParent) {
  907. targetParent.render();
  908. }
  909. // TODO: fix selection state
  910. // TODO: fix active state
  911. /*
  912. var tree = this.tree;
  913. var opts = tree.options;
  914. var pers = tree.persistence;
  915. // Always expand, if it's below minExpandLevel
  916. // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel());
  917. if ( opts.minExpandLevel >= ftnode.getLevel() ) {
  918. // tree.logDebug ("Force expand for %o", ftnode);
  919. this.bExpanded = true;
  920. }
  921. // In multi-hier mode, update the parents selection state
  922. // issue #82: only if not initializing, because the children may not exist yet
  923. // if( !ftnode.data.isStatusNode && opts.selectMode==3 && !isInitializing )
  924. // ftnode._fixSelectionState();
  925. // In multi-hier mode, update the parents selection state
  926. if( ftnode.bSelected && opts.selectMode==3 ) {
  927. var p = this;
  928. while( p ) {
  929. if( !p.hasSubSel )
  930. p._setSubSel(true);
  931. p = p.parent;
  932. }
  933. }
  934. // render this node and the new child
  935. if ( tree.bEnableUpdate )
  936. this.render();
  937. return ftnode;
  938. */
  939. },
  940. /**
  941. * Discard and reload all children of a lazy node.
  942. * @param {Boolean} [discard=false]
  943. * @returns $.Promise
  944. */
  945. lazyLoad: function(discard) {
  946. if(discard){
  947. this.discard();
  948. }else{
  949. _assert(!$.isArray(this.children));
  950. }
  951. var source = this.tree._triggerNodeEvent("lazyload", this);
  952. _assert(typeof source !== "boolean", "lazyload event must return source in data.result");
  953. return this.tree._callHook("nodeLoadChildren", this, source);
  954. },
  955. /**
  956. * @see Fancytree#nodeRender
  957. */
  958. render: function(force, deep) {
  959. return this.tree._callHook("nodeRender", this, force, deep);
  960. },
  961. /**
  962. * @see Fancytree#nodeRenderTitle
  963. */
  964. renderTitle: function() {
  965. return this.tree._callHook("nodeRenderTitle", this);
  966. },
  967. /**
  968. * @see Fancytree#nodeRenderStatus
  969. */
  970. renderStatus: function() {
  971. return this.tree._callHook("nodeRenderStatus", this);
  972. },
  973. /** Remove this node (not allowed for root).*/
  974. remove: function() {
  975. return this.parent.removeChild(this);
  976. },
  977. /**Remove childNode from list of direct children.*/
  978. removeChild: function(childNode) {
  979. return this.tree._callHook("nodeRemoveChild", this, childNode);
  980. },
  981. /**Remove all child nodes (and descendents).*/
  982. removeChildren: function() {
  983. return this.tree._callHook("nodeRemoveChildren", this);
  984. },
  985. // TODO: resetLazy()
  986. /** Schedule activity for delayed execution (cancel any pending request).
  987. * scheduleAction('cancel') will cancel the request.
  988. */
  989. scheduleAction: function(mode, ms) {
  990. if( this.tree.timer ) {
  991. clearTimeout(this.tree.timer);
  992. // this.tree.debug("clearTimeout(%o)", this.tree.timer);
  993. }
  994. this.tree.timer = null;
  995. var self = this; // required for closures
  996. switch (mode) {
  997. case "cancel":
  998. // Simply made sure that timer was cleared
  999. break;
  1000. case "expand":
  1001. this.tree.timer = setTimeout(function(){
  1002. self.tree.debug("setTimeout: trigger expand");
  1003. self.setExpanded(true);
  1004. }, ms);
  1005. break;
  1006. case "activate":
  1007. this.tree.timer = setTimeout(function(){
  1008. self.tree.debug("setTimeout: trigger activate");
  1009. self.setActive(true);
  1010. }, ms);
  1011. break;
  1012. default:
  1013. throw "Invalid mode " + mode;
  1014. }
  1015. // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer);
  1016. },
  1017. /**
  1018. *
  1019. * @param {Boolean | PlainObject} [effects=false] animation options.
  1020. * @param {FancytreeNode} [topNode=null] this node will remain visible in
  1021. * any case, even if `this` is outside the scroll pane.
  1022. * @returns $.Promise
  1023. */
  1024. scrollIntoView: function(effects, topNode) {
  1025. effects = (effects === true) ? {duration: 200, queue: false} : effects;
  1026. var topNodeY,
  1027. dfd = new $.Deferred(),
  1028. nodeY = $(this.span).position().top,
  1029. nodeHeight = $(this.span).height(),
  1030. $container = this.tree.$container,
  1031. scrollTop = $container[0].scrollTop,
  1032. horzScrollHeight = Math.max(0, ($container.innerHeight() - $container[0].clientHeight)),
  1033. // containerHeight = $container.height(),
  1034. containerHeight = $container.height() - horzScrollHeight,
  1035. newScrollTop = null;
  1036. // console.log("horzScrollHeight: " + horzScrollHeight);
  1037. // console.log("$container[0].scrollTop: " + $container[0].scrollTop);
  1038. // console.log("$container[0].scrollHeight: " + $container[0].scrollHeight);
  1039. // console.log("$container[0].clientHeight: " + $container[0].clientHeight);
  1040. // console.log("$container.innerHeight(): " + $container.innerHeight());
  1041. // console.log("$container.height(): " + $container.height());
  1042. if(nodeY < 0){
  1043. newScrollTop = scrollTop + nodeY;
  1044. }else if((nodeY + nodeHeight) > containerHeight){
  1045. newScrollTop = scrollTop + nodeY - containerHeight + nodeHeight;
  1046. // If a topNode was passed, make sure that it is never scrolled
  1047. // outside the upper border
  1048. if(topNode){
  1049. topNodeY = topNode ? $(topNode.span).position().top : 0;
  1050. if((nodeY - topNodeY) > containerHeight){
  1051. newScrollTop = scrollTop + nodeY;
  1052. }
  1053. }
  1054. }
  1055. if(newScrollTop !== null){
  1056. if(effects){
  1057. // TODO: resolve dfd after animation
  1058. // var that = this;
  1059. $container.animate({scrollTop: newScrollTop}, effects);
  1060. }else{
  1061. $container[0].scrollTop = newScrollTop;
  1062. dfd.resolveWith(this);
  1063. }
  1064. }else{
  1065. dfd.resolveWith(this);
  1066. }
  1067. return dfd.promise();
  1068. /* from jQuery.menu:
  1069. var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
  1070. if ( this._hasScroll() ) {
  1071. borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
  1072. paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
  1073. offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
  1074. scroll = this.activeMenu.scrollTop();
  1075. elementHeight = this.activeMenu.height();
  1076. itemHeight = item.height();
  1077. if ( offset < 0 ) {
  1078. this.activeMenu.scrollTop( scroll + offset );
  1079. } else if ( offset + itemHeight > elementHeight ) {
  1080. this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
  1081. }
  1082. }
  1083. */
  1084. },
  1085. /**Activate this node.
  1086. * @param {Boolean} [flag=true] pass false to deactivate
  1087. */
  1088. setActive: function(flag){
  1089. return this.tree._callHook("nodeSetActive", this, flag);
  1090. },
  1091. /**Expand this node.
  1092. * @param {Boolean} [flag=true] pass false to collapse
  1093. */
  1094. setExpanded: function(flag){
  1095. return this.tree._callHook("nodeSetExpanded", this, flag);
  1096. },
  1097. /**Set keyboard focus to this node.
  1098. * @param {Boolean} [flag=true] pass false to blur
  1099. * @see Fancytree#setFocus
  1100. */
  1101. setFocus: function(flag){
  1102. return this.tree._callHook("nodeSetFocus", this, flag);
  1103. },
  1104. // TODO: setLazyNodeStatus
  1105. /**Select this node.
  1106. * @param {Boolean} [flag=true] pass false to deselect
  1107. */
  1108. setSelected: function(flag){
  1109. return this.tree._callHook("nodeSetSelected", this, flag);
  1110. },
  1111. setTitle: function(title){
  1112. this.title = title;
  1113. this.renderTitle();
  1114. },
  1115. /**Sort child list by title.
  1116. * @param {function} [cmd] custom compare function.
  1117. * @param {Boolean} [deep] pass true to sort all descendant nodes
  1118. */
  1119. sortChildren: function(cmp, deep) {
  1120. var i,l,
  1121. cl = this.children;
  1122. if( !cl ){
  1123. return;
  1124. }
  1125. cmp = cmp || function(a, b) {
  1126. var x = a.title.toLowerCase(),
  1127. y = b.title.toLowerCase();
  1128. return x === y ? 0 : x > y ? 1 : -1;
  1129. };
  1130. cl.sort(cmp);
  1131. if( deep ){
  1132. for(i=0, l=cl.length; i<l; i++){
  1133. if( cl[i].children ){
  1134. cl[i].sortChildren(cmp, "$norender$");
  1135. }
  1136. }
  1137. }
  1138. if( deep !== "$norender$" ){
  1139. this.render();
  1140. }
  1141. },
  1142. /** Convert node (or whole branch) into a dictionary.
  1143. *
  1144. * The result is compatible with node.addChildren().
  1145. *
  1146. * @param {Boolean} recursive
  1147. * @param {function} callback callback(dict) is called for every dict (), in order to allow modifications
  1148. * @returns {NodePatch}
  1149. */
  1150. toDict: function(recursive, callback) {
  1151. var i, l, node,
  1152. dict = {},
  1153. self = this;
  1154. $.each(NODE_ATTRS, function(i, a){
  1155. // if(self[a] !== undefined && self[a] !== null){
  1156. if(self[a] || self[a] === false){
  1157. dict[a] = self[a];
  1158. }
  1159. });
  1160. if(!$.isEmptyObject(this.data)){
  1161. dict.data = $.extend({}, this.data);
  1162. if($.isEmptyObject(dict.data)){
  1163. delete dict.data;
  1164. }
  1165. }
  1166. if( callback ){
  1167. callback(dict);
  1168. }
  1169. if( recursive ) {
  1170. if(this.hasChildren()){
  1171. dict.children = [];
  1172. for(i=0, l=this.children.length; i<l; i++ ){
  1173. node = this.children[i];
  1174. if( !node.isStatusNode ){
  1175. dict.children.push(node.toDict(true, callback));
  1176. }
  1177. }
  1178. }else{
  1179. // dict.children = null;
  1180. }
  1181. }
  1182. return dict;
  1183. },
  1184. /** Flip expanded status. */
  1185. toggleExpanded: function(){
  1186. return this.tree._callHook("nodeToggleExpanded", this);
  1187. },
  1188. /** Flip selection status. */
  1189. toggleSelected: function(){
  1190. return this.tree._callHook("nodeToggleSelected", this);
  1191. },
  1192. toString: function() {
  1193. return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>";
  1194. },
  1195. /** Call fn(node) for all child nodes. Stop iteration, if fn() returns false.
  1196. * Skip current branch, if fn() returns 'skip'.
  1197. * @param {function} fn the callback function.
  1198. * Return false to stop iteration, return "skip" to skip this node and children only.
  1199. * @param {Boolean} [includeSelf=false]
  1200. * @returns {Boolean} false, if the iterator was stopped.
  1201. */
  1202. visit: function(fn, includeSelf) {
  1203. var i, l,
  1204. res = true,
  1205. children = this.children;
  1206. if( includeSelf === true ) {
  1207. res = fn(this);
  1208. if( res === false || res === "skip" ){
  1209. return res;
  1210. }
  1211. }
  1212. if(children){
  1213. for(i=0, l=children.length; i<l; i++){
  1214. res = children[i].visit(fn, true);
  1215. if( res === false ){
  1216. break;
  1217. }
  1218. }
  1219. }
  1220. return res;
  1221. },
  1222. /**
  1223. *
  1224. * @param fn
  1225. * @param includeSelf
  1226. * @returns {Boolean}
  1227. */
  1228. visitParents: function(fn, includeSelf) {
  1229. // Visit parent nodes (bottom up)
  1230. if(includeSelf && fn(this) === false){
  1231. return false;
  1232. }
  1233. var p = this.parent;
  1234. while( p ) {
  1235. if(fn(p) === false){
  1236. return false;
  1237. }
  1238. p = p.parent;
  1239. }
  1240. return true;
  1241. },
  1242. /** Write warning to browser console (prepending node info)
  1243. *
  1244. * @param {*} msg string or object or array of such
  1245. */
  1246. warn: function(msg){
  1247. Array.prototype.unshift.call(arguments, this.toString());
  1248. consoleApply("warn", arguments);
  1249. }
  1250. };
  1251. /* *****************************************************************************
  1252. * Fancytree
  1253. */
  1254. /**
  1255. * Construct a new tree.
  1256. * @class The controller behind a fancytree.
  1257. * @name Fancytree
  1258. * @constructor
  1259. * @param {Widget} widget
  1260. *
  1261. * @property {FancytreeOptions} options
  1262. * @property {FancytreeNode} rootNode
  1263. * @property {FancytreeNode} activeNode
  1264. * @property {FancytreeNode} focusNode
  1265. * @property {jQueryObject} $div
  1266. * @property {object} widget
  1267. * @property {String} _id
  1268. * @property {String} statusClassPropName
  1269. * @property {String} ariaPropName
  1270. * @property {String} nodeContainerAttrName
  1271. * @property {FancytreeNode} lastSelectedNode
  1272. */
  1273. function Fancytree(widget){
  1274. // TODO: rename widget to widget (it's not a jQuery object)
  1275. this.widget = widget;
  1276. this.$div = widget.element;
  1277. this.options = widget.options;
  1278. this._id = $.ui.fancytree._nextId++;
  1279. this._ns = ".fancytree-" + this._id; // append for namespaced events
  1280. this.activeNode = null;
  1281. this.focusNode = null;
  1282. this.lastSelectedNode = null;
  1283. this.systemFocusElement = null,
  1284. this.statusClassPropName = "span";
  1285. this.ariaPropName = "li";
  1286. this.nodeContainerAttrName = "li";
  1287. // Remove previous markup if any
  1288. this.$div.find(">ul.fancytree-container").remove();
  1289. // Create a node without parent.
  1290. var fakeParent = { tree: this },
  1291. $ul;
  1292. this.rootNode = new FancytreeNode(fakeParent, {
  1293. title: "root",
  1294. key: "root_" + this._id,
  1295. children: null
  1296. });
  1297. this.rootNode.parent = null;
  1298. // Create root markup
  1299. $ul = $("<ul>", {
  1300. "class": "ui-fancytree fancytree-container"
  1301. }).appendTo(this.$div);
  1302. this.$container = $ul;
  1303. this.rootNode.ul = $ul[0];
  1304. if(this.options.debugLevel == null){
  1305. this.options.debugLevel = FT.debugLevel;
  1306. }
  1307. // Add container to the TAB chain
  1308. // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant
  1309. if(this.options.tabbable){
  1310. this.$container.attr("tabindex", "0");
  1311. }
  1312. if(this.options.aria){
  1313. this.$container.attr("role", "tree")
  1314. .attr("aria-multiselectable", true);
  1315. }
  1316. }
  1317. Fancytree.prototype = /**@lends Fancytree*/{
  1318. /** Return a context object that can be re-used for _callHook().
  1319. * @param {Fancytree | FancytreeNode | EventData} obj
  1320. * @param {Event} originalEvent
  1321. * @returns {EventData}
  1322. */
  1323. _makeHookContext: function(obj, originalEvent) {
  1324. if(obj.node !== undefined){
  1325. // obj is already a context object
  1326. if(originalEvent && obj.originalEvent !== originalEvent){
  1327. $.error("invalid args");
  1328. }
  1329. return obj;
  1330. }else if(obj.tree){
  1331. // obj is a FancytreeNode
  1332. var tree = obj.tree;
  1333. return { node: obj, tree: tree, widget: tree.widget, options: tree.widget.options, originalEvent: originalEvent };
  1334. }else if(obj.widget){
  1335. // obj is a Fancytree
  1336. return { node: null, tree: obj, widget: obj.widget, options: obj.widget.options, originalEvent: originalEvent };
  1337. }
  1338. $.error("invalid args");
  1339. },
  1340. /** Trigger a hook function: funcName(ctx, [...]).
  1341. *
  1342. * @param {String} funcName
  1343. * @param {Fancytree|FancytreeNode|EventData} contextObject
  1344. * @param {any, ...} [_extraArgs] optional additional arguments
  1345. * @returns {any}
  1346. */
  1347. _callHook: function(funcName, contextObject, _extraArgs) {
  1348. var ctx = this._makeHookContext(contextObject),
  1349. fn = this[funcName],
  1350. args = Array.prototype.slice.call(arguments, 2);
  1351. if(!$.isFunction(fn)){
  1352. $.error("_callHook('" + funcName + "') is not a function");
  1353. }
  1354. args.unshift(ctx);
  1355. // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args);
  1356. return fn.apply(this, args);
  1357. },
  1358. /** Activate node with a given key.
  1359. *
  1360. * A prevously activated node will be deactivated.
  1361. * Pass key = false, to deactivate the current node only.
  1362. * @param {String} key
  1363. * @returns {FancytreeNode} activated node (null, if not found)
  1364. */
  1365. activateKey: function(key) {
  1366. var node = this.getNodeByKey(key);
  1367. if(node){
  1368. node.setActive();
  1369. }else if(this.activeNode){
  1370. this.activeNode.setActive(false);
  1371. }
  1372. return node;
  1373. },
  1374. /**
  1375. *
  1376. * @param {Array} patchList array of [key, NodePatch] arrays
  1377. * @returns {$.Promise} resolved, when all patches have been applied
  1378. * @see TreePatch
  1379. */
  1380. applyPatch: function(patchList) {
  1381. var dfd, i, p2, key, patch, node,
  1382. patchCount = patchList.length,
  1383. deferredList = [];
  1384. for(i=0; i<patchCount; i++){
  1385. p2 = patchList[i];
  1386. _assert(p2.length === 2, "patchList must be an array of length-2-arrays");
  1387. key = p2[0];
  1388. patch = p2[1];
  1389. node = (key === null) ? this.rootNode : this.getNodeByKey(key);
  1390. if(node){
  1391. dfd = new $.Deferred();
  1392. deferredList.push(dfd);
  1393. node.applyPatch(patch).always(_makeResolveFunc(dfd, node));
  1394. }else{
  1395. this.warn("could not find node with key '" + key + "'");
  1396. }
  1397. }
  1398. // Return a promise that is resovled, when ALL patches were applied
  1399. return $.when.apply($, deferredList).promise();
  1400. },
  1401. /* TODO: implement in dnd extension
  1402. cancelDrag: function() {
  1403. var dd = $.ui.ddmanager.current;
  1404. if(dd){
  1405. dd.cancel();
  1406. }
  1407. },
  1408. */
  1409. count: function() {
  1410. return this.rootNode.countChildren();
  1411. },
  1412. /** Write to browser console if debugLevel >= 2 (prepending tree info)
  1413. *
  1414. * @param {*} msg string or object or array of such
  1415. */
  1416. debug: function(msg){
  1417. if( this.options.debugLevel >= 2 ) {
  1418. Array.prototype.unshift.call(arguments, this.toString());
  1419. consoleApply("debug", arguments);
  1420. }
  1421. },
  1422. // TODO: disable()
  1423. // TODO: enable()
  1424. // TODO: enableUpdate()
  1425. // TODO: fromDict
  1426. /**
  1427. * Generate INPUT elements that can be submitted with html forms.
  1428. *
  1429. * In selectMode 3 only the topmost selected nodes are considered.
  1430. *
  1431. * @param {Boolean | String} [selected=true]
  1432. * @param {Boolean | String} [active=true]
  1433. */
  1434. generateFormElements: function(selected, active) {
  1435. // TODO: test case
  1436. var nodeList,
  1437. selectedName = (selected !== false) ? "ft_" + this._id : selected,
  1438. activeName = (active !== false) ? "ft_" + this._id + "_active" : active,
  1439. id = "fancytree_result_" + this._id,
  1440. $result = this.$container.find("div#" + id);
  1441. if($result.length){
  1442. $result.empty();
  1443. }else{
  1444. $result = $("<div>", {
  1445. id: id
  1446. }).hide().appendTo(this.$container);
  1447. }
  1448. if(selectedName){
  1449. nodeList = this.getSelectedNodes( this.options.selectMode === 3 );
  1450. $.each(nodeList, function(idx, node){
  1451. $result.append($("<input>", {
  1452. type: "checkbox",
  1453. name: selectedName,
  1454. value: node.key,
  1455. checked: true
  1456. }));
  1457. });
  1458. }
  1459. if(activeName && this.activeNode){
  1460. $result.append($("<input>", {
  1461. type: "radio",
  1462. name: activeName,
  1463. value: this.activeNode.key,
  1464. checked: true
  1465. }));
  1466. }
  1467. },
  1468. /**
  1469. * Return node that is active.
  1470. * @returns {FancytreeNode}
  1471. */
  1472. getActiveNode: function() {
  1473. return this.activeNode;
  1474. },
  1475. /** @returns {FancytreeNode | null}*/
  1476. getFirstChild: function() {
  1477. return this.rootNode.getFirstChild();
  1478. },
  1479. /**
  1480. * Return node that has keyboard focus.
  1481. * @param {Boolean} [ifTreeHasFocus=false]
  1482. * @returns {FancytreeNode}
  1483. */
  1484. getFocusNode: function(ifTreeHasFocus) {
  1485. // TODO: implement ifTreeHasFocus
  1486. return this.focusNode;
  1487. },
  1488. getNodeByKey: function(key, searchRoot) {
  1489. // Search the DOM by element ID (assuming this is faster than traversing all nodes).
  1490. // $("#...") has problems, if the key contains '.', so we use getElementById()
  1491. var el, match;
  1492. if(!searchRoot){
  1493. el = document.getElementById(this.options.idPrefix + key);
  1494. if( el ){
  1495. return el.ftnode ? el.ftnode : null;
  1496. }
  1497. }
  1498. // Not found in the DOM, but still may be in an unrendered part of tree
  1499. // TODO: optimize with specialized loop
  1500. // TODO: consider keyMap?
  1501. searchRoot = searchRoot || this.rootNode;
  1502. match = null;
  1503. searchRoot.visit(function(node){
  1504. // window.console.log("getNodeByKey(" + key + "): ", node.key);
  1505. if(node.key === key) {
  1506. match = node;
  1507. return false;
  1508. }
  1509. }, true);
  1510. return match;
  1511. },
  1512. // TODO: getRoot()
  1513. /**
  1514. * Return a list of selected nodes.
  1515. * @param {Boolean} [stopOnParents=false] only return the topmost selected
  1516. * node (useful with selectMode 3)
  1517. * @returns {FancytreeNode[]}
  1518. */
  1519. getSelectedNodes: function(stopOnParents) {
  1520. var nodeList = [];
  1521. this.rootNode.visit(function(node){
  1522. if( node.selected ) {
  1523. nodeList.push(node);
  1524. if( stopOnParents === true ){
  1525. return "skip"; // stop processing this branch
  1526. }
  1527. }
  1528. });
  1529. return nodeList;
  1530. },
  1531. /**
  1532. * @returns {Boolean} true if the tree control has keyboard focus
  1533. */
  1534. hasFocus: function(){
  1535. return FT.focusTree === this;
  1536. },
  1537. /** Write to browser console if debugLevel >= 1 (prepending tree info)
  1538. *
  1539. * @param {*} msg string or object or array of such
  1540. */
  1541. info: function(msg){
  1542. if( this.options.debugLevel >= 1 ) {
  1543. Array.prototype.unshift.call(arguments, this.toString());
  1544. consoleApply("info", arguments);
  1545. }
  1546. },
  1547. /*
  1548. TODO: isInitializing: function() {
  1549. return ( this.phase=="init" || this.phase=="postInit" );
  1550. },
  1551. TODO: isReloading: function() {
  1552. return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound;
  1553. },
  1554. TODO: isUserEvent: function() {
  1555. return ( this.phase=="userEvent" );
  1556. },
  1557. */
  1558. /**
  1559. * Expand all parents of one or more nodes.
  1560. * Calls
  1561. * @param {String | String[]} keyPath one or more key paths (e.g. '/3/2_1/7')
  1562. * @param {function} callback callbeck(mode) is called for every visited node
  1563. * @returns {$.Promise}
  1564. */
  1565. /*
  1566. _loadKeyPath: function(keyPath, callback) {
  1567. var tree = this.tree;
  1568. tree.logDebug("%s._loadKeyPath(%s)", this, keyPath);
  1569. if(keyPath === ""){
  1570. throw "Key path must not be empty";
  1571. }
  1572. var segList = keyPath.split(tree.options.keyPathSeparator);
  1573. if(segList[0] === ""){
  1574. throw "Key path must be relative (don't start with '/')";
  1575. }
  1576. var seg = segList.shift();
  1577. for(var i=0, l=this.childList.length; i < l; i++){
  1578. var child = this.childList[i];
  1579. if( child.data.key === seg ){
  1580. if(segList.length === 0) {
  1581. // Found the end node
  1582. callback.call(tree, child, "ok");
  1583. }else if(child.data.isLazy && (child.childList === null || child.childList === undefined)){
  1584. tree.logDebug("%s._loadKeyPath(%s) -> reloading %s...", this, keyPath, child);
  1585. var self = this;
  1586. child.reloadChildren(function(node, isOk){
  1587. // After loading, look for direct child with that key
  1588. if(isOk){
  1589. tree.logDebug("%s._loadKeyPath(%s) -> reloaded %s.", node, keyPath, node);
  1590. callback.call(tree, child, "loaded");
  1591. node._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback);
  1592. }else{
  1593. tree.logWarning("%s._loadKeyPath(%s) -> reloadChildren() failed.", self, keyPath);
  1594. callback.call(tree, child, "error");
  1595. }
  1596. }); // Note: this line gives a JSLint warning (Don't make functions within a loop)
  1597. // we can ignore it, since it will only be exectuted once, the the loop is ended
  1598. // See also http://stackoverflow.com/questions/3037598/how-to-get-around-the-jslint-error-dont-make-functions-within-a-loop
  1599. } else {
  1600. callback.call(tree, child, "loaded");
  1601. // Look for direct child with that key
  1602. child._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback);
  1603. }
  1604. return;
  1605. }
  1606. }
  1607. // Could not find key
  1608. tree.logWarning("Node not found: " + seg);
  1609. return;
  1610. },
  1611. */
  1612. loadKeyPath: function(keyPathList, callback, _rootNode) {
  1613. var deferredList, dfd, i, path, key, loadMap, node, segList,
  1614. root = _rootNode || this.rootNode,
  1615. sep = this.options.keyPathSeparator,
  1616. self = this;
  1617. if(!$.isArray(keyPathList)){
  1618. keyPathList = [keyPathList];
  1619. }
  1620. // Pass 1: handle all path segments for nodes that are already loaded
  1621. // Collect distinct top-most lazy nodes in a map
  1622. loadMap = {};
  1623. for(i=0; i<keyPathList.length; i++){
  1624. path = keyPathList[i];
  1625. // strip leading slash
  1626. if(path.charAt(0) === sep){
  1627. path = path.substr(1);
  1628. }
  1629. // traverse and strip keys, until we hit a lazy, unloaded node
  1630. segList = path.split(sep);
  1631. while(segList.length){
  1632. key = segList.shift();
  1633. // node = _findDirectChild(root, key);
  1634. node = root._findDirectChild(key);
  1635. if(!node){
  1636. this.warn("loadKeyPath: key not found: " + key + " (parent: " + root + ")");
  1637. callback.call(this, key, "error");
  1638. break;
  1639. }else if(segList.length === 0){
  1640. callback.call(this, node, "ok");
  1641. break;
  1642. }else if(!node.lazy || (node.hasChildren() !== undefined )){
  1643. callback.call(this, node, "loaded");
  1644. root = node;
  1645. }else{
  1646. callback.call(this, node, "loaded");
  1647. // segList.unshift(key);
  1648. if(loadMap[key]){
  1649. loadMap[key].push(segList.join(sep));
  1650. }else{
  1651. loadMap[key] = [segList.join(sep)];
  1652. }
  1653. break;
  1654. }
  1655. }
  1656. }
  1657. // alert("loadKeyPath: loadMap=" + JSON.stringify(loadMap));
  1658. // Now load all lazy nodes and continue itearation for remaining paths
  1659. deferredList = [];
  1660. // Avoid jshint warning 'Don't make functions within a loop.':
  1661. function __lazyload(key, node, dfd){
  1662. callback.call(self, node, "loading");
  1663. node.lazyLoad().done(function(){
  1664. self.loadKeyPath.call(self, loadMap[key], callback, node).always(_makeResolveFunc(dfd, self));
  1665. }).fail(function(errMsg){
  1666. self.warn("loadKeyPath: error loading: " + key + " (parent: " + root + ")");
  1667. callback.call(self, node, "error");
  1668. dfd.reject();
  1669. });
  1670. }
  1671. for(key in loadMap){
  1672. node = root._findDirectChild(key);
  1673. // alert("loadKeyPath: lazy node(" + key + ") = " + node);
  1674. dfd = new $.Deferred();
  1675. deferredList.push(dfd);
  1676. __lazyload(key, node, dfd);
  1677. }
  1678. // Return a promise that is resovled, when ALL paths were loaded
  1679. return $.when.apply($, deferredList).promise();
  1680. },
  1681. /** _Default handling for mouse click events. */
  1682. nodeClick: function(ctx) {
  1683. // this.tree.logDebug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which);
  1684. var activate, expand,
  1685. event = ctx.originalEvent,
  1686. targetType = ctx.targetType,
  1687. node = ctx.node;
  1688. // TODO: use switch
  1689. // TODO: make sure clicks on embedded <input> doesn't steal focus (see table sample)
  1690. if( targetType === "expander" ) {
  1691. // Clicking the expander icon always expands/collapses
  1692. this._callHook("nodeToggleExpanded", ctx);
  1693. // this._callHook("nodeSetFocus", ctx, true); // issue 95
  1694. } else if( targetType === "checkbox" ) {
  1695. // Clicking the checkbox always (de)selects
  1696. this._callHook("nodeToggleSelected", ctx);
  1697. this._callHook("nodeSetFocus", ctx, true); // issue 95
  1698. } else {
  1699. // Honor `clickFolderMode` for
  1700. expand = false;
  1701. activate = true;
  1702. if( node.folder ) {
  1703. switch( ctx.options.clickFolderMode ) {
  1704. case 2: // expand only
  1705. expand = true;
  1706. activate = false;
  1707. break;
  1708. case 3: // expand and activate
  1709. activate = true;
  1710. expand = true; //!node.isExpanded();
  1711. break;
  1712. // else 1 or 4: just activate
  1713. }
  1714. }
  1715. if( activate ) {
  1716. this.nodeSetFocus(ctx);
  1717. this._callHook("nodeSetActive", ctx, true);
  1718. }
  1719. if( expand ) {
  1720. if(!activate){
  1721. // this._callHook("nodeSetFocus", ctx);
  1722. }
  1723. // this._callHook("nodeSetExpanded", ctx, true);
  1724. this._callHook("nodeToggleExpanded", ctx);
  1725. }
  1726. }
  1727. // Make sure that clicks stop, otherwise <a href='#'> jumps to the top
  1728. if(event.target.localName === "a" && event.target.className === "fancytree-title"){
  1729. event.preventDefault();
  1730. }
  1731. // TODO: return promise?
  1732. },
  1733. nodeCollapseSiblings: function(ctx) {
  1734. // TODO: return promise?
  1735. var ac, i, l,
  1736. node = ctx.node;
  1737. if( node.parent ){
  1738. ac = node.parent.children;
  1739. for (i=0, l=ac.length; i<l; i++) {
  1740. if ( ac[i] !== node && ac[i].expanded ){
  1741. this._callHook("nodeSetExpanded", ac[i], false);
  1742. }
  1743. }
  1744. }
  1745. },
  1746. nodeDblclick: function(ctx) {
  1747. // TODO: return promise?
  1748. if( ctx.targetType === "title" && ctx.options.clickFolderMode === 4) {
  1749. // this.nodeSetFocus(ctx);
  1750. // this._callHook("nodeSetActive", ctx, true);
  1751. this._callHook("nodeToggleExpanded", ctx);
  1752. }
  1753. // TODO: prevent text selection on dblclicks
  1754. if( ctx.targetType === "title" ) {
  1755. ctx.originalEvent.preventDefault();
  1756. }
  1757. },
  1758. /** Default handling for mouse keydown events.
  1759. *
  1760. * NOTE: this may be called with node == null if tree (but no node) has focus.
  1761. */
  1762. nodeKeydown: function(ctx) {
  1763. // TODO: return promise?
  1764. var i, parents,
  1765. event = ctx.originalEvent,
  1766. node = ctx.node,
  1767. tree = ctx.tree,
  1768. opts = ctx.options,
  1769. handled = true,
  1770. KC = $.ui.keyCode,
  1771. sib = null;
  1772. // node.debug("ftnode.nodeKeydown(" + event.type + "): ftnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which);
  1773. // Set focus to first node, if no other node has the focus yet
  1774. if( !node ){
  1775. this.rootNode.getFirstChild().setFocus();
  1776. node = ctx.node = this.focusNode;
  1777. node.debug("Keydown force focus on first node");
  1778. }
  1779. // Navigate to node
  1780. function _goto(n){
  1781. if( n ){
  1782. n.makeVisible();
  1783. return (event.ctrlKey || !opts.autoActivate ) ? n.setFocus() : n.setActive();
  1784. }
  1785. }
  1786. switch( event.which ) {
  1787. // charCodes:
  1788. case KC.NUMPAD_ADD: //107: // '+'
  1789. case 187: // '+' @ Chrome, Safari
  1790. tree.nodeSetExpanded(ctx, true);
  1791. break;
  1792. case KC.NUMPAD_SUBTRACT: // '-'
  1793. case 189: // '-' @ Chrome, Safari
  1794. tree.nodeSetExpanded(ctx, false);
  1795. break;
  1796. case KC.SPACE:
  1797. if(opts.checkbox){
  1798. tree.nodeToggleSelected(ctx);
  1799. }else{
  1800. tree.nodeSetActive(ctx, true);
  1801. }
  1802. break;
  1803. case KC.ENTER:
  1804. tree.nodeSetActive(ctx, true);
  1805. break;
  1806. case KC.BACKSPACE:
  1807. _goto(node.parent);
  1808. break;
  1809. case KC.LEFT:
  1810. if( node.expanded ) {
  1811. tree.nodeSetExpanded(ctx, false);
  1812. // tree.nodeSetFocus(ctx);
  1813. _goto(node);
  1814. } else if( node.parent && node.parent.parent ) {
  1815. // node.parent.setFocus();
  1816. _goto(node.parent);
  1817. }
  1818. break;
  1819. case KC.RIGHT:
  1820. if( !node.expanded && (node.children || node.lazy) ) {
  1821. tree.nodeSetExpanded(ctx, true);
  1822. // tree.nodeSetFocus(ctx);
  1823. _goto(node);
  1824. } else if( node.children ) {
  1825. // node.children[0].setFocus();
  1826. _goto(node.children[0]);
  1827. }
  1828. break;
  1829. case KC.UP:
  1830. sib = node.getPrevSibling();
  1831. while( sib && sib.expanded && sib.children ){
  1832. sib = sib.children[sib.children.length - 1];
  1833. }
  1834. if( !sib && node.parent && node.parent.parent ){
  1835. sib = node.parent;
  1836. }
  1837. _goto(sib);
  1838. break;
  1839. case KC.DOWN:
  1840. if( node.expanded && node.children ) {
  1841. sib = node.children[0];
  1842. } else {
  1843. parents = node.getParentList(false, true);
  1844. for(i=parents.length-1; i>=0; i--) {
  1845. sib = parents[i].getNextSibling();
  1846. if( sib ){ break; }
  1847. }
  1848. }
  1849. _goto(sib);
  1850. break;
  1851. default:
  1852. handled = false;
  1853. }
  1854. if(handled){
  1855. event.preventDefault();
  1856. }
  1857. },
  1858. // /** Default handling for mouse keypress events. */
  1859. // nodeKeypress: function(ctx) {
  1860. // var event = ctx.originalEvent;
  1861. // },
  1862. // /** Trigger lazyload event (async). */
  1863. // nodeLazyLoad: function(ctx) {
  1864. // var node = ctx.node;
  1865. // if(this._triggerNodeEvent())
  1866. // },
  1867. /** Load children (async).
  1868. * source may be
  1869. * - an array of children
  1870. * - a node object
  1871. * - an Ajax options object
  1872. * - an Ajax.promise
  1873. *
  1874. * @param {object} ctx
  1875. * @param {object[]|object|string|$.Promise|function} source
  1876. * @returns {$.Promise} The deferred will be resolved as soon as the (ajax)
  1877. * data was rendered.
  1878. */
  1879. nodeLoadChildren: function(ctx, source) {
  1880. var ajax, children, delay, dfd,
  1881. tree = ctx.tree,
  1882. node = ctx.node,
  1883. self = this;
  1884. if($.isFunction(source)){
  1885. source = source();
  1886. }
  1887. if(source.url || $.isFunction(source.done)){
  1888. tree.nodeSetStatus(ctx, "loading");
  1889. if(source.url){
  1890. // `source` is an Ajax options object
  1891. ajax = $.extend({}, ctx.options.ajax, source);
  1892. if(ajax.debugLazyDelay){
  1893. // simulate a slow server
  1894. delay = ajax.debugLazyDelay;
  1895. if($.isArray(delay)){ // random delay range [min..max]
  1896. delay = delay[0] + Math.random() * (delay[1] - delay[0]);
  1897. }
  1898. node.debug("nodeLoadChildren waiting debug delay " + Math.round(delay) + "ms");
  1899. dfd = $.Deferred();
  1900. setTimeout(function(){
  1901. ajax.debugLazyDelay = false;
  1902. self.nodeLoadChildren(ctx, ajax).complete(function(){
  1903. dfd.resolve.apply(this, arguments);
  1904. });
  1905. }, delay);
  1906. return dfd;
  1907. }else{
  1908. dfd = $.ajax(ajax);
  1909. }
  1910. }else{
  1911. // `source` is a promise, as returned by a $.ajax call
  1912. dfd = source;
  1913. }
  1914. dfd.done(function(data, textStatus, jqXHR){
  1915. var res;
  1916. tree.nodeSetStatus(ctx, "ok");
  1917. if(typeof data === "string"){ $.error("Ajax request returned a string (did you get the JSON dataType wrong?)."); }
  1918. // postProcess is similar to the standard dataFilter hook,
  1919. // but it is also called for JSONP
  1920. if( ctx.options.postProcess ){
  1921. res = tree._triggerNodeEvent("postProcess", ctx, ctx.originalEvent, {response: data, dataType: this.dataType});
  1922. data = $.isArray(res) ? res : data;
  1923. } else if (data && data.hasOwnProperty("d") && ctx.options.enableAspx ) {
  1924. // Process ASPX WebMethod JSON object inside "d" property
  1925. data = (typeof data.d === "string") ? $.parseJSON(data.d) : data.d;
  1926. }
  1927. children = data;
  1928. }).fail(function(jqXHR, textStatus, errorThrown){
  1929. tree.nodeSetStatus(ctx, "error", textStatus, jqXHR.status + ": " + errorThrown);
  1930. alert("error: " + textStatus + " (" + jqXHR.status + ": " + (errorThrown.message || errorThrown) + ")");
  1931. });
  1932. }else{
  1933. // `source` is an array of child objects
  1934. dfd = $.Deferred();
  1935. children = source;
  1936. dfd.resolve();
  1937. }
  1938. dfd.done(function(){
  1939. _assert($.isArray(children), "expected array of children");
  1940. node._setChildren(children);
  1941. if(node.parent){
  1942. // if nodeLoadChildren was called for rootNode, the caller must
  1943. // use tree.render() instead
  1944. if(node.isVisible()){
  1945. tree.nodeRender(ctx);
  1946. }
  1947. // trigger fancytreeloadchildren (except for tree-reload)
  1948. tree._triggerNodeEvent("loadChildren", node);
  1949. }
  1950. }).fail(function(){
  1951. tree.nodeRender(ctx);
  1952. });
  1953. return dfd;
  1954. },
  1955. // isVisible: function() {
  1956. // // Return true, if all parents are expanded.
  1957. // var parents = ctx.node.getParentList(false, false);
  1958. // for(var i=0, l=parents.length; i<l; i++){
  1959. // if( ! parents[i].expanded ){ return false; }
  1960. // }
  1961. // return true;
  1962. // },
  1963. /** Expand all keys that */
  1964. nodeLoadKeyPath: function(ctx, keyPathList) {
  1965. // TODO: implement and improve
  1966. // http://code.google.com/p/fancytree/issues/detail?id=222
  1967. },
  1968. /** Expand all parents.*/
  1969. nodeMakeVisible: function(ctx) {
  1970. // TODO: also scroll as neccessary: http://stackoverflow.com/questions/8938352/fancytree-how-to-scroll-to-active-node
  1971. // Do we need an extra parameter?
  1972. var i, l,
  1973. parents = ctx.node.getParentList(false, false);
  1974. for(i=0, l=parents.length; i<l; i++){
  1975. parents[i].setExpanded(true);
  1976. }
  1977. },
  1978. // /** Handle focusin/focusout events.*/
  1979. // nodeOnFocusInOut: function(ctx) {
  1980. // if(ctx.originalEvent.type === "focusin"){
  1981. // this.nodeSetFocus(ctx);
  1982. // // if(ctx.tree.focusNode){
  1983. // // $(ctx.tree.focusNode.li).removeClass("fancytree-focused");
  1984. // // }
  1985. // // ctx.tree.focusNode = ctx.node;
  1986. // // $(ctx.node.li).addClass("fancytree-focused");
  1987. // }else{
  1988. // _assert(ctx.originalEvent.type === "focusout");
  1989. // // ctx.tree.focusNode = null;
  1990. // // $(ctx.node.li).removeClass("fancytree-focused");
  1991. // }
  1992. // // $(ctx.node.li).toggleClass("fancytree-focused", ctx.originalEvent.type === "focus");
  1993. // },
  1994. /**
  1995. * Remove a single direct child of ctx.node.
  1996. * @param ctx
  1997. * @param {FancytreeNode} childNode dircect child of ctx.node
  1998. */
  1999. nodeRemoveChild: function(ctx, childNode) {
  2000. var idx,
  2001. node = ctx.node,
  2002. opts = ctx.options,
  2003. subCtx = $.extend({}, ctx, {node: childNode}),
  2004. children = node.children;
  2005. FT.debug("nodeRemoveChild()", node.toString(), childNode.toString());
  2006. if( children.length === 1 ) {
  2007. _assert(childNode === children[0]);
  2008. return this.nodeRemoveChildren(ctx);
  2009. }
  2010. if( this.activeNode && (childNode === this.activeNode || this.activeNode.isDescendantOf(childNode))){
  2011. this.activeNode.setActive(false); // TODO: don't fire events
  2012. }
  2013. if( this.focusNode && (childNode === this.focusNode || this.focusNode.isDescendantOf(childNode))){
  2014. this.focusNode = null;
  2015. }
  2016. // TODO: persist must take care to clear select and expand cookies
  2017. this.nodeRemoveMarkup(subCtx);
  2018. this.nodeRemoveChildren(subCtx);
  2019. idx = $.inArray(childNode, children);
  2020. _assert(idx >= 0);
  2021. // Unlink to support GC
  2022. childNode.visit(function(n){
  2023. n.parent = null;
  2024. }, true);
  2025. if ( opts.removeNode ){
  2026. opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx);
  2027. }
  2028. // remove from child list
  2029. children.splice(idx, 1);
  2030. },
  2031. /**Remove HTML markup for all descendents of ctx.node.
  2032. * @param {EventData} ctx
  2033. */
  2034. nodeRemoveChildMarkup: function(ctx) {
  2035. var node = ctx.node;
  2036. FT.debug("nodeRemoveChildMarkup()", node.toString());
  2037. // TODO: Unlink attr.ftnode to support GC
  2038. if(node.ul){
  2039. $(node.ul).remove();
  2040. node.visit(function(n){
  2041. n.li = n.ul = null;
  2042. });
  2043. node.ul = null;
  2044. }
  2045. },
  2046. /**Remove all descendants of ctx.node.
  2047. * @param {EventData} ctx
  2048. */
  2049. nodeRemoveChildren: function(ctx) {
  2050. var subCtx,
  2051. node = ctx.node,
  2052. children = node.children,
  2053. opts = ctx.options;
  2054. FT.debug("nodeRemoveChildren()", node.toString());
  2055. if(!children){
  2056. return;
  2057. }
  2058. if( this.activeNode && this.activeNode.isDescendantOf(node)){
  2059. this.activeNode.setActive(false); // TODO: don't fire events
  2060. }
  2061. if( this.focusNode && this.focusNode.isDescendantOf(node)){
  2062. this.focusNode = null;
  2063. }
  2064. // TODO: persist must take care to clear select and expand cookies
  2065. this.nodeRemoveChildMarkup(ctx);
  2066. // Unlink children to support GC
  2067. // TODO: also delete this.children (not possible using visit())
  2068. subCtx = $.extend({}, ctx);
  2069. node.visit(function(n){
  2070. n.parent = null;
  2071. if ( opts.removeNode ){
  2072. subCtx.node = n;
  2073. opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx);
  2074. }
  2075. });
  2076. // Set to 'undefined' which is interpreted as 'not yet loaded' for lazy nodes
  2077. node.children = undefined;
  2078. // TODO: ? this._isLoading = false;
  2079. this.nodeRenderStatus(ctx);
  2080. },
  2081. /**Remove HTML markup for ctx.node and all its descendents.
  2082. * @param {EventData} ctx
  2083. */
  2084. nodeRemoveMarkup: function(ctx) {
  2085. var node = ctx.node;
  2086. FT.debug("nodeRemoveMarkup()", node.toString());
  2087. // TODO: Unlink attr.ftnode to support GC
  2088. if(node.li){
  2089. $(node.li).remove();
  2090. node.li = null;
  2091. }
  2092. this.nodeRemoveChildMarkup(ctx);
  2093. },
  2094. /**
  2095. * Create `<li><span>..</span> .. </li>` tags for this node.
  2096. *
  2097. * This method takes care that all HTML markup is created that is required
  2098. * to display this node in it's current state.
  2099. *
  2100. * Call this method to create new nodes, or after the strucuture
  2101. * was changed (e.g. after moving this node or adding/removing children)
  2102. * nodeRenderTitle() and nodeRenderStatus() are implied.
  2103. *
  2104. * Note: if a node was created/removed, nodeRender() must be called for the
  2105. * parent!
  2106. * <code>
  2107. * <li id='KEY' ftnode=NODE>
  2108. * <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'>
  2109. * <span class="fancytree-expander"></span>
  2110. * <span class="fancytree-checkbox"></span> // only present in checkbox mode
  2111. * <span class="fancytree-icon"></span>
  2112. * <a href="#" class="fancytree-title"> Node 1 </a>
  2113. * </span>
  2114. * <ul> // only present if node has children
  2115. * <li id='KEY' ftnode=NODE> child1 ... </li>
  2116. * <li id='KEY' ftnode=NODE> child2 ... </li>
  2117. * </ul>
  2118. * </li>
  2119. * </code>
  2120. *
  2121. * @param: {EventData} ctx
  2122. * @param: {Boolean} [force=false] re-render, even if html markup was already created
  2123. * @param: {Boolean} [deep=false] also render all descendants, even if parent is collapsed
  2124. * @param: {Boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later
  2125. */
  2126. nodeRender: function(ctx, force, deep, collapsed, _recursive) {
  2127. /* This method must take care of all cases where the current data mode
  2128. * (i.e. node hierarchy) does not match the current markup.
  2129. *
  2130. * - node was not yet rendered:
  2131. * create markup
  2132. * - node was rendered: exit fast
  2133. * - children have been added
  2134. * - childern have been removed
  2135. */
  2136. var childLI, childNode1, childNode2, i, l, subCtx,
  2137. node = ctx.node,
  2138. tree = ctx.tree,
  2139. opts = ctx.options,
  2140. aria = opts.aria,
  2141. firstTime = false,
  2142. parent = node.parent,
  2143. isRootNode = !parent,
  2144. children = node.children;
  2145. // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString());
  2146. _assert(isRootNode || parent.ul, "parent UL must exist");
  2147. // Render the node
  2148. if( !isRootNode ){
  2149. // Discard markup on force-mode, or if it is not linked to parent <ul>
  2150. if(node.li && (force || (node.li.parentNode !== node.parent.ul) ) ){
  2151. if(node.li.parentNode !== node.parent.ul){
  2152. // alert("unlink " + node + " (must be child of " + node.parent + ")");
  2153. this.warn("unlink " + node + " (must be child of " + node.parent + ")");
  2154. }
  2155. // this.debug("nodeRemoveMarkup...");
  2156. this.nodeRemoveMarkup(ctx);
  2157. }
  2158. // Create <li><span /> </li>
  2159. // node.debug("render...");
  2160. if( !node.li ) {
  2161. // node.debug("render... really");
  2162. firstTime = true;
  2163. node.li = document.createElement("li");
  2164. node.li.ftnode = node;
  2165. if(aria){
  2166. // TODO: why doesn't this work:
  2167. // node.li.role = "treeitem";
  2168. // $(node.li).attr("role", "treeitem")
  2169. // .attr("aria-labelledby", "ftal_" + node.key);
  2170. }
  2171. if( node.key && opts.generateIds ){
  2172. node.li.id = opts.idPrefix + node.key;
  2173. }
  2174. node.span = document.createElement("span");
  2175. node.span.className = "fancytree-node";
  2176. if(aria){
  2177. $(node.span).attr("aria-labelledby", "ftal_" + node.key);
  2178. }
  2179. node.li.appendChild(node.span);
  2180. // Note: we don't add the LI to the DOM know, but only after we
  2181. // added all sub elements (hoping that this performs better since
  2182. // the browser only have to render once)
  2183. // TODO: benchmarks to prove this
  2184. // parent.ul.appendChild(node.li);
  2185. // Create inner HTML for the <span> (expander, checkbox, icon, and title)
  2186. this.nodeRenderTitle(ctx);
  2187. // Allow tweaking and binding, after node was created for the first time
  2188. // tree._triggerNodeEvent("createNode", ctx);
  2189. if ( opts.createNode ){
  2190. opts.createNode.call(tree, {type: "createNode"}, ctx);
  2191. }
  2192. }else{
  2193. // this.nodeRenderTitle(ctx);
  2194. }
  2195. // Allow tweaking after node state was rendered
  2196. // tree._triggerNodeEvent("renderNode", ctx);
  2197. if ( opts.renderNode ){
  2198. opts.renderNode.call(tree, {type: "renderNode"}, ctx);
  2199. }
  2200. }
  2201. // Visit child nodes
  2202. if( children ){
  2203. if( isRootNode || node.expanded || deep === true ) {
  2204. // Create a UL to hold the children
  2205. if( !node.ul ){
  2206. node.ul = document.createElement("ul");
  2207. if((collapsed === true && !_recursive) || !node.expanded){
  2208. // hide top UL, so we can use an animation to show it later
  2209. node.ul.style.display = "none";
  2210. }
  2211. if(aria){
  2212. $(node.ul).attr("role", "group");
  2213. }
  2214. node.li.appendChild(node.ul);
  2215. }
  2216. // Add child markup
  2217. for(i=0, l=children.length; i<l; i++) {
  2218. subCtx = $.extend({}, ctx, {node: children[i]});
  2219. this.nodeRender(subCtx, force, deep, false, true);
  2220. }
  2221. // Make sure, that <li> order matches node.children order.
  2222. // this.nodeFixOrder(ctx);
  2223. childLI = node.ul.firstChild;
  2224. for(i=0, l=children.length-1; i<l; i++) {
  2225. childNode1 = children[i];
  2226. childNode2 = childLI.ftnode;
  2227. if( childNode1 !== childNode2 ) {
  2228. node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2);
  2229. node.ul.insertBefore(childNode1.li, childNode2.li);
  2230. } else {
  2231. childLI = childLI.nextSibling;
  2232. }
  2233. }
  2234. // TODO: need to check, if node.ul has <li>s, that are not in node.children[] ?
  2235. }
  2236. }else{
  2237. // No children: remove markup if any
  2238. if( node.ul ){
  2239. // alert("remove child markup for " + node);
  2240. this.warn("remove child markup for " + node);
  2241. this.nodeRemoveChildMarkup(ctx);
  2242. }
  2243. }
  2244. if( !isRootNode ){
  2245. // Update element classes according to node state
  2246. this.nodeRenderStatus(ctx);
  2247. // Finally add the whole structure to the DOM, so the browser can render
  2248. if(firstTime){
  2249. parent.ul.appendChild(node.li);
  2250. }
  2251. }
  2252. return;
  2253. },
  2254. /** Create HTML for the node's outer <span> (expander, checkbox, icon, and title).
  2255. * @param {EventData} ctx
  2256. */
  2257. nodeRenderTitle: function(ctx, title) {
  2258. // set node connector images, links and text
  2259. var id, imageSrc, nodeTitle, role, tooltip,
  2260. node = ctx.node,
  2261. tree = ctx.tree,
  2262. opts = ctx.options,
  2263. aria = opts.aria,
  2264. level = node.getLevel(),
  2265. ares = [],
  2266. icon = node.data.icon;
  2267. if(title !== undefined){
  2268. node.title = title;
  2269. }
  2270. if(!node.span){
  2271. // Silently bail out if node was not rendered yet, assuming
  2272. // node.render() will be called as the node becomes visible
  2273. return;
  2274. }
  2275. // connector (expanded, expandable or simple)
  2276. // TODO: optiimize this if clause
  2277. if( level < opts.minExpandLevel ) {
  2278. if(level > 1){
  2279. if(aria){
  2280. ares.push("<span role='button' class='fancytree-expander'></span>");
  2281. }else{
  2282. ares.push("<span class='fancytree-expander'></span>");
  2283. }
  2284. }
  2285. // .. else (i.e. for root level) skip expander/connector alltogether
  2286. } else {
  2287. if(aria){
  2288. ares.push("<span role='button' class='fancytree-expander'></span>");
  2289. }else{
  2290. ares.push("<span class='fancytree-expander'></span>");
  2291. }
  2292. }
  2293. // Checkbox mode
  2294. if( opts.checkbox && node.hideCheckbox !== true && !node.isStatusNode ) {
  2295. if(aria){
  2296. ares.push("<span role='checkbox' class='fancytree-checkbox'></span>");
  2297. }else{
  2298. ares.push("<span class='fancytree-checkbox'></span>");
  2299. }
  2300. }
  2301. // folder or doctype icon
  2302. role = aria ? " role='img'" : "";
  2303. if ( icon && typeof icon === "string" ) {
  2304. imageSrc = (icon.charAt(0) === "/") ? icon : (opts.imagePath + icon);
  2305. ares.push("<img src='" + imageSrc + "' alt='' />");
  2306. } else if ( node.data.iconclass ) {
  2307. // TODO: review and test and document
  2308. ares.push("<span " + role + " class='fancytree-custom-icon" + " " + node.data.iconclass + "'></span>");
  2309. } else if ( icon === true || (icon !== false && opts.icons !== false) ) {
  2310. // opts.icons defines the default behavior.
  2311. // node.icon == true/false can override this
  2312. ares.push("<span " + role + " class='fancytree-icon'></span>");
  2313. }
  2314. // node title
  2315. nodeTitle = "";
  2316. // TODO: currently undocumented; may be removed?
  2317. if ( opts.renderTitle ){
  2318. nodeTitle = opts.renderTitle.call(tree, {type: "renderTitle"}, ctx) || "";
  2319. }
  2320. if(!nodeTitle){
  2321. // TODO: escape tooltip string
  2322. tooltip = node.tooltip ? " title='" + node.tooltip.replace(/\"/g, "&quot;") + "'" : "";
  2323. id = aria ? " id='ftal_" + node.key + "'" : "";
  2324. role = aria ? " role='treeitem'" : "";
  2325. // href = node.data.href || "#";
  2326. // if( opts.nolink || node.nolink ) {
  2327. // nodeTitle = "<span role='treeitem' tabindex='-1' class='fancytree-title'" + id + tooltip + ">" + node.title + "</span>";
  2328. nodeTitle = "<span " + role + " class='fancytree-title'" + id + tooltip + ">" + node.title + "</span>";
  2329. // } else {
  2330. // nodeTitle = "<a href='" + href + "' tabindex='-1' class='fancytree-title'" + tooltip + ">" + node.title + "</a>";
  2331. // }
  2332. }
  2333. ares.push(nodeTitle);
  2334. // Note: this will trigger focusout, if node had the focus
  2335. node.span.innerHTML = ares.join("");
  2336. },
  2337. /** Update element classes according to node state.
  2338. * @param {EventData} ctx
  2339. */
  2340. nodeRenderStatus: function(ctx) {
  2341. // Set classes for current status
  2342. var node = ctx.node,
  2343. tree = ctx.tree,
  2344. opts = ctx.options,
  2345. // nodeContainer = node[tree.nodeContainerAttrName],
  2346. hasChildren = node.hasChildren(),
  2347. isLastSib = node.isLastSibling(),
  2348. aria = opts.aria,
  2349. // $ariaElem = aria ? $(node[tree.ariaPropName]) : null,
  2350. $ariaElem = $(node.span).find(".fancytree-title"),
  2351. cn = opts._classNames,
  2352. cnList = [],
  2353. statusElem = node[tree.statusClassPropName];
  2354. if( !statusElem ){
  2355. // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway)
  2356. return;
  2357. }
  2358. // Build a list of class names that we will add to the node <span>
  2359. cnList.push(cn.node);
  2360. if( tree.activeNode === node ){
  2361. cnList.push(cn.active);
  2362. // $(">span.fancytree-title", statusElem).attr("tabindex", "0");
  2363. // tree.$container.removeAttr("tabindex");
  2364. }else{
  2365. // $(">span.fancytree-title", statusElem).removeAttr("tabindex");
  2366. // tree.$container.attr("tabindex", "0");
  2367. }
  2368. if( tree.focusNode === node ){
  2369. cnList.push(cn.focused);
  2370. if(aria){
  2371. // $(">span.fancytree-title", statusElem).attr("tabindex", "0");
  2372. // $(">span.fancytree-title", statusElem).attr("tabindex", "-1");
  2373. // TODO: is this the right element for this attribute?
  2374. $ariaElem
  2375. .attr("aria-activedescendant", true);
  2376. // .attr("tabindex", "-1");
  2377. }
  2378. }else if(aria){
  2379. // $(">span.fancytree-title", statusElem).attr("tabindex", "-1");
  2380. $ariaElem
  2381. .removeAttr("aria-activedescendant");
  2382. // .removeAttr("tabindex");
  2383. }
  2384. if( node.expanded ){
  2385. cnList.push(cn.expanded);
  2386. if(aria){
  2387. $ariaElem.attr("aria-expanded", true);
  2388. }
  2389. }else if(aria){
  2390. $ariaElem.removeAttr("aria-expanded");
  2391. }
  2392. if( node.folder ){
  2393. cnList.push(cn.folder);
  2394. }
  2395. if( hasChildren !== false ){
  2396. cnList.push(cn.hasChildren);
  2397. }
  2398. // TODO: required?
  2399. if( isLastSib ){
  2400. cnList.push(cn.lastsib);
  2401. }
  2402. if( node.lazy && node.children === null ){
  2403. cnList.push(cn.lazy);
  2404. }
  2405. if( node.partsel ){
  2406. cnList.push(cn.partsel);
  2407. }
  2408. if( node.selected ){
  2409. cnList.push(cn.selected);
  2410. if(aria){
  2411. $ariaElem.attr("aria-selected", true);
  2412. }
  2413. }else if(aria){
  2414. $ariaElem.attr("aria-selected", false);
  2415. }
  2416. if( node.extraClasses ){
  2417. cnList.push(node.extraClasses);
  2418. }
  2419. // IE6 doesn't correctly evaluate multiple class names,
  2420. // so we create combined class names that can be used in the CSS
  2421. if( hasChildren === false ){
  2422. cnList.push(cn.combinedExpanderPrefix + "n" +
  2423. (isLastSib ? "l" : "")
  2424. );
  2425. }else{
  2426. cnList.push(cn.combinedExpanderPrefix +
  2427. (node.expanded ? "e" : "c") +
  2428. (node.lazy && node.children === null ? "d" : "") +
  2429. (isLastSib ? "l" : "")
  2430. );
  2431. }
  2432. cnList.push(cn.combinedIconPrefix +
  2433. (node.expanded ? "e" : "c") +
  2434. (node.folder ? "f" : "")
  2435. );
  2436. // node.span.className = cnList.join(" ");
  2437. node[tree.statusClassPropName].className = cnList.join(" ");
  2438. // TODO: we should not set this in the <span> tag also, if we set it here:
  2439. // Maybe most (all) of the classes should be set in LI instead of SPAN?
  2440. if(node.li){
  2441. node.li.className = isLastSib ? cn.lastsib : "";
  2442. }
  2443. },
  2444. /** Activate node.
  2445. * flag defaults to true.
  2446. * If flag is true, the node is activated (must be a synchronous operation)
  2447. * If flag is false, the node is deactivated (must be a synchronous operation)
  2448. * @param {EventData} ctx
  2449. * @param {Boolean} [flag=true]
  2450. */
  2451. nodeSetActive: function(ctx, flag) {
  2452. // Handle user click / [space] / [enter], according to clickFolderMode.
  2453. var subCtx,
  2454. node = ctx.node,
  2455. tree = ctx.tree,
  2456. opts = ctx.options,
  2457. // userEvent = !!ctx.originalEvent,
  2458. isActive = (node === tree.activeNode);
  2459. // flag defaults to true
  2460. flag = (flag !== false);
  2461. node.debug("nodeSetActive", flag);
  2462. if(isActive === flag){
  2463. // Nothing to do
  2464. return _getResolvedPromise(node);
  2465. }else if(flag && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){
  2466. // Callback returned false
  2467. return _getRejectedPromise(node, ["rejected"]);
  2468. }
  2469. if(flag){
  2470. if(tree.activeNode){
  2471. _assert(tree.activeNode !== node, "node was active (inconsistency)");
  2472. subCtx = $.extend({}, ctx, {node: tree.activeNode});
  2473. tree.nodeSetActive(subCtx, false);
  2474. _assert(tree.activeNode === null, "deactivate was out of sync?");
  2475. }
  2476. if(opts.activeVisible){
  2477. tree.nodeMakeVisible(ctx);
  2478. }
  2479. tree.activeNode = node;
  2480. tree.nodeRenderStatus(ctx);
  2481. tree.nodeSetFocus(ctx);
  2482. tree._triggerNodeEvent("activate", node);
  2483. }else{
  2484. _assert(tree.activeNode === node, "node was not active (inconsistency)");
  2485. tree.activeNode = null;
  2486. this.nodeRenderStatus(ctx);
  2487. ctx.tree._triggerNodeEvent("deactivate", node);
  2488. }
  2489. },
  2490. /** Expand or collapse node, return Deferred.promise.
  2491. *
  2492. * @param {EventData} ctx
  2493. * @param {Boolean} [flag=true]
  2494. * @returns {$.Promise} The deferred will be resolved as soon as the (lazy)
  2495. * data was retrieved, rendered, and the expand animation finshed.
  2496. */
  2497. nodeSetExpanded: function(ctx, flag) {
  2498. var _afterLoad, dfd, i, l, parents, prevAC,
  2499. node = ctx.node,
  2500. tree = ctx.tree,
  2501. opts = ctx.options;
  2502. // flag defaults to true
  2503. flag = (flag !== false);
  2504. node.debug("nodeSetExpanded(" + flag + ")");
  2505. // TODO: !!node.expanded is nicer, but doesn't pass jshint
  2506. // https://github.com/jshint/jshint/issues/455
  2507. // if( !!node.expanded === !!flag){
  2508. if((node.expanded && flag) || (!node.expanded && !flag)){
  2509. // Nothing to do
  2510. node.debug("nodeSetExpanded(" + flag + "): nothing to do");
  2511. return _getResolvedPromise(node);
  2512. }else if(flag && !node.lazy && !node.hasChildren() ){
  2513. // Prevent expanding of empty nodes
  2514. return _getRejectedPromise(node, ["empty"]);
  2515. }else if( !flag && node.getLevel() < opts.minExpandLevel ) {
  2516. // Prevent collapsing locked levels
  2517. return _getRejectedPromise(node, ["locked"]);
  2518. }else if ( this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){
  2519. // Callback returned false
  2520. return _getRejectedPromise(node, ["rejected"]);
  2521. }
  2522. //
  2523. dfd = new $.Deferred();
  2524. // Auto-collapse mode: collapse all siblings
  2525. if( flag && !node.expanded && opts.autoCollapse ) {
  2526. parents = node.getParentList(false, true);
  2527. prevAC = opts.autoCollapse;
  2528. try{
  2529. opts.autoCollapse = false;
  2530. for(i=0, l=parents.length; i<l; i++){
  2531. // TODO: should return promise?
  2532. this._callHook("nodeCollapseSiblings", parents[i]);
  2533. }
  2534. }finally{
  2535. opts.autoCollapse = prevAC;
  2536. }
  2537. }
  2538. // Trigger expand/collapse after expanding
  2539. dfd.done(function(){
  2540. ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
  2541. if(opts.autoScroll){
  2542. // Scroll down to last child, but keep current node visible
  2543. node.getLastChild().scrollIntoView(true, node);
  2544. }
  2545. });
  2546. // vvv Code below is executed after loading finished:
  2547. _afterLoad = function(){
  2548. var duration, easing, isVisible, isExpanded;
  2549. node.expanded = flag;
  2550. // Create required markup, but make sure the top UL is hidden, so we
  2551. // can animate later
  2552. tree._callHook("nodeRender", ctx, false, false, true);
  2553. // If the currently active node is now hidden, deactivate it
  2554. // if( opts.activeVisible && this.activeNode && ! this.activeNode.isVisible() ) {
  2555. // this.activeNode.deactivate();
  2556. // }
  2557. // Expanding a lazy node: set 'loading...' and call callback
  2558. // if( bExpand && this.data.isLazy && this.childList === null && !this._isLoading ) {
  2559. // this._loadContent();
  2560. // return;
  2561. // }
  2562. // Hide children, if node is collapsed
  2563. if( node.ul ) {
  2564. isVisible = (node.ul.style.display !== "none");
  2565. isExpanded = !!node.expanded;
  2566. // _assert(isVisible !== isExpanded);
  2567. if( isVisible === isExpanded ) {
  2568. node.warn("nodeSetExpanded: UL.style.display already set");
  2569. dfd.resolveWith(node);
  2570. } else if( opts.fx ) {
  2571. duration = opts.fx.duration || 200;
  2572. easing = opts.fx.easing;
  2573. node.debug("nodeSetExpanded: animate start...");
  2574. $(node.ul).animate(opts.fx, duration, easing, function(){
  2575. node.debug("nodeSetExpanded: animate done");
  2576. dfd.resolveWith(node);
  2577. });
  2578. } else {
  2579. node.ul.style.display = ( node.expanded || !parent ) ? "" : "none";
  2580. dfd.resolveWith(node);
  2581. }
  2582. }else{
  2583. dfd.resolveWith(node);
  2584. }
  2585. };
  2586. // ^^^ Code above is executed after loading finshed.
  2587. // Load lazy nodes, if any. Then continue with _afterLoad()
  2588. if(flag && node.lazy && node.hasChildren() === undefined){
  2589. node.debug("nodeSetExpanded: load start...");
  2590. node.lazyLoad().done(function(){
  2591. node.debug("nodeSetExpanded: load done");
  2592. if(dfd.notifyWith){ // requires jQuery 1.6+
  2593. dfd.notifyWith(node, ["loaded"]);
  2594. }
  2595. _afterLoad.call(tree);
  2596. }).fail(function(errMsg){
  2597. dfd.rejectWith(node, ["load failed (" + errMsg + ")"]);
  2598. });
  2599. /*
  2600. var source = tree._triggerNodeEvent("lazyload", node, ctx.originalEvent);
  2601. _assert(typeof source !== "boolean", "lazyload event must return source in data.result");
  2602. node.debug("nodeSetExpanded: load start...");
  2603. this._callHook("nodeLoadChildren", ctx, source).done(function(){
  2604. node.debug("nodeSetExpanded: load done");
  2605. if(dfd.notifyWith){ // requires jQuery 1.6+
  2606. dfd.notifyWith(node, ["loaded"]);
  2607. }
  2608. _afterLoad.call(tree);
  2609. }).fail(function(errMsg){
  2610. dfd.rejectWith(node, ["load failed (" + errMsg + ")"]);
  2611. });
  2612. */
  2613. }else{
  2614. _afterLoad();
  2615. }
  2616. node.debug("nodeSetExpanded: returns");
  2617. return dfd.promise();
  2618. },
  2619. /**
  2620. * @param {EventData} ctx
  2621. * @param {Boolean} [flag=true]
  2622. */
  2623. nodeSetFocus: function(ctx, flag) {
  2624. ctx.node.debug("nodeSetFocus(" + flag + ")");
  2625. var ctx2,
  2626. tree = ctx.tree,
  2627. node = ctx.node;
  2628. flag = (flag !== false);
  2629. // Blur previous node if any
  2630. if(tree.focusNode){
  2631. if(tree.focusNode === node && flag){
  2632. node.debug("nodeSetFocus(" + flag + "): nothing to do");
  2633. return;
  2634. }
  2635. ctx2 = $.extend({}, ctx, {node: tree.focusNode});
  2636. tree.focusNode = null;
  2637. this._triggerNodeEvent("blur", ctx2);
  2638. this._callHook("nodeRenderStatus", ctx2);
  2639. }
  2640. // Set focus to container and node
  2641. if(flag){
  2642. if(FT.focusTree !== tree){
  2643. node.debug("nodeSetFocus: forcing container focus");
  2644. // Note: we pass _calledByNodeSetFocus=true
  2645. this._callHook("treeSetFocus", ctx, true, true);
  2646. }
  2647. this.nodeMakeVisible(ctx);
  2648. tree.focusNode = node;
  2649. // node.debug("FOCUS...");
  2650. // $(node.span).find(".fancytree-title").focus();
  2651. this._triggerNodeEvent("focus", ctx);
  2652. // if(ctx.options.autoActivate){
  2653. // tree.nodeSetActive(ctx, true);
  2654. // }
  2655. if(ctx.options.autoScroll){
  2656. node.scrollIntoView();
  2657. }
  2658. this._callHook("nodeRenderStatus", ctx);
  2659. }
  2660. },
  2661. /** (De)Select node, return new status (sync).
  2662. *
  2663. * @param {EventData} ctx
  2664. * @param {Boolean} [flag=true]
  2665. */
  2666. nodeSetSelected: function(ctx, flag) {
  2667. var node = ctx.node,
  2668. tree = ctx.tree,
  2669. opts = ctx.options;
  2670. // flag defaults to true
  2671. flag = (flag !== false);
  2672. node.debug("nodeSetSelected(" + flag + ")", ctx);
  2673. if( node.unselectable){
  2674. return;
  2675. }
  2676. // TODO: !!node.expanded is nicer, but doesn't pass jshint
  2677. // https://github.com/jshint/jshint/issues/455
  2678. // if( !!node.expanded === !!flag){
  2679. if((node.selected && flag) || (!node.selected && !flag)){
  2680. return !!node.selected;
  2681. }else if ( this._triggerNodeEvent("beforeSelect", node, ctx.originalEvent) === false ){
  2682. return !!node.selected;
  2683. }
  2684. if(flag && opts.selectMode === 1){
  2685. // single selection mode
  2686. if(tree.lastSelectedNode){
  2687. tree.lastSelectedNode.setSelected(false);
  2688. }
  2689. }else if(opts.selectMode === 3){
  2690. // multi.hier selection mode
  2691. node.selected = flag;
  2692. // this._fixSelectionState(node);
  2693. node.fixSelection3AfterClick();
  2694. }
  2695. node.selected = flag;
  2696. this.nodeRenderStatus(ctx);
  2697. tree.lastSelectedNode = flag ? node : null;
  2698. tree._triggerNodeEvent("select", ctx);
  2699. },
  2700. /** Show node status (ok, loading, error) using styles and a dummy child node.
  2701. *
  2702. * @param {EventData} ctx
  2703. * @param status
  2704. * @param message
  2705. * @param details
  2706. */
  2707. nodeSetStatus: function(ctx, status, message, details) {
  2708. var _clearStatusNode, _setStatusNode,
  2709. node = ctx.node,
  2710. tree = ctx.tree;
  2711. _clearStatusNode = function() {
  2712. var firstChild = ( node.children ? node.children[0] : null );
  2713. if ( firstChild && firstChild.isStatusNode ) {
  2714. try{
  2715. // I've seen exceptions here with loadKeyPath...
  2716. if(node.ul){
  2717. node.ul.removeChild(firstChild.li);
  2718. firstChild.li = null; // avoid leaks (issue 215)
  2719. }
  2720. }catch(e){}
  2721. if( node.children.length === 1 ){
  2722. node.children = [];
  2723. }else{
  2724. node.children.shift();
  2725. }
  2726. }
  2727. return;
  2728. };
  2729. _setStatusNode = function(data) {
  2730. var firstChild = ( node.children ? node.children[0] : null );
  2731. if ( firstChild && firstChild.isStatusNode ) {
  2732. $.extend(firstChild, data);
  2733. tree._callHook("nodeRender", firstChild);
  2734. } else {
  2735. data.key = "_statusNode";
  2736. node._setChildren([data]);
  2737. node.children[0].isStatusNode = true;
  2738. tree.render();
  2739. }
  2740. return node.children[0];
  2741. };
  2742. switch(status){
  2743. case "ok":
  2744. _clearStatusNode();
  2745. $(node.span).removeClass("fancytree-loading");
  2746. break;
  2747. case "loading":
  2748. $(node.span).addClass("fancytree-loading");
  2749. if(!node.parent){
  2750. _setStatusNode({
  2751. title: tree.options.strings.loading +
  2752. (message ? " (" + message + ") " : ""),
  2753. tooltip: details,
  2754. extraClasses: "fancytree-statusnode-wait"
  2755. });
  2756. }
  2757. break;
  2758. case "error":
  2759. $(node.span).addClass("fancytree-error");
  2760. _setStatusNode({
  2761. title: tree.options.strings.loadError + " (" + message + ")",
  2762. tooltip: details,
  2763. extraClasses: "fancytree-statusnode-error"
  2764. });
  2765. break;
  2766. default:
  2767. $.error("invalid status " + status);
  2768. }
  2769. },
  2770. /**
  2771. *
  2772. * @param {EventData} ctx
  2773. */
  2774. nodeToggleExpanded: function(ctx) {
  2775. return this.nodeSetExpanded(ctx, !ctx.node.expanded);
  2776. },
  2777. /**
  2778. * @param {EventData} ctx
  2779. */
  2780. nodeToggleSelected: function(ctx) {
  2781. return this.nodeSetSelected(ctx, !ctx.node.selected);
  2782. },
  2783. /** Remove all nodes.
  2784. * @param {EventData} ctx
  2785. */
  2786. treeClear: function(ctx) {
  2787. var tree = ctx.tree;
  2788. tree.activeNode = null;
  2789. tree.focusNode = null;
  2790. tree.$div.find(">ul.fancytree-container").empty();
  2791. // TODO: call destructors and remove reference loops
  2792. tree.rootNode.children = null;
  2793. },
  2794. /** Widget was created (called only once, even it re-initialized).
  2795. * @param {EventData} ctx
  2796. */
  2797. treeCreate: function(ctx) {
  2798. },
  2799. /** Widget was destroyed.
  2800. * @param {EventData} ctx
  2801. */
  2802. treeDestroy: function(ctx) {
  2803. },
  2804. /** Widget was (re-)initialized.
  2805. * @param {EventData} ctx
  2806. */
  2807. treeInit: function(ctx) {
  2808. this.treeLoad(ctx);
  2809. },
  2810. /** Parse Fancytree from source, as configured in the options.
  2811. * @param {EventData} ctx
  2812. * @param {object} [source] new source
  2813. */
  2814. treeLoad: function(ctx, source) {
  2815. var type, $ul,
  2816. tree = ctx.tree,
  2817. $container = ctx.widget.element,
  2818. dfd,
  2819. // calling context for root node
  2820. rootCtx = $.extend({}, ctx, {node: this.rootNode});
  2821. if(tree.rootNode.children){
  2822. this.treeClear(ctx);
  2823. }
  2824. source = source || this.options.source;
  2825. if(!source){
  2826. type = $container.data("type") || "html";
  2827. switch(type){
  2828. case "html":
  2829. $ul = $container.find(">ul:first");
  2830. $ul.addClass("ui-fancytree-source ui-helper-hidden");
  2831. source = $.ui.fancytree.parseHtml($ul);
  2832. break;
  2833. case "json":
  2834. // $().addClass("ui-helper-hidden");
  2835. source = $.parseJSON($container.text());
  2836. if(source.children){
  2837. if(source.title){tree.title = source.title;}
  2838. source = source.children;
  2839. }
  2840. break;
  2841. default:
  2842. $.error("Invalid data-type: " + type);
  2843. }
  2844. }else if(typeof source === "string"){
  2845. // TODO: source is an element ID
  2846. _raiseNotImplemented();
  2847. }
  2848. // $container.addClass("ui-widget ui-widget-content ui-corner-all");
  2849. // Trigger fancytreeinit after nodes have been loaded
  2850. dfd = this.nodeLoadChildren(rootCtx, source).done(function(){
  2851. tree.render();
  2852. if( ctx.options.selectMode === 3 ){
  2853. tree.rootNode.fixSelection3FromEndNodes();
  2854. }
  2855. tree._triggerTreeEvent("init", true);
  2856. }).fail(function(){
  2857. tree.render();
  2858. tree._triggerTreeEvent("init", false);
  2859. });
  2860. return dfd;
  2861. },
  2862. /* Handle focus and blur events for the container (also fired for child elements). */
  2863. treeOnFocusInOut: function(event) {
  2864. var flag = (event.type === "focusin"),
  2865. node = $.ui.fancytree.getNode(event);
  2866. try{
  2867. this.debug("treeOnFocusInOut(" + flag + "), node=", node);
  2868. _assert(!this._inFocusHandler, "Focus handler recursion");
  2869. this.systemFocusElement = flag ? event.target : null;
  2870. this._inFocusHandler = true;
  2871. if(node){
  2872. // For example clicking into an <input> that is part of a node
  2873. this._callHook("nodeSetFocus", node, flag);
  2874. }else{
  2875. this._callHook("treeSetFocus", this, flag);
  2876. }
  2877. }finally{
  2878. this._inFocusHandler = false;
  2879. }
  2880. },
  2881. /* */
  2882. treeSetFocus: function(ctx, flag, _calledByNodeSetFocus) {
  2883. flag = (flag !== false);
  2884. this.debug("treeSetFocus(" + flag + "), _calledByNodeSetFocus: " + _calledByNodeSetFocus);
  2885. this.debug(" focusNode: " + this.focusNode);
  2886. this.debug(" activeNode: " + this.activeNode);
  2887. // Blur previous tree if any
  2888. if(FT.focusTree){
  2889. if(this !== FT.focusTree || !flag ){
  2890. // prev. node looses focus, if prev. tree blurs
  2891. if(FT.focusTree.focusNode){
  2892. FT.focusTree.focusNode.setFocus(false);
  2893. }
  2894. FT.focusTree.$container.removeClass("fancytree-focused");
  2895. this._triggerTreeEvent("blurTree");
  2896. FT.focusTree = null;
  2897. }
  2898. }
  2899. //
  2900. if( flag && FT.focusTree !== this ){
  2901. FT.focusTree = this;
  2902. this.$container.addClass("fancytree-focused");
  2903. // Make sure container gets `:focus` when we clicked inside
  2904. if( !this.systemFocusElement ){
  2905. this.debug("Set `:focus` to container");
  2906. this.$container.focus();
  2907. }
  2908. // Set focus to a node
  2909. if( ! this.focusNode && !_calledByNodeSetFocus){
  2910. if( this.activeNode ){
  2911. this.activeNode.setFocus();
  2912. }else if( this.rootNode.hasChildren()){
  2913. this.warn("NOT setting focus to first child");
  2914. // this.rootNode.getFirstChild().setFocus();
  2915. }
  2916. }
  2917. this._triggerTreeEvent("focusTree");
  2918. }else{
  2919. FT.focusTree = null;
  2920. }
  2921. },
  2922. /** Re-fire beforeActivate and activate events. */
  2923. reactivate: function(setFocus) {
  2924. var node = this.activeNode;
  2925. if( node ) {
  2926. this.activeNode = null; // Force re-activating
  2927. node.setActive();
  2928. if( setFocus ){
  2929. node.setFocus();
  2930. }
  2931. }
  2932. },
  2933. // TODO: redraw()
  2934. /** Reload tree from source and return a promise.
  2935. * @param source
  2936. * @returns {$.Promise}
  2937. */
  2938. reload: function(source) {
  2939. this._callHook("treeClear", this);
  2940. return this._callHook("treeLoad", this, source);
  2941. },
  2942. /**Render tree (i.e. all top-level nodes).
  2943. * @param {Boolean} [force=false]
  2944. * @param {Boolean} [deep=false]
  2945. */
  2946. render: function(force, deep) {
  2947. return this.rootNode.render(force, deep);
  2948. },
  2949. // TODO: selectKey: function(key, select)
  2950. // TODO: serializeArray: function(stopOnParents)
  2951. /**
  2952. * @param {Boolean} [flag=true]
  2953. */
  2954. setFocus: function(flag) {
  2955. // _assert(false, "Not implemented");
  2956. return this._callHook("treeSetFocus", this, flag);
  2957. },
  2958. /**
  2959. * Return all nodes as nested list of {@link NodeData}.
  2960. *
  2961. * @param {Boolean} [includeRoot=false] Returns the hidden system root node (and it's children)
  2962. * @param {function} [callback] Called for every node
  2963. * @returns {Array | object}
  2964. * @see FancytreeNode#toDict
  2965. */
  2966. toDict: function(includeRoot, callback){
  2967. var res = this.rootNode.toDict(true, callback);
  2968. return includeRoot ? res : res.children;
  2969. },
  2970. /**Implicitly called for string conversions.
  2971. * @returns {String}
  2972. */
  2973. toString: function(){
  2974. return "<Fancytree(#" + this._id + ")>";
  2975. },
  2976. /** _trigger a widget event with additional node ctx.
  2977. * @see EventData
  2978. */
  2979. _triggerNodeEvent: function(type, node, originalEvent, extra) {
  2980. // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx);
  2981. var res,
  2982. ctx = this._makeHookContext(node, originalEvent);
  2983. if( extra ) {
  2984. $.extend(ctx, extra);
  2985. }
  2986. res = this.widget._trigger(type, originalEvent, ctx);
  2987. if(res !== false && ctx.result !== undefined){
  2988. return ctx.result;
  2989. }
  2990. return res;
  2991. },
  2992. /** _trigger a widget event with additional tree data. */
  2993. _triggerTreeEvent: function(type, originalEvent) {
  2994. // this.debug("_trigger(" + type + ")", ctx);
  2995. var ctx = this._makeHookContext(this, originalEvent),
  2996. res = this.widget._trigger(type, originalEvent, ctx);
  2997. if(res !== false && ctx.result !== undefined){
  2998. return ctx.result;
  2999. }
  3000. return res;
  3001. },
  3002. /**
  3003. *
  3004. * @param {function} fn
  3005. */
  3006. visit: function(fn) {
  3007. return this.rootNode.visit(fn, false);
  3008. },
  3009. /** Write warning to browser console (prepending tree info)
  3010. *
  3011. * @param {*} msg string or object or array of such
  3012. */
  3013. warn: function(msg){
  3014. Array.prototype.unshift.call(arguments, this.toString());
  3015. consoleApply("warn", arguments);
  3016. }
  3017. };
  3018. /* ******************************************************************************
  3019. * jQuery UI widget boilerplate
  3020. * @ name ui_fancytree
  3021. * @ class The jQuery.ui.fancytree widget
  3022. */
  3023. /* * @namespace ui */
  3024. /* * @namespace ui.fancytree */
  3025. /** @namespace $.ui.fancytree */
  3026. $.widget("ui.fancytree",
  3027. /** @lends $.ui.fancytree.prototype */
  3028. {
  3029. /**These options will be used as defaults
  3030. * @type {FancytreeOptions}
  3031. */
  3032. options:
  3033. {
  3034. /** @type {Boolean} Make sure, active nodes are visible (expanded). */
  3035. activeVisible: true,
  3036. ajax: {
  3037. type: "GET",
  3038. cache: false, // false: Append random '_' argument to the request url to prevent caching.
  3039. // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable
  3040. dataType: "json" // Expect json format and pass json object to callbacks.
  3041. }, //
  3042. aria: false, // TODO: default to true
  3043. autoActivate: true,
  3044. autoCollapse: false,
  3045. // autoFocus: false,
  3046. autoScroll: false,
  3047. checkbox: false,
  3048. /**defines click behavior*/
  3049. clickFolderMode: 4,
  3050. debugLevel: null, // 0..2 (null: use global setting $.ui.fancytree.debugInfo)
  3051. disabled: false, // TODO: required anymore?
  3052. enableAspx: true, // TODO: document
  3053. extensions: [],
  3054. fx: { height: "toggle", duration: 200 },
  3055. generateIds: false,
  3056. icons: true,
  3057. idPrefix: "ft_",
  3058. keyboard: true,
  3059. keyPathSeparator: "/",
  3060. minExpandLevel: 1,
  3061. selectMode: 2,
  3062. strings: {
  3063. loading: "Loading&#8230;",
  3064. loadError: "Load error!"
  3065. },
  3066. tabbable: true,
  3067. _classNames: {
  3068. // container: "fancytree-container",
  3069. node: "fancytree-node",
  3070. folder: "fancytree-folder",
  3071. // empty: "fancytree-empty",
  3072. // vline: "fancytree-vline",
  3073. // expander: "fancytree-expander",
  3074. // connector: "fancytree-connector",
  3075. // checkbox: "fancytree-checkbox",
  3076. // icon: "fancytree-icon",
  3077. // title: "fancytree-title",
  3078. // noConnector: "fancytree-no-connector",
  3079. // statusnodeError: "fancytree-statusnode-error",
  3080. // statusnodeWait: "fancytree-statusnode-wait",
  3081. // hidden: "fancytree-hidden",
  3082. combinedExpanderPrefix: "fancytree-exp-",
  3083. combinedIconPrefix: "fancytree-ico-",
  3084. // loading: "fancytree-loading",
  3085. hasChildren: "fancytree-has-children",
  3086. active: "fancytree-active",
  3087. selected: "fancytree-selected",
  3088. expanded: "fancytree-expanded",
  3089. lazy: "fancytree-lazy",
  3090. focused: "fancytree-focused ui-state-focus",
  3091. partsel: "fancytree-partsel",
  3092. lastsib: "fancytree-lastsib"
  3093. },
  3094. // events
  3095. lazyload: null,
  3096. postProcess: null
  3097. },
  3098. /* Set up the widget, Called on first $().fancytree() */
  3099. _create: function() {
  3100. this.tree = new Fancytree(this);
  3101. this.$source = this.source || this.element.data("type") === "json" ? this.element
  3102. : this.element.find(">ul:first");
  3103. // Subclass Fancytree instance with all enabled extensions
  3104. var extension, extName, i,
  3105. extensions = this.options.extensions,
  3106. base = this.tree;
  3107. for(i=0; i<extensions.length; i++){
  3108. extName = extensions[i];
  3109. extension = $.ui.fancytree._extensions[extName];
  3110. if(!extension){
  3111. $.error("Could not apply extension '" + extName + "' (it is not registered, did you forget to include it?)");
  3112. }
  3113. // Add extension options as tree.options.EXTENSION
  3114. // _assert(!this.tree.options[extName], "Extension name must not exist as option name: " + extName);
  3115. this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]);
  3116. // Add a namespace tree.EXTENSION, to hold instance data
  3117. _assert(!this.tree[extName], "Extension name must not exist as Fancytree attribute: " + extName);
  3118. // this.tree[extName] = extension;
  3119. this.tree[extName] = {};
  3120. // Subclass Fancytree methods using proxies.
  3121. _subclassObject(this.tree, base, extension, extName);
  3122. // current extension becomes base for the next extension
  3123. base = extension;
  3124. }
  3125. //
  3126. this.tree._callHook("treeCreate", this.tree);
  3127. // Note: 'fancytreecreate' event is fired by widget base class
  3128. // this.tree._triggerTreeEvent("create");
  3129. },
  3130. /* Called on every $().fancytree() */
  3131. _init: function() {
  3132. this.tree._callHook("treeInit", this.tree);
  3133. // TODO: currently we call bind after treeInit, because treeInit
  3134. // might change tree.$container.
  3135. // It would be better, to move ebent binding into hooks altogether
  3136. this._bind();
  3137. },
  3138. /* Use the _setOption method to respond to changes to options */
  3139. _setOption: function(key, value) {
  3140. var callDefault = true,
  3141. rerender = false;
  3142. switch( key ) {
  3143. case "aria":
  3144. case "checkbox":
  3145. case "icons":
  3146. case "minExpandLevel":
  3147. case "tabbable":
  3148. // case "nolink":
  3149. this.tree._callHook("treeCreate", this.tree);
  3150. rerender = true;
  3151. break;
  3152. case "source":
  3153. callDefault = false;
  3154. this.tree._callHook("treeLoad", this.tree, value);
  3155. break;
  3156. }
  3157. this.tree.debug("set option " + key + "=" + value + " <" + typeof(value) + ">");
  3158. if(callDefault){
  3159. // In jQuery UI 1.8, you have to manually invoke the _setOption method from the base widget
  3160. $.Widget.prototype._setOption.apply(this, arguments);
  3161. // TODO: In jQuery UI 1.9 and above, you use the _super method instead
  3162. // this._super( "_setOption", key, value );
  3163. }
  3164. if(rerender){
  3165. this.tree.render(true, false); // force, not-deep
  3166. }
  3167. },
  3168. /** Use the destroy method to clean up any modifications your widget has made to the DOM */
  3169. destroy: function() {
  3170. this._unbind();
  3171. this.tree._callHook("treeDestroy", this.tree);
  3172. // this.element.removeClass("ui-widget ui-widget-content ui-corner-all");
  3173. this.tree.$div.find(">ul.fancytree-container").remove();
  3174. this.$source && this.$source.removeClass("ui-helper-hidden");
  3175. // In jQuery UI 1.8, you must invoke the destroy method from the base widget
  3176. $.Widget.prototype.destroy.call(this);
  3177. // TODO: delete tree and nodes to make garbage collect easier?
  3178. // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method
  3179. },
  3180. // -------------------------------------------------------------------------
  3181. /* Remove all event handlers for our namespace */
  3182. _unbind: function() {
  3183. var ns = this.tree._ns;
  3184. this.element.unbind(ns);
  3185. this.tree.$container.unbind(ns);
  3186. $(document).unbind(ns);
  3187. },
  3188. /* Add mouse and kyboard handlers to the container */
  3189. _bind: function() {
  3190. var that = this,
  3191. opts = this.options,
  3192. tree = this.tree,
  3193. ns = tree._ns,
  3194. selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" );
  3195. // Remove all previuous handlers for this tree
  3196. this._unbind();
  3197. //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container);
  3198. tree.debug("bind events; container: ", tree.$container);
  3199. tree.$container.bind("focusin" + ns + " focusout" + ns, function(event){
  3200. tree.debug("Tree container got event " + event.type);
  3201. tree.treeOnFocusInOut.call(tree, event);
  3202. }).delegate("span.fancytree-title", selstartEvent + ns, function(event){
  3203. // prevent mouse-drags to select text ranges
  3204. tree.debug("<span> got event " + event.type);
  3205. event.preventDefault();
  3206. });
  3207. // keydown must be bound to document, because $container might not
  3208. // receive these events
  3209. $(document).bind("keydown" + ns, function(event){
  3210. // TODO: also bind keyup and keypress
  3211. tree.debug("doc got event " + event.type + ", hasFocus:" + tree.hasFocus());
  3212. if(opts.disabled || opts.keyboard === false || !tree.hasFocus()){
  3213. return true;
  3214. }
  3215. var node = tree.focusNode,
  3216. // node may be null
  3217. ctx = tree._makeHookContext(node || tree, event),
  3218. prevPhase = tree.phase;
  3219. try {
  3220. tree.phase = "userEvent";
  3221. if(node){
  3222. return ( tree._triggerNodeEvent("keydown", node, event) === false ) ? false : tree._callHook("nodeKeydown", ctx);
  3223. }else{
  3224. return ( tree._triggerTreeEvent("keydown", event) === false ) ? false : tree._callHook("nodeKeydown", ctx);
  3225. }
  3226. } finally {
  3227. tree.phase = prevPhase;
  3228. }
  3229. });
  3230. this.element.bind("click" + ns + " dblclick" + ns, function(event){
  3231. if(opts.disabled){
  3232. return true;
  3233. }
  3234. var ctx,
  3235. et = FT.getEventTarget(event),
  3236. node = et.node,
  3237. tree = that.tree,
  3238. prevPhase = tree.phase;
  3239. if( !node ){
  3240. return true; // Allow bubbling of other events
  3241. }
  3242. ctx = tree._makeHookContext(node, event);
  3243. // that.tree.debug("event(" + event.type + "): node: ", node);
  3244. try {
  3245. tree.phase = "userEvent";
  3246. switch(event.type) {
  3247. case "click":
  3248. ctx.targetType = et.type;
  3249. return ( tree._triggerNodeEvent("click", ctx, event) === false ) ? false : tree._callHook("nodeClick", ctx);
  3250. case "dblclick":
  3251. ctx.targetType = et.type;
  3252. return ( tree._triggerNodeEvent("dblclick", ctx, event) === false ) ? false : tree._callHook("nodeDblclick", ctx);
  3253. }
  3254. // } catch(e) {
  3255. // // var _ = null; // issue 117 // TODO
  3256. // $.error(e);
  3257. } finally {
  3258. tree.phase = prevPhase;
  3259. }
  3260. });
  3261. },
  3262. /** @returns {FancytreeNode} the active node or null */
  3263. getActiveNode: function() {
  3264. return this.tree.activeNode;
  3265. },
  3266. /**
  3267. * @param {String} key
  3268. * @returns {FancytreeNode} the matching node or null
  3269. */
  3270. getNodeByKey: function(key) {
  3271. return this.tree.getNodeByKey(key);
  3272. },
  3273. /** @returns {FancytreeNode} the invisible system root node */
  3274. getRootNode: function() {
  3275. return this.tree.rootNode;
  3276. },
  3277. /** @returns {Fancytree} the current tree instance */
  3278. getTree: function() {
  3279. return this.tree;
  3280. }
  3281. });
  3282. // $.ui.fancytree was created by the widget factory. Create a local shortcut:
  3283. FT = $.ui.fancytree;
  3284. /**
  3285. * Static members in the `$.ui.fancytree` namespace.
  3286. * @ name $.ui.fancytree
  3287. * @example:
  3288. * alert(""version: " + $.ui.fancytree.version);
  3289. * var node = $.ui.fancytree.()
  3290. */
  3291. $.extend($.ui.fancytree,
  3292. /** @lends $.ui.fancytree */
  3293. {
  3294. /** @type {String} */
  3295. version: "2.0.0-1",
  3296. /** @type {String} */
  3297. buildType: "develop",
  3298. /** @type {int} */
  3299. debugLevel: 2, // used by $.ui.fancytree.debug() and as default for tree.options.debugLevel
  3300. _nextId: 1,
  3301. _nextNodeKey: 1,
  3302. _extensions: {},
  3303. focusTree: null,
  3304. /** Expose class object as $.ui.fancytree._FancytreeClass */
  3305. _FancytreeClass: Fancytree,
  3306. /** Expose class object as $.ui.fancytree._FancytreeNodeClass */
  3307. _FancytreeNodeClass: FancytreeNode,
  3308. /* Feature checks to provide backwards compatibility */
  3309. jquerySupports: {
  3310. // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at
  3311. positionMyOfs: isVersionAtLeast($.ui.version, 1, 9)
  3312. },
  3313. debug: function(msg){
  3314. /*jshint expr:true */
  3315. ($.ui.fancytree.debugLevel >= 2) && consoleApply("log", arguments);
  3316. },
  3317. error: function(msg){
  3318. consoleApply("error", arguments);
  3319. },
  3320. /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event.
  3321. *
  3322. * @static
  3323. * @param {Event} event Mouse event, e.g. click, ...
  3324. * @returns {String} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
  3325. */
  3326. getEventTargetType: function(event){
  3327. return this.getEventTarget(event).type;
  3328. },
  3329. /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event.
  3330. *
  3331. * @param {Event} event Mouse event, e.g. click, ...
  3332. * @returns {object} Return a {node: FancytreeNode, type: TYPE} object
  3333. * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
  3334. */
  3335. getEventTarget: function(event){
  3336. var tcn = event && event.target ? event.target.className : "",
  3337. res = {node: this.getNode(event.target), type: undefined};
  3338. // TODO: use map for fast lookup
  3339. // FIXME: cannot work, when tcn also contains UI themeroller classes
  3340. // Use $(res.node).hasClass() instead
  3341. if( tcn === "fancytree-title" ){
  3342. res.type = "title";
  3343. }else if( tcn === "fancytree-expander" ){
  3344. res.type = (res.node.hasChildren() === false ? "prefix" : "expander");
  3345. }else if( tcn === "fancytree-checkbox" ){
  3346. res.type = "checkbox";
  3347. }else if( tcn === "fancytree-icon" ){
  3348. res.type = "icon";
  3349. }else if( tcn.indexOf("fancytree-node") >= 0 ){
  3350. // TODO: issue #93 (http://code.google.com/p/fancytree/issues/detail?id=93)
  3351. // res.type = this._getTypeForOuterNodeEvent(event);
  3352. res.type = "title";
  3353. }
  3354. return res;
  3355. },
  3356. /** Return a FancytreeNode instance from element.
  3357. *
  3358. * @param {Element | jQueryObject | Event} el
  3359. * @returns {FancytreeNode} matching node or null
  3360. */
  3361. getNode: function(el){
  3362. if(el instanceof FancytreeNode){
  3363. return el; // el already was a FancytreeNode
  3364. }else if(el.selector !== undefined){
  3365. el = el[0]; // el was a jQuery object: use the DOM element
  3366. }else if(el.originalEvent !== undefined){
  3367. el = el.target; // el was an Event
  3368. }
  3369. while( el ) {
  3370. if(el.ftnode) {
  3371. return el.ftnode;
  3372. }
  3373. el = el.parentNode;
  3374. }
  3375. return null;
  3376. },
  3377. /* Return a Fancytree instance from element.
  3378. * TODO: this function could help to get around the data('fancytree') / data('ui-fancytree') problem
  3379. * @param {Element | jQueryObject | Event} el
  3380. * @returns {Fancytree} matching tree or null
  3381. * /
  3382. getTree: function(el){
  3383. if(el instanceof Fancytree){
  3384. return el; // el already was a Fancytree
  3385. }else if(el.selector !== undefined){
  3386. el = el[0]; // el was a jQuery object: use the DOM element
  3387. }else if(el.originalEvent !== undefined){
  3388. el = el.target; // el was an Event
  3389. }
  3390. ...
  3391. return null;
  3392. },
  3393. */
  3394. info: function(msg){
  3395. /*jshint expr:true */
  3396. ($.ui.fancytree.debugLevel >= 1) && consoleApply("info", arguments);
  3397. },
  3398. /**
  3399. * Parse tree data from HTML <ul> markup
  3400. *
  3401. * @param {jQueryObject} $ul
  3402. * @returns {NodeData[]}
  3403. */
  3404. parseHtml: function($ul) {
  3405. // TODO: understand this:
  3406. /*jshint validthis:true */
  3407. var $children = $ul.find(">li"),
  3408. extraClasses, i, l, iPos, tmp, classes, className,
  3409. children = [];
  3410. // that = this;
  3411. $children.each(function() {
  3412. var allData, jsonData,
  3413. $li = $(this),
  3414. $liSpan = $li.find(">span:first", this),
  3415. $liA = $liSpan.length ? null : $li.find(">a:first"),
  3416. d = { tooltip: null, data: {} };
  3417. if( $liSpan.length ) {
  3418. d.title = $liSpan.html();
  3419. } else if( $liA && $liA.length ) {
  3420. // If a <li><a> tag is specified, use it literally and extract href/target.
  3421. d.title = $liA.html();
  3422. d.data.href = $liA.attr("href");
  3423. d.data.target = $liA.attr("target");
  3424. d.tooltip = $liA.attr("title");
  3425. } else {
  3426. // If only a <li> tag is specified, use the trimmed string up to
  3427. // the next child <ul> tag.
  3428. d.title = $li.html();
  3429. iPos = d.title.search(/<ul/i);
  3430. if( iPos >= 0 ){
  3431. d.title = d.title.substring(0, iPos);
  3432. }
  3433. }
  3434. d.title = $.trim(d.title);
  3435. // Make sure all fields exist
  3436. for(i=0, l=CLASS_ATTRS.length; i<l; i++){
  3437. d[CLASS_ATTRS[i]] = undefined;
  3438. }
  3439. // Initialize to `true`, if class is set and collect extraClasses
  3440. classes = this.className.split(" ");
  3441. extraClasses = [];
  3442. for(i=0, l=classes.length; i<l; i++){
  3443. className = classes[i];
  3444. if(CLASS_ATTR_MAP[className]){
  3445. d[className] = true;
  3446. }else{
  3447. extraClasses.push(className);
  3448. }
  3449. }
  3450. d.extraClasses = extraClasses.join(" ");
  3451. // Parse node options from ID, title and class attributes
  3452. tmp = $li.attr("title");
  3453. if( tmp ){
  3454. d.tooltip = tmp; // overrides <a title='...'>
  3455. }
  3456. tmp = $li.attr("id");
  3457. if( tmp ){
  3458. d.key = tmp;
  3459. }
  3460. // Add <li data-NAME='...'> as node.data.NAME
  3461. // See http://api.jquery.com/data/#data-html5
  3462. allData = $li.data();
  3463. // alert("d: " + JSON.stringify(allData));
  3464. if(allData && !$.isEmptyObject(allData)) {
  3465. // Special handling for <li data-json='...'>
  3466. jsonData = allData.json;
  3467. delete allData.json;
  3468. $.extend(d.data, allData);
  3469. // If a 'data-json' attribute is present, evaluate and add to node.data
  3470. if(jsonData) {
  3471. // alert("$li.data()" + JSON.stringify(jsonData));
  3472. // <li data-json='...'> is already returned as object
  3473. // see http://api.jquery.com/data/#data-html5
  3474. $.extend(d.data, jsonData);
  3475. }
  3476. }
  3477. // that.debug("parse ", d);
  3478. // var childNode = parentTreeNode.addChild(data);
  3479. // Recursive reading of child nodes, if LI tag contains an UL tag
  3480. $ul = $li.find(">ul:first");
  3481. if( $ul.length ) {
  3482. d.children = $.ui.fancytree.parseHtml($ul);
  3483. }else{
  3484. d.children = d.lazy ? undefined : null;
  3485. }
  3486. children.push(d);
  3487. // FT.debug("parse ", d, children);
  3488. });
  3489. return children;
  3490. },
  3491. /** Add Fancytree extension definition to the list of globally available extensions.
  3492. *
  3493. * @param name
  3494. * @param definition
  3495. */
  3496. registerExtension: function(name, definition){
  3497. $.ui.fancytree._extensions[name] = definition;
  3498. },
  3499. warn: function(msg){
  3500. consoleApply("warn", arguments);
  3501. }
  3502. });
  3503. // Use $.ui.fancytree.debugLevel as default for tree.options.debugLevel
  3504. //$.ui.fancytree.debug($.ui.fancytree.prototype);
  3505. //$.ui.fancytree.prototype.options.debugLevel = $.ui.fancytree.debugLevel;
  3506. /* *****************************************************************************
  3507. * Register AMD
  3508. */
  3509. // http://stackoverflow.com/questions/10918063/how-to-make-a-jquery-plugin-loadable-with-requirejs
  3510. // if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
  3511. // define( "jquery", [], function () { return jQuery; } );
  3512. // }
  3513. // TODO: maybe like so:?
  3514. // https://raw.github.com/malsup/blockui/master/jquery.blockUI.js
  3515. /*
  3516. if( typeof define === "function" && define.amd ) {
  3517. define( ["jquery"], function () {
  3518. return jQuery.ui.fancytree;
  3519. });
  3520. }
  3521. */
  3522. }(jQuery, window, document));