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.js271
1 files changed, 258 insertions, 13 deletions
diff --git a/program/js/treelist.js b/program/js/treelist.js
index 4cffdac21..65f5fd4f4 100644
--- a/program/js/treelist.js
+++ b/program/js/treelist.js
@@ -44,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 || {});
@@ -52,6 +53,8 @@ function rcube_treelist_widget(node, p)
indexbyid = {},
selection = null,
drag_active = false,
+ search_active = false,
+ last_search = '',
has_focus = false,
box_coords = {},
item_coords = [],
@@ -60,6 +63,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;
@@ -74,10 +81,12 @@ function rcube_treelist_widget(node, p)
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)
@@ -106,6 +115,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;
+ })
+ }
+
container.on('focusin', function(e){
// TODO: only accept focus on virtual nodes from keyboard events
has_focus = true;
@@ -140,6 +186,7 @@ function rcube_treelist_widget(node, p)
}
me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node);
+ save_state(id, node.collapsed);
}
}
@@ -194,9 +241,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 indexbyid[id];
+ }
+
+ /**
+ * Return the DOM element of the list item with the given ID
+ */
+ function get_item(id, real)
{
- return id2dom(id).get(0);
+ return id2dom(id, real).get(0);
}
/**
@@ -205,7 +260,19 @@ 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) {
@@ -213,6 +280,7 @@ function rcube_treelist_widget(node, p)
if (!parent_node.children)
parent_node.children = [];
+ search_active = false;
parent_node.children.push(node);
parent_li = id2dom(parent_id);
@@ -225,6 +293,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 {
@@ -292,7 +375,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);
}
@@ -308,7 +391,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;
@@ -352,6 +435,80 @@ function rcube_treelist_widget(node, p)
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);
}
/**
@@ -385,7 +542,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))
.attr('role', 'treeitem')
- .addClass((node.classes || []).join(' '));
+ .addClass((node.classes || []).join(' '))
+ .data('id', node.id);
if (replace)
replace.replaceWith(li);
@@ -397,6 +555,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)
@@ -427,13 +588,14 @@ 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'),
level: level,
html: li.children().first().get(0).outerHTML,
+ text: li.children().first().text(),
children: walk_list(sublist, level+1)
}
@@ -442,6 +604,17 @@ 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);
+
li.attr('aria-expanded', node.collapsed ? 'false' : 'true');
}
if (li.hasClass('selected')) {
@@ -449,6 +622,8 @@ function rcube_treelist_widget(node, p)
selection = node.id;
}
+ li.data('id', node.id);
+
// declare list item as treeitem
li.attr('role', 'treeitem').attr('aria-level', node.level+1);
@@ -484,17 +659,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);
}
/**
@@ -511,6 +687,39 @@ function rcube_treelist_widget(node, p)
}
/**
+ * 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)
@@ -571,6 +780,7 @@ function rcube_treelist_widget(node, p)
focus_next(parent, dir, true);
}
}
+>>>>>>> dev-accessibility
}
@@ -696,7 +906,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;
}
@@ -717,6 +928,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) {
@@ -745,6 +958,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