fancytree-rails 2.0.0.pre.6.pre.1 → 2.0.0.pre.11.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Rakefile +6 -7
- data/lib/fancytree/rails/version.rb +2 -2
- data/vendor/assets/images/fancytree/skin-win8-xxl/icons.gif +0 -0
- data/vendor/assets/images/fancytree/skin-win8-xxl/loading.gif +0 -0
- data/vendor/assets/javascripts/fancytree.js +1 -1
- data/vendor/assets/javascripts/fancytree/MIT-LICENSE.txt +21 -0
- data/vendor/assets/javascripts/fancytree/jquery.fancytree-all.js +1267 -475
- data/vendor/assets/javascripts/fancytree/jquery.fancytree-custom.min.js +41 -0
- data/vendor/assets/javascripts/fancytree/jquery.fancytree.js +582 -310
- data/vendor/assets/javascripts/fancytree/jquery.fancytree.min.js +14 -7
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.childcounter.js +185 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.clones.js +417 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.columnview.js +149 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.debug.js +142 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.dnd.js +539 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.edit.js +318 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.filter.js +173 -0
- data/vendor/assets/javascripts/fancytree/{jquery.fancytree.awesome.js → src/jquery.fancytree.glyph.js} +28 -26
- data/vendor/assets/javascripts/fancytree/{jquery.fancytree.gridnav.js → src/jquery.fancytree.gridnav.js} +77 -41
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.js +4027 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.menu.js +155 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.persist.js +345 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.table.js +345 -0
- data/vendor/assets/javascripts/fancytree/src/jquery.fancytree.themeroller.js +82 -0
- data/vendor/assets/stylesheets/fancytree/skin-awesome/ui.fancytree.css +29 -15
- data/vendor/assets/stylesheets/fancytree/skin-awesome/ui.fancytree.min.css +1 -1
- data/vendor/assets/stylesheets/fancytree/skin-bootstrap/ui.fancytree.css +366 -0
- data/vendor/assets/stylesheets/fancytree/skin-bootstrap/ui.fancytree.min.css +6 -0
- data/vendor/assets/stylesheets/fancytree/skin-lion/ui.fancytree.css +34 -7
- data/vendor/assets/stylesheets/fancytree/skin-lion/ui.fancytree.min.css +1 -1
- data/vendor/assets/stylesheets/fancytree/skin-vista/ui.fancytree.css +34 -7
- data/vendor/assets/stylesheets/fancytree/skin-vista/ui.fancytree.min.css +1 -1
- data/vendor/assets/stylesheets/fancytree/skin-win7/ui.fancytree.css +34 -7
- data/vendor/assets/stylesheets/fancytree/skin-win7/ui.fancytree.min.css +1 -1
- data/vendor/assets/stylesheets/fancytree/skin-win8-xxl/ui.fancytree.css +507 -0
- data/vendor/assets/stylesheets/fancytree/skin-win8-xxl/ui.fancytree.min.css +11 -0
- data/vendor/assets/stylesheets/fancytree/skin-win8/ui.fancytree.css +34 -7
- data/vendor/assets/stylesheets/fancytree/skin-win8/ui.fancytree.min.css +1 -1
- data/vendor/assets/stylesheets/fancytree/skin-xp/ui.fancytree.css +34 -7
- data/vendor/assets/stylesheets/fancytree/skin-xp/ui.fancytree.min.css +1 -1
- metadata +24 -13
- data/vendor/assets/javascripts/fancytree/jquery.fancytree-all.min.js +0 -7
- data/vendor/assets/javascripts/fancytree/jquery.fancytree-all.min.js.map +0 -1
- data/vendor/assets/javascripts/fancytree/jquery.fancytree.min.js.map +0 -1
- data/vendor/assets/stylesheets/fancytree/skin-lion/ui.fancytree-org.css +0 -460
- data/vendor/assets/stylesheets/fancytree/skin-themeroller/ui.fancytree-org.css +0 -505
- data/vendor/assets/stylesheets/fancytree/skin-vista/ui.fancytree-org.css +0 -610
- data/vendor/assets/stylesheets/fancytree/skin-win7/ui.fancytree-org.css +0 -592
- data/vendor/assets/stylesheets/fancytree/skin-win8/ui.fancytree-org.css +0 -602
- data/vendor/assets/stylesheets/fancytree/skin-xp/ui.fancytree-org.css +0 -578
@@ -1,5 +1,5 @@
|
|
1
1
|
/*!
|
2
|
-
* jquery.fancytree.
|
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
|
13
|
-
* @date
|
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: "
|
30
|
-
version: "0.0.
|
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-
|
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.
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
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.
|
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 = $("
|
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)
|
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
|
13
|
-
* @date
|
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 $
|
76
|
+
var $tr, colIdx,
|
77
|
+
$td = $target.closest("td"),
|
78
|
+
$tdNext = null;
|
79
|
+
|
41
80
|
switch( keyCode ){
|
42
81
|
case KC.LEFT:
|
43
|
-
|
82
|
+
$tdNext = $td.prev();
|
83
|
+
break;
|
44
84
|
case KC.RIGHT:
|
45
|
-
|
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
|
-
|
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
|
109
|
+
return $tdNext;
|
52
110
|
}
|
53
111
|
|
112
|
+
|
54
113
|
/*******************************************************************************
|
55
114
|
* Extension code
|
56
115
|
*/
|
57
|
-
$.ui.fancytree.registerExtension(
|
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
|
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(
|
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.
|
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
|
-
|
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 = {"&": "&", "<": "<", ">": ">", "\"": """, "'": "'", "/": "/"},
|
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 `<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 `<li><span>..</span> .. </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…",
|
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 <, >, &, ", ', / 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));
|