summaryrefslogtreecommitdiff
path: root/program/js/treelist.js
diff options
context:
space:
mode:
Diffstat (limited to 'program/js/treelist.js')
-rw-r--r--program/js/treelist.js337
1 files changed, 302 insertions, 35 deletions
diff --git a/program/js/treelist.js b/program/js/treelist.js
index d940e396c..90eeeeae7 100644
--- a/program/js/treelist.js
+++ b/program/js/treelist.js
@@ -1,20 +1,33 @@
-/*
- +-----------------------------------------------------------------------+
- | Roundcube Treelist widget |
- | |
- | This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013, The Roundcube Dev Team |
- | |
- | Licensed under the GNU General Public License version 3 or |
- | any later version with exceptions for skins & plugins. |
- | See the README file for a full license statement. |
- | |
- +-----------------------------------------------------------------------+
- | Authors: Thomas Bruederli <roundcube@gmail.com> |
- +-----------------------------------------------------------------------+
- | Requires: common.js |
- +-----------------------------------------------------------------------+
-*/
+/**
+ * Roundcube Treelist Widget
+ *
+ * This file is part of the Roundcube Webmail client
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (c) 2013-2014, The Roundcube Dev Team
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ *
+ * @author Thomas Bruederli <roundcube@gmail.com>
+ * @requires jquery.js, common.js
+ */
/**
@@ -31,6 +44,7 @@ function rcube_treelist_widget(node, p)
scroll_delay: 500,
scroll_step: 5,
scroll_speed: 20,
+ save_state: false,
check_droptarget: function(node){ return !node.virtual }
}, p || {});
@@ -39,6 +53,8 @@ function rcube_treelist_widget(node, p)
indexbyid = {},
selection = null,
drag_active = false,
+ search_active = false,
+ last_search = '',
box_coords = {},
item_coords = [],
autoexpand_timer,
@@ -46,6 +62,10 @@ function rcube_treelist_widget(node, p)
body_scroll_top = 0,
list_scroll_top = 0,
scroll_timer,
+ searchfield,
+ tree_state,
+ ui_droppable,
+ list_id = (container.attr('id') || p.id_prefix || '0'),
me = this;
@@ -56,13 +76,16 @@ function rcube_treelist_widget(node, p)
this.collapse = collapse;
this.select = select;
this.render = render;
+ this.reset = reset;
this.drag_start = drag_start;
this.drag_end = drag_end;
this.intersects = intersects;
+ this.droppable = droppable;
this.update = update_node;
this.insert = insert;
this.remove = remove;
this.get_item = get_item;
+ this.get_node = get_node;
this.get_selection = get_selection;
/////// startup code (constructor)
@@ -80,6 +103,7 @@ function rcube_treelist_widget(node, p)
// register click handlers on list
container.on('click', 'div.treetoggle', function(e){
toggle(dom2id($(this).parent()));
+ e.stopPropagation();
});
container.on('click', 'li', function(e){
@@ -90,6 +114,43 @@ function rcube_treelist_widget(node, p)
}
});
+ // activate search function
+ if (p.searchbox) {
+ searchfield = $(p.searchbox).on('keyup', function(e) {
+ var key = rcube_event.get_keycode(e),
+ mod = rcube_event.get_modifier(e);
+
+ switch (key) {
+ case 9: // tab
+ break;
+
+ case 13: // enter
+ search(this.value, true);
+ return rcube_event.cancel(e);
+
+ case 27: // escape
+ reset_search();
+ break;
+
+ case 38: // arrow up
+ case 37: // left
+ case 39: // right
+ case 40: // arrow down
+ return; // ignore arrow keys
+
+ default:
+ search(this.value, false);
+ break;
+ }
+ }).attr('autocomplete', 'off');
+
+ // find the reset button for this search field
+ searchfield.parent().find('a.reset').click(function(e) {
+ reset_search();
+ return false;
+ })
+ }
+
/////// private methods
@@ -104,11 +165,6 @@ function rcube_treelist_widget(node, p)
node.collapsed = typeof set == 'undefined' || set;
update_dom(node);
- // Work around a bug in IE6 and IE7, see #1485309
- if (window.bw && (bw.ie6 || bw.ie7) && node.collapsed) {
- id2dom(node.id).next().children('ul:visible').hide().show();
- }
-
if (recursive && node.children) {
for (var i=0; i < node.children.length; i++) {
collapse(node.children[i].id, recursive, set);
@@ -116,6 +172,7 @@ function rcube_treelist_widget(node, p)
}
me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node);
+ save_state(id, node.collapsed);
}
}
@@ -170,9 +227,17 @@ function rcube_treelist_widget(node, p)
/**
* Return the DOM element of the list item with the given ID
*/
- function get_item(id)
+ function get_node(id)
{
- return id2dom(id).get(0);
+ return indexbyid[id];
+ }
+
+ /**
+ * Return the DOM element of the list item with the given ID
+ */
+ function get_item(id, real)
+ {
+ return id2dom(id, real).get(0);
}
/**
@@ -181,13 +246,26 @@ function rcube_treelist_widget(node, p)
function insert(node, parent_id, sort)
{
var li, parent_li,
- parent_node = parent_id ? indexbyid[parent_id] : null;
+ parent_node = parent_id ? indexbyid[parent_id] : null
+ search_ = search_active;
+
+ // ignore, already exists
+ if (indexbyid[node.id]) {
+ return;
+ }
+
+ // apply saved state
+ state = get_state(node.id, node.collapsed);
+ if (state !== undefined) {
+ node.collapsed = state;
+ }
// insert as child of an existing node
if (parent_node) {
if (!parent_node.children)
parent_node.children = [];
+ search_active = false;
parent_node.children.push(node);
parent_li = id2dom(parent_id);
@@ -200,6 +278,21 @@ function rcube_treelist_widget(node, p)
// append new node to parent's child list
li = render_node(node, parent_li.children('ul').first());
}
+
+ // list is in search mode
+ if (search_) {
+ search_active = search_;
+
+ // add clone to current search results (top level)
+ if (!li.is(':visible')) {
+ $('<li>')
+ .attr('id', li.attr('id') + '--xsR')
+ .attr('class', li.attr('class'))
+ .addClass('searchresult__')
+ .append(li.children().first().clone(true, true))
+ .appendTo(container);
+ }
+ }
}
// insert at top level
else {
@@ -266,7 +359,7 @@ function rcube_treelist_widget(node, p)
if (sibling) {
li.insertAfter(sibling);
}
- else if (first.id != myid) {
+ else if (first && first.id != myid) {
li.insertBefore(first);
}
@@ -282,7 +375,7 @@ function rcube_treelist_widget(node, p)
var node, li;
if (node = indexbyid[id]) {
- li = id2dom(id);
+ li = id2dom(id, true);
li.remove();
node.deleted = true;
@@ -314,6 +407,94 @@ function rcube_treelist_widget(node, p)
}
/**
+ *
+ */
+ function reset()
+ {
+ select('');
+
+ data = [];
+ indexbyid = {};
+ drag_active = false;
+
+ container.html('');
+
+ reset_search();
+ }
+
+ /**
+ *
+ */
+ function search(q, enter)
+ {
+ q = String(q).toLowerCase();
+
+ if (!q.length)
+ return reset_search();
+ else if (q == last_search && !enter)
+ return 0;
+
+ var hits = [];
+ var search_tree = function(items) {
+ $.each(items, function(i, node) {
+ var li, sli;
+ if (!node.virtual && !node.deleted && String(node.text).toLowerCase().indexOf(q) >= 0 && hits.indexOf(node.id) < 0) {
+ li = id2dom(node.id);
+ sli = $('<li>')
+ .attr('id', li.attr('id') + '--xsR')
+ .attr('class', li.attr('class'))
+ .addClass('searchresult__')
+ .append(li.children().first().clone(true, true))
+ .appendTo(container);
+ hits.push(node.id);
+ }
+
+ if (node.children && node.children.length) {
+ search_tree(node.children);
+ }
+ });
+ };
+
+ // reset old search results
+ if (search_active) {
+ $(container).children('li.searchresult__').remove();
+ search_active = false;
+ }
+
+ // hide all list items
+ $(container).children('li').hide().removeClass('selected');
+
+ // search recursively in tree (to keep sorting order)
+ search_tree(data);
+ search_active = true;
+ last_search = q;
+
+ me.triggerEvent('search', { query: q, last: last_search, count: hits.length, ids: hits, execute: enter||false });
+
+ return hits.count;
+ }
+
+ /**
+ *
+ */
+ function reset_search()
+ {
+ if (searchfield)
+ searchfield.val('');
+
+ $(container).children('li.searchresult__').remove();
+ $(container).children('li').show();
+
+ search_active = false;
+
+ me.triggerEvent('search', { query: false, last: last_search });
+ last_search = '';
+
+ if (selection)
+ select(selection);
+ }
+
+ /**
* Render the tree list from the internal data structure
*/
function render()
@@ -342,7 +523,8 @@ function rcube_treelist_widget(node, p)
var li = $('<li>')
.attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id))
- .addClass((node.classes || []).join(' '));
+ .addClass((node.classes || []).join(' '))
+ .data('id', node.id);
if (replace)
replace.replaceWith(li);
@@ -354,6 +536,9 @@ function rcube_treelist_widget(node, p)
else if (typeof node.html == 'object')
li.append(node.html);
+ if (!node.text)
+ node.text = li.children().first().text();
+
if (node.virtual)
li.addClass('virtual');
if (node.id == selection)
@@ -382,12 +567,13 @@ function rcube_treelist_widget(node, p)
{
var result = [];
ul.children('li').each(function(i,e){
- var li = $(e), sublist = li.children('ul');
+ var state, li = $(e), sublist = li.children('ul');
var node = {
id: dom2id(li),
- classes: li.attr('class').split(' '),
+ classes: String(li.attr('class')).split(' '),
virtual: li.hasClass('virtual'),
html: li.children().first().get(0).outerHTML,
+ text: li.children().first().text(),
children: walk_list(sublist)
}
@@ -396,11 +582,22 @@ function rcube_treelist_widget(node, p)
}
if (node.children.length) {
node.collapsed = sublist.css('display') == 'none';
+
+ // apply saved state
+ state = get_state(node.id, node.collapsed);
+ if (state !== undefined) {
+ node.collapsed = state;
+ sublist[(state?'hide':'show')]();
+ }
+
+ if (!li.children('div.treetoggle').length)
+ $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '">&nbsp;</div>').appendTo(li);
}
if (li.hasClass('selected')) {
selection = node.id;
}
+ li.data('id', node.id);
result.push(node);
indexbyid[node.id] = node;
})
@@ -426,17 +623,18 @@ function rcube_treelist_widget(node, p)
*/
function dom2id(li)
{
- var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), '');
+ var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), '').replace(/--xsR$/, '');
return p.id_decode ? p.id_decode(domid) : domid;
}
/**
* Get the <li> element for the given node ID
*/
- function id2dom(id)
+ function id2dom(id, real)
{
- var domid = p.id_encode ? p.id_encode(id) : id;
- return $('#' + p.id_prefix + domid);
+ var domid = p.id_encode ? p.id_encode(id) : id,
+ suffix = search_active && !real ? '--xsR' : '';
+ return $('#' + p.id_prefix + domid + suffix, container);
}
/**
@@ -452,6 +650,40 @@ function rcube_treelist_widget(node, p)
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;
+ }
+
+
///// drag & drop support
/**
@@ -574,7 +806,8 @@ function rcube_treelist_widget(node, p)
// 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
- $('li.droptarget', container).removeClass('droptarget');
+ if (highlight)
+ $('li.droptarget', container).removeClass('droptarget');
return result;
}
@@ -595,6 +828,8 @@ function rcube_treelist_widget(node, p)
expand(autoexpand_item);
drag_start(); // 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) {
@@ -623,6 +858,38 @@ function rcube_treelist_widget(node, p)
return result;
}
+
+ /**
+ * Wrapper for jQuery.UI.droppable() activation on this widget
+ *
+ * @param object Options as passed to regular .droppable() function
+ */
+ function droppable(opts)
+ {
+ var my_opts = $.extend({ greedy: true, 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);
+ }
}
// use event processing functions from Roundcube's rcube_event_engine