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