fancytree-rails 2.0.0.pre.6.pre.1 → 2.0.0.pre.11.pre.1

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