From eecb447b2a067f3e9af012e512dec7a7b73f4a6b Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Tue, 1 Jan 2013 12:12:37 +0100 Subject: Fix checkbox/label alignment. We do this by additional span inside label and vertical-align:middle on both checkbox and span. --- skins/classic/templates/addressbook.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'skins/classic/templates/addressbook.html') diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index d77bb38d9..74673007a 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -40,11 +40,11 @@
-- cgit v1.2.3 From 9a6c38e14895bd093627e12f2fcf2c6ff1e3af3c Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 11 Jan 2013 14:39:23 +0100 Subject: New feature to export only selected contacts from addressbook (by Phil Weir) --- CHANGELOG | 1 + program/js/app.js | 8 ++++++++ program/localization/en_US/labels.inc | 2 ++ program/steps/addressbook/export.inc | 23 +++++++++++++++++++++++ skins/classic/addressbook.css | 8 ++++++++ skins/classic/templates/addressbook.html | 10 ++++++++++ skins/larry/styles.css | 2 +- skins/larry/templates/addressbook.html | 12 +++++++++++- 8 files changed, 64 insertions(+), 2 deletions(-) (limited to 'skins/classic/templates/addressbook.html') diff --git a/CHANGELOG b/CHANGELOG index 25d79d1e2..ecc45c220 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Feature to export only selected contacts from addressbook (by Phil Weir) - Force autocommit mode in mysql database driver (#1488902) RELEASE 0.9-beta diff --git a/program/js/app.js b/program/js/app.js index c627983f4..2804e88df 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1090,6 +1090,12 @@ function rcube_webmail() } break; + case 'export-selected': + if (this.contact_list.rowcount > 0) { + this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }); + } + break; + case 'upload-photo': this.upload_contact_photo(props || this.gui_objects.uploadform); break; @@ -4115,6 +4121,7 @@ function rcube_webmail() // thend we can enable the group-remove-selected command this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0); this.enable_command('compose', this.env.group || list.selection.length > 0); + this.enable_command('export-selected', list.selection.length > 0); this.enable_command('edit', id && writable); this.enable_command('delete', list.selection.length && writable); @@ -6238,6 +6245,7 @@ function rcube_webmail() this.enable_command('compose', (uid && this.contact_list.rows[uid])); this.enable_command('delete', 'edit', writable); this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0)); + this.enable_command('export-selected', false); } case 'moveto': diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 9deaa6677..a0b6e6a31 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -335,6 +335,8 @@ $labels['composeto'] = 'Compose mail to'; $labels['contactsfromto'] = 'Contacts $from to $to of $count'; $labels['print'] = 'Print'; $labels['export'] = 'Export'; +$labels['exportall'] = 'Export all'; +$labels['exportsel'] = 'Export selected'; $labels['exportvcards'] = 'Export contacts in vCard format'; $labels['newcontactgroup'] = 'Create new contact group'; $labels['grouprename'] = 'Rename group'; diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 850795c85..bf0657b74 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -56,6 +56,29 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search $result = new rcube_result_set($count); $result->records = array_values($records); } +// selected contacts +else if (!empty($_REQUEST['_cid'])) { + $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name'); + $records = array(); + + $cids = explode(',', get_input_value('_cid', RCUBE_INPUT_GET)); + $CONTACTS = rcmail_contact_source(null, true); + + // Get records from all sources + foreach ($cids as $cid) { + $record = $CONTACTS->get_record($cid, true); + $key = rcmail_contact_key($record, $sort_col); + $records[$key] = $record; + unset($record); + } + + ksort($records, SORT_LOCALE_STRING); + + // create resultset object + $count = count($records); + $result = new rcube_result_set($count); + $result->records = array_values($records); +} // selected directory/group else { $CONTACTS = rcmail_contact_source(null, true); diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css index a398325b4..1bb1e2c61 100644 --- a/skins/classic/addressbook.css +++ b/skins/classic/addressbook.css @@ -67,6 +67,14 @@ background-position: -128px -32px; } +#abooktoolbar a.exportAll { + background-position: -128px 0; +} + +#abooktoolbar a.exportAllSel { + background-position: -128px -32px; +} + #abooktoolbar span.separator { width: 5px; background-position: -162px 0; diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index 74673007a..404fb2c11 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -27,7 +27,10 @@   + + + @@ -38,6 +41,13 @@ +
+
    +
  • +
  • +
+
+
  • diff --git a/skins/larry/styles.css b/skins/larry/styles.css index 9386c79d7..773ef23d4 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -1680,6 +1680,7 @@ ul.proplist li { } .toolbar a.button.export { + min-width: 74px; background-position: center -1054px; } @@ -1695,7 +1696,6 @@ ul.proplist li { background-position: 0 -1745px; } - a.menuselector { display: inline-block; border: 1px solid #ababab; diff --git a/skins/larry/templates/addressbook.html b/skins/larry/templates/addressbook.html index 9a9a2d747..7904f6f3a 100644 --- a/skins/larry/templates/addressbook.html +++ b/skins/larry/templates/addressbook.html @@ -13,7 +13,11 @@
    - + + + + + @@ -75,6 +79,12 @@
    +
    +
      +
    • +
    • +
    +
      -- cgit v1.2.3 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 +- skins/classic/addressbook.css | 35 ++- skins/classic/templates/addressbook.html | 3 +- skins/larry/addressbook.css | 21 +- skins/larry/templates/addressbook.html | 2 +- 8 files changed, 566 insertions(+), 170 deletions(-) create mode 100644 program/js/treelist.js (limited to 'skins/classic/templates/addressbook.html') 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 = $('
        ').appendTo(li); + if (node.collapsed) + ul.hide(); + + for (var i=0; i < node.children.length; i++) { + render_node(node.children[i], ul); + } + } + } + + /** + * Recursively walk the DOM tree and build an internal data structure + * representing the skeleton of this tree list. + */ + function walk_list(ul) + { + var result = []; + ul.children('li').each(function(i,e){ + var li = $(e); + var node = { + id: dom2id(li), + classes: li.attr('class').split(' '), + virtual: li.hasClass('virtual'), + html: li.children().first().get(0).outerHTML, + children: walk_list(li.children('ul')) + } + + if (node.children.length) { + node.collapsed = li.children('ul').css('display') == 'none'; + } + if (li.hasClass('selected')) { + selection = node.id; + } + + result.push(node); + indexbyid[node.id] = node; + }) + + return result; + } + + /** + * Recursively walk the data tree and index nodes by their ID + */ + function index_data(node) + { + if (node.id) { + indexbyid[node.id] = node; + } + for (var c=0; node.children && c < node.children.length; c++) { + index_data(node.children[c]); + } + } + + /** + * Get the (stripped) node ID from the given DOM element + */ + function dom2id(li) + { + var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), ''); + return p.id_decode ? p.id_decode(domid) : domid; + } + + /** + * Get the
      • element for the given node ID + */ + function id2dom(id) + { + var domid = p.id_encode ? p.id_encode(id) : id; + return $('#' + p.id_prefix + domid); + } + + /** + * Scroll the parent container to make the given list item visible + */ + function scroll_to_node(li) + { + var scroller = container.parent(); + scroller.scrollTop(li.offset().top - scroller.offset().top + scroller.scrollTop()); + } + + ///// 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() + { + var li, item, height, + pos = container.offset(); + + body_scroll_top = bw.ie ? 0 : window.pageYOffset; + list_scroll_top = container.parent().scrollTop(); + + drag_active = true; + 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 (height = item.offsetHeight) { + pos = $(item).offset(); + item_coords[id] = { + x1: pos.left, + y1: pos.top, + x2: pos.left + item.offsetWidth, + y2: pos.top + height, + on: id == autoexpand_item + }; + } + } + } + + /** + * Signal that dragging has stopped + */ + function drag_end() + { + drag_active = false; + + if (autoexpand_timer) { + clearTimeout(autoexpand_timer); + autoexpand_timer = null; + autoexpand_item = null; + } + + $('li.droptarget', container).removeClass('droptarget'); + } + + /** + * Determine if the given mouse coords intersect the list and one if 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 = list_scroll_top - 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 + $('li.droptarget', container).removeClass('droptarget'); + return result; + } + + // check intersection with visible list items + var pos, node; + for (var 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(); // re-calculate item coords + autoexpand_item = 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; + } +} + +// 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; diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 7fb862d5e..80631cd61 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -187,7 +187,7 @@ function rcmail_directory_list($attrib) $jsdata = array(); $line_templ = html::tag('li', array( - 'id' => 'rcmli%s', 'class' => '%s'), + 'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true), html::a(array('href' => '%s', 'rel' => '%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s')); @@ -213,7 +213,7 @@ function rcmail_directory_list($attrib) $name = !empty($source['name']) ? $source['name'] : $id; $out .= sprintf($line_templ, - html_identifier($id), + rcube_utils::html_identifier($id, true), $class_name, Q(rcmail_url(null, array('_source' => $id))), $source['id'], @@ -224,10 +224,11 @@ function rcmail_directory_list($attrib) $groupdata = rcmail_contact_groups($groupdata); $jsdata = $groupdata['jsdata']; $out = $groupdata['out']; + $out .= '
      • '; } $line_templ = html::tag('li', array( - 'id' => 'rcmliS%s', 'class' => '%s'), + 'id' => 'rcmli%s', 'class' => '%s'), html::a(array('href' => '#', 'rel' => 'S%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s')); @@ -245,14 +246,17 @@ function rcmail_directory_list($attrib) $class_name .= ' ' . $source['class_name']; $out .= sprintf($line_templ, - html_identifier($id), + rcube_utils::html_identifier('S'.$id, true), $class_name, $id, $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id))); } $OUTPUT->set_env('contactgroups', $jsdata); + $OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks','')); $OUTPUT->add_gui_object('folderlist', $attrib['id']); + $OUTPUT->include_script('treelist.js'); + // add some labels to client $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember'); @@ -265,18 +269,24 @@ function rcmail_contact_groups($args) global $RCMAIL; $groups = $RCMAIL->get_address_book($args['source'])->list_groups(); + $js_id = $RCMAIL->JQ($args['source']); if (!empty($groups)) { $line_templ = html::tag('li', array( - 'id' => 'rcmliG%s', 'class' => 'contactgroup'), + 'id' => 'rcmli%s', 'class' => 'contactgroup'), html::a(array('href' => '#', 'rel' => '%s:%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s')); + // append collapse/expand toggle and open a new
          + $is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false; + $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); + $jsdata = array(); + $groups_html = ''; foreach ($groups as $group) { - $args['out'] .= sprintf($line_templ, - html_identifier($args['source'] . $group['ID']), + $groups_html .= sprintf($line_templ, + rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true), $args['source'], $group['ID'], $args['source'], $group['ID'], Q($group['name']) ); @@ -286,6 +296,10 @@ function rcmail_contact_groups($args) } } + $args['out'] .= html::tag('ul', + array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), + $groups_html); + return $args; } diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css index 78314538a..5afa4592f 100644 --- a/skins/classic/addressbook.css +++ b/skins/classic/addressbook.css @@ -118,7 +118,7 @@ #directorylistbox input { - margin: 0px; + margin: 0 0 0 20px; font-size: 11px; width: 90%; } @@ -144,7 +144,8 @@ width: 280px; } -#directorylist +#directorylist, +#directorylist li ul { list-style: none; margin: 0; @@ -152,11 +153,15 @@ background-color: #FFFFFF; } +#directorylist li ul +{ + border-top: 1px solid #EBEBEB; +} + #directorylist li { display: block; font-size: 11px; - background: url(images/icons/folders.png) 5px -108px no-repeat; border-bottom: 1px solid #EBEBEB; white-space: nowrap; } @@ -168,31 +173,37 @@ padding-left: 25px; padding-top: 2px; padding-bottom: 2px; + height: 16px; text-decoration: none; white-space: nowrap; + background: url(images/icons/folders.png) 5px -108px no-repeat; } -#directorylist li.contactgroup +#directorylist li ul li a { - padding-left: 15px; - background-position: 20px -143px; + padding-left: 45px; } -#directorylist li.contactsearch +#directorylist li ul li:last-child { - background-position: 6px -162px; + border-bottom: 0; } -#directorylist li.selected +#directorylist li.contactgroup a { - background-color: #929292; - border-bottom: 1px solid #898989; + background-position: 22px -143px; +} + +#directorylist li.contactsearch a +{ + background-position: 6px -162px; } -#directorylist li.selected a +#directorylist li.selected > a { color: #FFF; font-weight: bold; + background-color: #929292; } #directorylist li.droptarget diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index 404fb2c11..9d959d5ef 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -63,8 +63,7 @@
          - - +