jquery.fancytree.js 123 KB

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