From 3c309af0663f1673f921682c6ae3f673426e1397 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 31 Jan 2013 13:49:35 +0100 Subject: - Refactored the hierarchical mailboxlist control into a separate widget class - Build address book directories list as hierarchical list - Make address book groups collapsible using the new new treelist widget - Use encoded identifiers for address book directory list items --- program/include/rcmail.php | 16 +- program/js/app.js | 206 ++++++------------ program/js/treelist.js | 425 +++++++++++++++++++++++++++++++++++++ program/steps/addressbook/func.inc | 28 ++- 4 files changed, 521 insertions(+), 154 deletions(-) create mode 100644 program/js/treelist.js (limited to 'program') diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 5f2a2177b..70dba4192 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -1406,6 +1406,7 @@ class rcmail extends rcube $js_mailboxlist = array(); $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib); + $rcmail->output->include_script('treelist.js'); $rcmail->output->add_gui_object('mailboxlist', $attrib['id']); $rcmail->output->set_env('mailboxes', $js_mailboxlist); $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']); @@ -1584,14 +1585,13 @@ class rcmail extends rcube 'id' => "rcmli".$folder_id, 'class' => join(' ', $classes), 'noclose' => true), - html::a($link_attrib, $html_name) . - (!empty($folder['folders']) ? html::div(array( - 'class' => ($is_collapsed ? 'collapsed' : 'expanded'), - 'style' => "position:absolute", - 'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail_output::JS_OBJECT_NAME, $js_name) - ), ' ') : '')); - - $jslist[$folder_id] = array( + html::a($link_attrib, $html_name)); + + if (!empty($folder['folders'])) { + $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); + } + + $jslist[$folder['id']] = array( 'id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual'] diff --git a/program/js/app.js b/program/js/app.js index 6d5bdfe74..dbb65eea4 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -4,7 +4,7 @@ | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2013, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2011-2013, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -459,9 +459,22 @@ function rcube_webmail() this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); // map implicit containers - if (this.gui_objects.folderlist) + if (this.gui_objects.folderlist) { this.gui_containers.foldertray = $(this.gui_objects.folderlist); + // init treelist widget + if (window.rcube_treelist_widget) { + this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { + id_prefix: 'rcmli', + id_encode: this.html_identifier_encode, + id_decode: this.html_identifier_decode, + check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) } + }); + this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) }); + this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) }); + } + } + // activate html5 file drop feature (if browser supports it and if configured) if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); }); @@ -1263,11 +1276,12 @@ function rcube_webmail() this.html_identifier = function(str, encode) { - str = String(str); - if (encode) - return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); - else - return str.replace(this.identifier_expr, '_'); + return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_'); + }; + + this.html_identifier_encode = function(str) + { + return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); }; this.html_identifier_decode = function(str) @@ -1320,29 +1334,9 @@ function rcube_webmail() if (this.preview_read_timer) clearTimeout(this.preview_read_timer); - // save folderlist and folders location/sizes for droptarget calculation in drag_move() - if (this.gui_objects.folderlist && model) { - this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset; - this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop; - - var k, li, height, - list = $(this.gui_objects.folderlist); - pos = list.offset(); - - this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() }; - - this.env.folder_coords = []; - for (k in model) { - if (li = this.get_folder_li(k)) { - // only visible folders - if (height = li.firstChild.offsetHeight) { - pos = $(li.firstChild).offset(); - this.env.folder_coords[k] = { x1:pos.left, y1:pos.top, - x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 }; - } - } - } - } + // prepare treelist widget for dragging interactions + if (this.treelist) + this.treelist.drag_start(); }; this.drag_end = function(e) @@ -1350,87 +1344,28 @@ function rcube_webmail() this.drag_active = false; this.env.last_folder_target = null; - if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - // over the folders - if (this.gui_objects.folderlist && this.env.folder_coords) { - for (var k in this.env.folder_coords) { - if (this.env.folder_coords[k].on) - $(this.get_folder_li(k)).removeClass('droptarget'); - } - } + if (this.treelist) + this.treelist.drag_end(); }; this.drag_move = function(e) { - if (this.gui_objects.folderlist && this.env.folder_coords) { - var k, li, div, check, oldclass, + if (this.gui_objects.folderlist) { + var drag_target, oldclass, layerclass = 'draglayernormal', - mouse = rcube_event.get_mouse_pos(e), - pos = this.env.folderlist_coords, - // offsets to compensate for scrolling while dragging a message - boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop, - moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop; + mouse = rcube_event.get_mouse_pos(e); if (this.contact_list && this.contact_list.draglayer) oldclass = this.contact_list.draglayer.attr('class'); - mouse.y += -moffset-boffset; - - // if mouse pointer is outside of folderlist - if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) { - if (this.env.last_folder_target) { - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); - this.env.folder_coords[this.env.last_folder_target].on = 0; - this.env.last_folder_target = null; - } - if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) - this.contact_list.draglayer.attr('class', layerclass); - return; + // mouse intersects a valid drop target on the treelist + if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) { + this.env.last_folder_target = drag_target; + layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal'); } - - // over the folders - for (k in this.env.folder_coords) { - pos = this.env.folder_coords[k]; - if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) { - if (check = this.check_droptarget(k)) { - li = this.get_folder_li(k); - div = $(li.getElementsByTagName('div')[0]); - - // if the folder is collapsed, expand it after 1sec and restart the drag & drop process. - if (div.hasClass('collapsed')) { - if (this.folder_auto_timer) - clearTimeout(this.folder_auto_timer); - - this.folder_auto_expand = this.env.mailboxes[k].id; - this.folder_auto_timer = setTimeout(function() { - rcmail.command('collapse-folder', rcmail.folder_auto_expand); - rcmail.drag_start(null); - }, 1000); - } - else if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - $(li).addClass('droptarget'); - this.env.folder_coords[k].on = 1; - this.env.last_folder_target = k; - layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal'); - } - // Clear target, otherwise drag end will trigger move into last valid droptarget - else - this.env.last_folder_target = null; - } - else if (pos.on) { - $(this.get_folder_li(k)).removeClass('droptarget'); - this.env.folder_coords[k].on = 0; - } + else { + // Clear target, otherwise drag end will trigger move into last valid droptarget + this.env.last_folder_target = null; } if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) @@ -1440,40 +1375,33 @@ function rcube_webmail() this.collapse_folder = function(name) { - var li = this.get_folder_li(name, '', true), - div = $('div:first', li), - ul = $('ul:first', li); + if (this.treelist) + this.treelist.toggle(name); + }; + + this.folder_collapsed = function(node) + { + var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders'; - if (div.hasClass('collapsed')) { - ul.show(); - div.removeClass('collapsed').addClass('expanded'); - var reg = new RegExp('&'+urlencode(name)+'&'); - this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, ''); - } - else if (div.hasClass('expanded')) { - ul.hide(); - div.removeClass('expanded').addClass('collapsed'); - this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&'; + if (node.collapsed) { + this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&'; // select the folder if one of its childs is currently selected // don't select if it's virtual (#1488346) - if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual')) + if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual) this.command('list', name); } - else - return; - - // Work around a bug in IE6 and IE7, see #1485309 - if (bw.ie6 || bw.ie7) { - var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null; - if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') { - li.style.display = 'none'; - li.style.display = ''; - } + else { + var reg = new RegExp('&'+urlencode(node.id)+'&'); + this.env[prefname] = this.env[prefname].replace(reg, ''); } - this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders }); - this.set_unread_count_display(name, false); + if (!this.drag_active) { + this.command('save-pref', { name: prefname, value: this.env[prefname] }); + + if (this.env.unread_counts) + this.set_unread_count_display(node.id, false); + } }; this.doc_mouse_up = function(e) @@ -1498,9 +1426,9 @@ function rcube_webmail() if (this.drag_active && model && this.env.last_folder_target) { var target = model[this.env.last_folder_target]; - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); this.env.last_folder_target = null; list.draglayer.hide(); + this.drag_end(e); if (!this.drag_menu(e, target)) this.command('moveto', target); @@ -4164,7 +4092,7 @@ function rcube_webmail() else if (!this.env.search_request) folder = group ? 'G'+src+group : src; - this.select_folder(folder); + this.select_folder(folder, '', true); this.env.source = src; this.env.group = group; @@ -4468,7 +4396,7 @@ function rcube_webmail() this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.env.group_renaming = true; - var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG'); + var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true); if (li && (link = li.firstChild)) { $(link).hide().before(this.name_input); } @@ -4489,7 +4417,7 @@ function rcube_webmail() this.remove_group_item = function(prop) { var li, key = 'G'+prop.source+prop.id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li }); li.parentNode.removeChild(li); @@ -4511,7 +4439,7 @@ function rcube_webmail() this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.name_input_li = $('
  • ').addClass(type).append(this.name_input); - var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source); + var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : $('ul.groups li:last', this.get_folder_li(this.env.source,'',true)); this.name_input_li.insertAfter(li); } @@ -4612,7 +4540,7 @@ function rcube_webmail() this.reset_add_input(); var key = 'G'+prop.source+prop.id, - li = this.get_folder_li(key), + li = this.get_folder_li(key,'',true), link; // group ID has changed, replace link node and identifiers @@ -4651,8 +4579,8 @@ function rcube_webmail() this.add_contact_group_row = function(prop, li, reloc) { var row, name = prop.name.toUpperCase(), - sibling = this.get_folder_li(prop.source), - prefix = 'rcmliG' + this.html_identifier(prop.source); + sibling = this.get_folder_li(prop.source,'',true), + prefix = 'rcmli' + this.html_identifier('G'+prop.source, true); // When renaming groups, we need to remove it from DOM and insert it in the proper place if (reloc) { @@ -4901,12 +4829,12 @@ function rcube_webmail() .attr('rel', id) .click(function() { return rcmail.command('listsearch', id, this); }) .html(name), - li = $('
  • ').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'}) + li = $('
  • ').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' }) .append(link), prop = {name:name, id:id, li:li[0]}; this.add_saved_search_row(prop, li); - this.select_folder('S'+id); + this.select_folder(key,'',true); this.enable_command('search-delete', true); this.env.search_id = id; @@ -4960,7 +4888,7 @@ function rcube_webmail() this.remove_search_item = function(id) { var li, key = 'S'+id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('search_delete', { id:id, li:li }); li.parentNode.removeChild(li); @@ -4982,7 +4910,7 @@ function rcube_webmail() } this.reset_qsearch(); - this.select_folder('S'+id); + this.select_folder('S'+id, '', true); // reset vars this.env.current_page = 1; diff --git a/program/js/treelist.js b/program/js/treelist.js new file mode 100644 index 000000000..47ac0c1a0 --- /dev/null +++ b/program/js/treelist.js @@ -0,0 +1,425 @@ +/* + +-----------------------------------------------------------------------+ + | 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 | + +-----------------------------------------------------------------------+ + | Requires: common.js | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Roundcube Treelist widget class + * @contructor + */ +function rcube_treelist_widget(node, p) +{ + // apply some defaults to p + p = $.extend({ + id_prefix: '', + autoexpand: 1000, + selectable: false, + check_droptarget: function(node){ return !node.virtual } + }, p || {}); + + var container = $(node); + var data = p.data || []; + var indexbyid = {}; + var selection = null; + var drag_active = false; + var box_coords = {}; + var item_coords = []; + var autoexpand_timer; + var autoexpand_item; + var body_scroll_top = 0; + var list_scroll_top = 0; + var me = this; + + + /////// export public members and methods + + this.container = container; + this.expand = expand; + this.collapse = collapse; + this.select = select; + this.render = render; + this.drag_start = drag_start; + this.drag_end = drag_end; + this.intersects = intersects; + + + /////// startup code (constructor) + + // abort if node not found + if (!container.length) + return; + + if (p.data) { + index_data({ children:data }); + } + // load data from DOM + else { + data = walk_list(container); + // console.log(data); + } + + // register click handlers on list + container.on('click', 'div.treetoggle', function(e){ + toggle(dom2id($(this).parent())); + }); + + container.on('click', 'li', function(e){ + var node = p.selectable ? indexbyid[dom2id($(this))] : null; + if (node && !node.virtual) { + select(node.id); + e.stopPropagation(); + } + }); + + + /////// private methods + + /** + * Collaps a the node with the given ID + */ + function collapse(id, recursive, set) + { + var node; + if (node = indexbyid[id]) { + 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); + } + } + + me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node); + } + } + + /** + * Expand a the node with the given ID + */ + function expand(id, recursive) + { + collapse(id, recursive, false); + } + + /** + * Toggle collapsed state of a list node + */ + function toggle(id, recursive) + { + var node; + if (node = indexbyid[id]) { + collapse(id, recursive, !node.collapsed); + } + } + + /** + * Select a tree node by it's ID + */ + function select(id) + { + if (selection) { + id2dom(selection).removeClass('selected'); + selection = null; + } + + var li = id2dom(id); + if (li.length) { + li.addClass('selected'); + selection = id; + // TODO: expand all parent nodes if collapsed + scroll_to_node(li); + } + + me.triggerEvent('select', indexbyid[id]); + } + + /** + * Getter for the currently selected node ID + */ + function get_selection() + { + return selection; + } + + /** + * Return the DOM element of the list item with the given ID + */ + function get_item(id) + { + return id2dom(id).get(0); + } + + /** + * Apply the 'collapsed' status of the data node to the corresponding DOM element(s) + */ + function update_dom(node) + { + var li = id2dom(node.id); + li.children('ul').first()[(node.collapsed ? 'hide' : 'show')](); + li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded'); + me.triggerEvent('toggle', node); + } + + /** + * Render the tree list from the internal data structure + */ + function render() + { + if (me.triggerEvent('renderBefore', data) === false) + return; + + // remove all child nodes + container.html(''); + + // render child nodes + for (var i=0; i < data.length; i++) { + render_node(data[i], container); + } + + me.triggerEvent('renderAfter', container); + } + + /** + * Render a specific node into the DOM list + */ + function render_node(node, parent) + { + var li = $('
  • ' + node.html + '
  • ') + .attr('id', p.id_prefix + node.id) + .addClass((node.classes || []).join(' ')) + .appendTo(parent); + + if (node.virtual) + li.addClass('virtual'); + if (node.id == selection) + li.addClass('selected'); + + // add child list and toggle icon + if (node.children && node.children.length) { + $('
     
    ').appendTo(li); + var ul = $('