element for the given node ID
*/
function id2dom(id, real)
{
var domid = p.id_encode ? p.id_encode(id) : id,
suffix = search_active && !real ? '--xsR' : '';
return $('#' + p.id_prefix + domid + suffix, container);
}
/**
* Scroll the parent container to make the given list item visible
*/
function scroll_to_node(li)
{
var scroller = container.parent(),
current_offset = scroller.scrollTop(),
rel_offset = li.offset().top - scroller.offset().top;
if (rel_offset < 0 || rel_offset + li.height() > scroller.height())
scroller.scrollTop(rel_offset + current_offset);
}
/**
* Save node collapse state to localStorage
*/
function save_state(id, collapsed)
{
if (p.save_state && window.rcmail) {
var key = 'treelist-' + list_id;
if (!tree_state) {
tree_state = rcmail.local_storage_get_item(key, {});
}
if (tree_state[id] != collapsed) {
tree_state[id] = collapsed;
rcmail.local_storage_set_item(key, tree_state);
}
}
}
/**
* Read node collapse state from localStorage
*/
function get_state(id)
{
if (p.save_state && window.rcmail) {
if (!tree_state) {
tree_state = rcmail.local_storage_get_item('treelist-' + list_id, {});
}
return tree_state[id];
}
return undefined;
}
/**
* Handler for keyboard events on treelist
*/
function keypress(e)
{
var target = e.target || {},
keyCode = rcube_event.get_keycode(e);
if (!has_focus || target.nodeName == 'INPUT' && keyCode != 38 && keyCode != 40 || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
return true;
switch (keyCode) {
case 38:
case 40:
case 63232: // 'up', in safari keypress
case 63233: // 'down', in safari keypress
var li = p.keyboard ? container.find(':focus').closest('li') : [];
if (li.length) {
focus_next(li, (mod = keyCode == 38 || keyCode == 63232 ? -1 : 1));
}
return rcube_event.cancel(e);
case 37: // Left arrow key
case 39: // Right arrow key
var id, node, li = container.find(':focus').closest('li');
if (li.length) {
id = dom2id(li);
node = indexbyid[id];
if (node && node.children.length && node.collapsed != (keyCode == 37))
toggle(id, rcube_event.get_modifier(e) == SHIFT_KEY); // toggle subtree
}
return false;
case 9: // Tab
if (p.keyboard && p.tabexit) {
// jump to last/first item to move focus away from the treelist widget by tab
var limit = rcube_event.get_modifier(e) == SHIFT_KEY ? 'first' : 'last';
focus_noscroll(container.find('li[role=treeitem]:has(a)')[limit]().find('a:'+limit));
}
break;
}
return true;
}
function focus_next(li, dir, from_child)
{
var mod = dir < 0 ? 'prev' : 'next',
next = li[mod](), limit, parent;
if (dir > 0 && !from_child && li.children('ul[role=group]:visible').length) {
li.children('ul').children('li').first().find('a').first().focus();
}
else if (dir < 0 && !from_child && next.children('ul[role=group]:visible').length) {
next.children('ul').children('li').last().find('a').first().focus();
}
else if (next.length && next.find('a').first().focus().length) {
// focused
}
else {
parent = li.parent().closest('li[role=treeitem]');
if (parent.length)
if (dir < 0) {
parent.find('a').first().focus();
}
else {
focus_next(parent, dir, true);
}
}
}
/**
* Focus the given element without scrolling the list container
*/
function focus_noscroll(elem)
{
if (elem.length) {
var frame = container.parent().get(0) || { scrollTop:0 },
y = frame.scrollTop || frame.scrollY;
elem.focus();
frame.scrollTop = y;
}
}
function get_next()
{
var node, child;
if (selection && (node = id2dom(selection))) {
child = node.children('ul').children('li').first();
if (child.length) {
return dom2id(child);
}
child = node.next();
if (child.length) {
return dom2id(child);
}
while ((node = node.parent('ul').parent('li')) && node.length) {
child = node.next();
if (child.length) {
return dom2id(child);
}
}
}
}
function get_prev()
{
var node, prev, child;
if (selection && (node = id2dom(selection))) {
prev = node.prev();
child = prev.find('li').last();
if (child.length) {
return dom2id(child);
}
if (prev.length) {
return dom2id(prev);
}
node = node.parent().parent();
if (node.length && node.is('li')) {
return dom2id(node);
}
}
}
///// drag & drop support
/**
* When dragging starts, compute absolute bounding boxes of the list and it's items
* for faster comparisons while mouse is moving
*/
function drag_start(force)
{
if (!force && drag_active)
return;
drag_active = true;
var li, item, height,
pos = container.offset();
body_scroll_top = bw.ie ? 0 : window.pageYOffset;
list_scroll_top = container.parent().scrollTop();
pos.top += list_scroll_top;
box_coords = {
x1: pos.left,
y1: pos.top,
x2: pos.left + container.width(),
y2: pos.top + container.height()
};
item_coords = [];
for (var id in indexbyid) {
li = id2dom(id);
item = li.children().first().get(0);
if (item && (height = item.offsetHeight)) {
pos = $(item).offset();
pos.top += list_scroll_top;
item_coords[id] = {
x1: pos.left,
y1: pos.top,
x2: pos.left + item.offsetWidth,
y2: pos.top + height,
on: id == autoexpand_item
};
}
}
// enable auto-scrolling of list container
if (container.height() > container.parent().height()) {
container.parent().on('mousemove.treelist', function(e) {
var scroll = 0,
mouse = rcube_event.get_mouse_pos(e);
mouse.y -= container.parent().offset().top;
if (mouse.y < 25 && list_scroll_top > 0) {
scroll = -1; // up
}
else if (mouse.y > container.parent().height() - 25) {
scroll = 1; // down
}
if (drag_active && scroll != 0) {
if (!scroll_timer)
scroll_timer = setTimeout(function() { drag_scroll(scroll); }, p.scroll_delay);
}
else if (scroll_timer) {
window.clearTimeout(scroll_timer);
scroll_timer = null;
}
})
.on('mouseleave.treelist', function() {
if (scroll_timer) {
window.clearTimeout(scroll_timer);
scroll_timer = null;
}
});
}
}
/**
* Signal that dragging has stopped
*/
function drag_end()
{
container.parent().off('.treelist');
$('li.droptarget', container).removeClass('droptarget');
if (!drag_active)
return;
drag_active = false;
scroll_timer = null;
if (autoexpand_timer) {
clearTimeout(autoexpand_timer);
autoexpand_timer = null;
autoexpand_item = null;
}
}
/**
* Scroll list container in the given direction
*/
function drag_scroll(dir)
{
if (!drag_active)
return;
var old_top = list_scroll_top;
container.parent().get(0).scrollTop += p.scroll_step * dir;
list_scroll_top = container.parent().scrollTop();
scroll_timer = null;
if (list_scroll_top != old_top)
scroll_timer = setTimeout(function() { drag_scroll(dir); }, p.scroll_speed);
}
/**
* Determine if the given mouse coords intersect the list and one of its items
*/
function intersects(mouse, highlight)
{
// offsets to compensate for scrolling while dragging a message
var boffset = bw.ie ? -document.documentElement.scrollTop : body_scroll_top,
moffset = container.parent().scrollTop(),
result = null;
mouse.top = mouse.y + moffset - boffset;
// no intersection with list bounding box
if (mouse.x < box_coords.x1 || mouse.x >= box_coords.x2 || mouse.top < box_coords.y1 || mouse.top >= box_coords.y2) {
// TODO: optimize performance for this operation
if (highlight)
$('li.droptarget', container).removeClass('droptarget');
return result;
}
// check intersection with visible list items
var id, pos, node;
for (id in item_coords) {
pos = item_coords[id];
if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.top >= pos.y1 && mouse.top < pos.y2) {
node = indexbyid[id];
// if the folder is collapsed, expand it after the configured time
if (node.children && node.children.length && node.collapsed && p.autoexpand && autoexpand_item != id) {
if (autoexpand_timer)
clearTimeout(autoexpand_timer);
autoexpand_item = id;
autoexpand_timer = setTimeout(function() {
expand(autoexpand_item);
drag_start(true); // re-calculate item coords
autoexpand_item = null;
if (ui_droppable)
$.ui.ddmanager.prepareOffsets($.ui.ddmanager.current, null);
}, p.autoexpand);
}
else if (autoexpand_timer && autoexpand_item != id) {
clearTimeout(autoexpand_timer);
autoexpand_item = null;
autoexpand_timer = null;
}
// check if this item is accepted as drop target
if (p.check_droptarget(node)) {
if (highlight) {
id2dom(id).addClass('droptarget');
pos.on = true;
}
result = id;
}
else {
result = null;
}
}
else if (pos.on) {
id2dom(id).removeClass('droptarget');
pos.on = false;
}
}
return result;
}
/**
* Wrapper for jQuery.UI.droppable() activation on this widget
*
* @param object Options as passed to regular .droppable() function
*/
function droppable(opts)
{
if (!opts) opts = {};
if ($.type(opts) == 'string') {
if (opts == 'destroy') {
ui_droppable = null;
}
$('li:not(.virtual)', container).droppable(opts);
return this;
}
droppable_opts = opts;
var my_opts = $.extend({
greedy: true,
tolerance: 'pointer',
hoverClass: 'droptarget',
addClasses: false
}, opts);
my_opts.activate = function(e, ui) {
drag_start();
ui_droppable = ui;
if (opts.activate)
opts.activate(e, ui);
};
my_opts.deactivate = function(e, ui) {
drag_end();
ui_droppable = null;
if (opts.deactivate)
opts.deactivate(e, ui);
};
my_opts.over = function(e, ui) {
intersects(rcube_event.get_mouse_pos(e), false);
if (opts.over)
opts.over(e, ui);
};
$('li:not(.virtual)', container).droppable(my_opts);
return this;
}
/**
* Wrapper for jQuery.UI.draggable() activation on this widget
*
* @param object Options as passed to regular .draggable() function
*/
function draggable(opts)
{
if (!opts) opts = {};
if ($.type(opts) == 'string') {
if (opts == 'destroy') {
ui_draggable = null;
}
$('li:not(.virtual)', container).draggable(opts);
return this;
}
draggable_opts = opts;
var my_opts = $.extend({
appendTo: 'body',
revert: 'invalid',
iframeFix: true,
addClasses: false,
cursorAt: {left: -20, top: 5},
create: function(e, ui) { ui_draggable = ui; },
helper: function(e) {
return $('').attr('id', 'rcmdraglayer')
.text($(e.target).first().text().trim());
}
}, opts);
$('li:not(.virtual)', container).draggable(my_opts);
return this;
}
function is_draggable()
{
return !!ui_draggable;
}
}
// use event processing functions from Roundcube's rcube_event_engine
rcube_treelist_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_treelist_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_treelist_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;