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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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));