-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtree.js
337 lines (242 loc) · 12.4 KB
/
tree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
(function ($) {
var methods = {
init : function (options) {
var settings = $.extend({'title':''}, options);
return this.each(function () {
// Get tree data
data = $(this).data('tree');
// If data is not yet set, initialize with root
if (!data) {
$(this).data('tree',{'root':$(this)});
data = $(this).data('tree');
}
$(this).addClass('root');
// Wrap the tree UL in a div for DOM searching purposes
$(this).wrap('<div class="one-tree-forest" />');
data.wrapper = $(this).parent();
// Add a title
if (settings.title != '')
{
data.wrapper.prepend('<h1>'+settings.title+'</h1>');
}
// Wrap the text in the LIs inside divs
data.wrapper.find('li').each(function () {
$($(this).contents().get(0)).wrap('<div class="tree-node" />');
});
// Draw the tree!
methods.drawTree($(this));
});
},
drawTree : function (list) {
data = list.data('tree');
wrapper = data.wrapper;
// Take sample sizes
data.nodeWidth = parseInt(wrapper.find('div.tree-node:first').css('width'));
data.nodeHeight = parseInt(wrapper.find('div.tree-node:first').css('height'));
data.horizontalSpacing = parseInt(wrapper.find('ul:first').css('padding-left')) * 2;
data.verticalSpacing = parseInt(wrapper.find('li:first').css('margin-top'));
data.marginTop = data.verticalSpacing / 2;
data.marginBottom = data.marginTop + 1;
// Set data for each child so that it knows its own parent
wrapper.find('ul:not(.root), li').data('tree', {'root':list});
// Add the top and bottom lines that poke out of nodes as well as the horizontal connectors
wrapper
.find('li:not(:has(.tree-node-top))')
.append('<div class="tree-node-top tree-line"></div>').end()
.find('li:not(:has(.tree-node-bottom))')
.append('<div class="tree-node-bottom tree-line"></div>').end()
// I'm adding divs to the uls, which isn't allowed but works anyway. I might change this to make standards happy
.find('ul:not(:has(.tree-branch-horizontal))')
.append('<div class="tree-branch-horizontal tree-line"></div>').end()
// Make sure every li has a child ul for future children
.find('li:not(:has(ul))')
.append('<ul></ul>')
.find('ul')
.data('tree', {'root':list}).end().end()
.find('ul')
.css('min-width', data.tree - data.nodeWidth).end()
.find('li .tree-node-top')
.css('top', '-' + data.marginTop + 'px').end()
.find('li .tree-node-bottom')
.css('top', data.nodeHeight + 1 + 'px').end()
// Activate the lines that poke out of the top on li's that are children
.find('ul li .tree-node-top')
.css('height', data.marginTop+'px').end()
// Activate the lines that poke out the bottom on li's that have children
.find('li:has(li) > .tree-node-bottom')
.css('height', data.marginBottom+'px').end()
// Deactivate the lines that poke out the bottom on li's that do not have children
.find('li:not(:has(li)) > .tree-node-bottom')
.css('height', '0px').end()
// Connect all the vertical lines with big horizontal ones
.find('ul:has(li)').each(function () {
if ($(this).find('> li:visible').length) {
var left = $(this).find('> li:visible:first > .tree-node-top').offset().left - $(this).offset().left;
var right = parseInt($(this).css('width')) - ($(this).find('> li:visible:last > .tree-node-top').offset().left - $(this).offset().left) + data.horizontalSpacing;
$(this).find('> .tree-branch-horizontal').css({
'top': data.marginTop+'px',
'left': left+'px',
'right': right+'px'
});
}
}).end()
// Make LIs draggable
.find('li').draggable({
stack: 'body',
handle: '> div.tree-node',
helper: 'clone',
appendTo: wrapper,
scroll: true,
revert: 'invalid',
start: methods.onDragStart,
stop: methods.onDragStop,
drag: methods.onDrag
}).end()
// Make ULs droppable
.find('ul:not(>li ul, li.ui-draggable-dragging ul)').droppable({
greedy: false,
tolerance: methods.handleTouch,
over: methods.onOver,
out: methods.onOut,
drop: methods.onDrop,
deactivate: methods.onDeactivate
});
// end wrapper
},
getRootData : function (element) {
return $(element).data('tree').root.data('tree');
},
handleTouch : function (draggable, droppable) {
var handle = $(draggable.helper).find('div.tree-node');
var x1 = handle.offset().left, x2 = x1 + handle.css('width'),
y1 = handle.offset().top, y2 = y1 + handle.css('height');
var l = droppable.offset.left, r = l + droppable.proportions.width,
t = droppable.offset.top, b = t + droppable.proportions.height;
return (
(y1 >= t && y1 <= b) || // Top edge touching
(y2 >= t && y2 <= b) || // Bottom edge touching
(y1 < t && y2 > b) // Surrounded vertically
) && (
(x1 >= l && x1 <= r) || // Left edge touching
(x2 >= l && x2 <= r) || // Right edge touching
(x1 < l && x2 > r) // Surrounded horizontally
);
},
onDragStart : function (event, ui) {
data = methods.getRootData(this);
// Hide the element on the tree, pending removal
$(event.target).hide();
methods.drawTree(data.root);
// On the spawned helper, hide the little top line that sticks out
ui.helper.find('> .tree-node-top').css('height', '0px');
ui.helper.find('.ui-droppable').droppable('disable');
// Dropped state lets us know if the object was moved or simply let go of
ui.helper.data('dropped', false);
},
onDragStop : function (event, ui) {
data = methods.getRootData(this);
// Reset dropzone placement variables
data.targetList = null;
console.log('drag stop', data.targetList);
data.dropIndex = -1;
// Decide whether the node should be returned to its home (did it hit a drop zone?)
if (!ui.helper.data('dropped')) {
$(event.target).show();
} else {
$(event.target).remove();
}
methods.drawTree(data.root);
},
onDrag : function (event, ui) {
data = methods.getRootData(this);
dropIndex = data.dropIndex;
// If the object is currently on a drop zone
if (data.targetList) {
// Get the position of the helper
var currentOffset = ui.helper.find('> .tree-node').offset().left + (data.nodeWidth / 2);
// Calculate where the drop zone indicator should be positioned
var newIndex;
// For each node in the drop zone, compare its position with the position of the helper
var nodesInList = $(data.targetList).find('> li:not(.tree-drop-zone)');
if (nodesInList.length) {
nodesInList.each(function (index) {
var compareOffset = $(this).find('> .tree-node').offset().left + (data.nodeWidth / 2);
// If the node is to the right of the helper, we found it
if (compareOffset > currentOffset) {
/*if (currentOffset < compareOffset + $(this).outerWidth()) {
console.log(currentOffset, compareOffset, $(this), $(this).outerWidth());
newIndex = -1;
data.targetList = $(this).find('ul').first();
return false;
} else {
*///console.log('do we EVER get here?');
newIndex = index;
return false;
/*}*/
}
// If we reached the end of the list, the helper must be at the right edge
if (index == nodesInList.length - 1) {
newIndex = index + 1;
}
});
} else {
// If there are no child elements, just place the indicator at position zero
newIndex = 0;
}
// If the new drop zone indicator is not in the same position as the previous one, redraw
if (newIndex != dropIndex) {
dropIndex = newIndex;
$('.tree-drop-zone').remove();
if ($(data.targetList).find('> li').length) {
if ($(data.targetList).find('> li')[dropIndex]) {
$('<li class="tree-drop-zone"><div class="tree-node"></div></li>').insertBefore($(data.targetList).find('> li')[dropIndex]);
} else {
$('<li class="tree-drop-zone"><div class="tree-node"></div></li>').insertAfter($(data.targetList).find('> li')[dropIndex-1]);
}
} else {
$('<li class="tree-drop-zone"><div class="tree-node"></div></li>').appendTo($(data.targetList));
}
methods.drawTree(data.root);
}
}
},
onOver : function (event, ui) {
console.log('something over', event, ui);
data = methods.getRootData(this);
data.targetList = event.target;
console.log('over', data.targetList);
},
onOut : function (event, ui) {
data = methods.getRootData(this);
// Remove the drop zone indicator
$(event.target).find('.tree-drop-zone').remove();
methods.drawTree(data.root);
// Reset drop zone placement variables
data.targetList = null;
console.log('out', data.targetList);
data.dropIndex = -1;
},
onDrop : function (event, ui) {
data = methods.getRootData(this);
// Add a clone of the helper to the correct place in the tree
ui.helper.clone().attr('style', '').removeClass().insertBefore($(event.target).find('.tree-drop-zone'));
// Remove the drop zone indicator
$(event.target).find('.tree-drop-zone').remove();
methods.drawTree(data.root);
// Indicate that the node was dropped on a zone and should not be returned
ui.helper.data('dropped', true);
},
onDeactivate : function (event, ui) {
}
};
$.fn.tree = function (method) {
// Method calling logic
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.tree');
}
}
})(jQuery);