From f0928eac13b9161e251a779f229e37ec526fd590 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Tue, 03 Jun 2014 13:13:05 -0400 Subject: [PATCH] Fix keyboard navigation and focus/blur behavior in list widgets; register real link element as menu opener --- program/js/app.js | 1370 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 files changed, 975 insertions(+), 395 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 28a1b38..ea7265d 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1,23 +1,37 @@ -/* - +-----------------------------------------------------------------------+ - | Roundcube Webmail Client Script | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2013, The Roundcube Dev Team | - | 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. | - | See the README file for a full license statement. | - | | - +-----------------------------------------------------------------------+ - | Authors: Thomas Bruederli <roundcube@gmail.com> | - | Aleksander 'A.L.E.C' Machniak <alec@alec.pl> | - | Charles McNulty <charles@charlesmcnulty.com> | - +-----------------------------------------------------------------------+ - | Requires: jquery.js, common.js, list.js | - +-----------------------------------------------------------------------+ -*/ +/** + * Roundcube Webmail Client Script + * + * 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) 2005-2014, The Roundcube Dev Team + * Copyright (C) 2011-2014, Kolab Systems AG + * + * 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> + * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl> + * @author Charles McNulty <charles@charlesmcnulty.com> + * + * @requires jquery.js, common.js, list.js + */ function rcube_webmail() { @@ -31,6 +45,8 @@ this.onloads = []; this.messages = {}; this.group2expand = {}; + this.http_request_jobs = {}; + this.menu_stack = new Array(); // webmail client settings this.dblclick_time = 500; @@ -138,11 +154,11 @@ // initialize webmail client this.init = function() { - var n, p = this; + var n; this.task = this.env.task; // check browser - if (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9)) { + if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9) || (bw.ie && bw.vendver < 7))) { this.goto_url('error', '_code=0x199'); return; } @@ -182,7 +198,10 @@ // enable general commands this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', - 'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true); + 'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true); + + // set active task button + this.set_button(this.task, 'sel'); if (this.env.permaurl) this.enable_command('permaurl', 'extwin', true); @@ -199,23 +218,28 @@ column_movable:this.env.col_movable, dblclick_time:this.dblclick_time }); this.message_list - .addEventListener('initrow', function(o) { p.init_message_row(o); }) - .addEventListener('dblclick', function(o) { p.msglist_dbl_click(o); }) - .addEventListener('click', function(o) { p.msglist_click(o); }) - .addEventListener('keypress', function(o) { p.msglist_keypress(o); }) - .addEventListener('select', function(o) { p.msglist_select(o); }) - .addEventListener('dragstart', function(o) { p.drag_start(o); }) - .addEventListener('dragmove', function(e) { p.drag_move(e); }) - .addEventListener('dragend', function(e) { p.drag_end(e); }) - .addEventListener('expandcollapse', function(o) { p.msglist_expand(o); }) - .addEventListener('column_replace', function(o) { p.msglist_set_coltypes(o); }) - .addEventListener('listupdate', function(o) { p.triggerEvent('listupdate', o); }) + .addEventListener('initrow', function(o) { ref.init_message_row(o); }) + .addEventListener('dblclick', function(o) { ref.msglist_dbl_click(o); }) + .addEventListener('click', function(o) { ref.msglist_click(o); }) + .addEventListener('keypress', function(o) { ref.msglist_keypress(o); }) + .addEventListener('select', function(o) { ref.msglist_select(o); }) + .addEventListener('dragstart', function(o) { ref.drag_start(o); }) + .addEventListener('dragmove', function(e) { ref.drag_move(e); }) + .addEventListener('dragend', function(e) { ref.drag_end(e); }) + .addEventListener('expandcollapse', function(o) { ref.msglist_expand(o); }) + .addEventListener('column_replace', function(o) { ref.msglist_set_coltypes(o); }) + .addEventListener('listupdate', function(o) { ref.triggerEvent('listupdate', o); }) .init(); - document.onmouseup = function(e){ return p.doc_mouse_up(e); }; - this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); }; + // TODO: this should go into the list-widget code + $(this.message_list.thead).on('click', 'a.sortcol', function(e){ + return ref.command('sort', $(this).attr('rel'), this); + }); + + this.gui_objects.messagelist.parentNode.onclick = function(e){ return ref.click_on_list(e || window.event); }; this.enable_command('toggle_status', 'toggle_flag', 'sort', true); + this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing()); // load messages this.command('list'); @@ -255,7 +279,7 @@ this.env.address_group_stack = []; this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin', - 'insert-response', 'save-response']; + 'insert-response', 'save-response', 'menu-open', 'menu-close']; if (this.env.drafts_mailbox) this.env.compose_commands.push('savedraft') @@ -276,10 +300,12 @@ $('a.insertresponse', this.gui_objects.responseslist) .attr('unselectable', 'on') .mousedown(function(e){ return rcube_event.cancel(e); }) - .mouseup(function(e){ - ref.command('insert-response', $(this).attr('rel')); - $(document.body).trigger('mouseup'); // hides the menu - return rcube_event.cancel(e); + .bind('mouseup keypress', function(e){ + if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { + ref.command('insert-response', $(this).attr('rel')); + $(document.body).trigger('mouseup'); // hides the menu + return rcube_event.cancel(e); + } }); // avoid textarea loosing focus when hitting the save-response button/link @@ -287,8 +313,6 @@ $('#'+this.buttons['save-response'][i].id).mousedown(function(e){ return rcube_event.cancel(e); }) } } - - document.onmouseup = function(e){ return p.doc_mouse_up(e); }; // init message compose form this.init_messageform(); @@ -313,11 +337,21 @@ // init address book widget if (this.gui_objects.contactslist) { this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, - { multiselect:true, draggable:false, keyboard:false }); + { multiselect:true, draggable:false, keyboard:true }); this.contact_list - .addEventListener('initrow', function(o) { p.triggerEvent('insertrow', { cid:o.uid, row:o }); }) + .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); }) .addEventListener('select', function(o) { ref.compose_recipient_select(o); }) .addEventListener('dblclick', function(o) { ref.compose_add_recipient('to'); }) + .addEventListener('keypress', function(o) { + if (o.key_pressed == o.ENTER_KEY) { + if (!ref.compose_add_recipient('to')) { + // execute link action on <enter> if not a recipient entry + if (o.last_selected && String(o.last_selected).charAt(0) == 'G') { + $(o.rows[o.last_selected].obj).find('a').first().click(); + } + } + } + }) .init(); } @@ -356,21 +390,20 @@ this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, {multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true}); this.contact_list - .addEventListener('initrow', function(o) { p.triggerEvent('insertrow', { cid:o.uid, row:o }); }) - .addEventListener('keypress', function(o) { p.contactlist_keypress(o); }) - .addEventListener('select', function(o) { p.contactlist_select(o); }) - .addEventListener('dragstart', function(o) { p.drag_start(o); }) - .addEventListener('dragmove', function(e) { p.drag_move(e); }) - .addEventListener('dragend', function(e) { p.drag_end(e); }) + .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); }) + .addEventListener('keypress', function(o) { ref.contactlist_keypress(o); }) + .addEventListener('select', function(o) { ref.contactlist_select(o); }) + .addEventListener('dragstart', function(o) { ref.drag_start(o); }) + .addEventListener('dragmove', function(e) { ref.drag_move(e); }) + .addEventListener('dragend', function(e) { ref.drag_end(e); }) .init(); if (this.env.cid) this.contact_list.highlight_row(this.env.cid); - this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return p.click_on_list(e); }; - document.onmouseup = function(e){ return p.doc_mouse_up(e); }; + this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); }; - $(this.gui_objects.qsearchbox).focusin(function() { rcmail.contact_list.blur(); }); + $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); }); this.update_group_commands(); this.command('list'); @@ -420,9 +453,14 @@ if (this.gui_objects.identitieslist) { this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, - {multiselect:false, draggable:false, keyboard:false}); + {multiselect:false, draggable:false, keyboard:true}); this.identity_list - .addEventListener('select', function(o) { p.identity_select(o); }) + .addEventListener('select', function(o) { ref.identity_select(o); }) + .addEventListener('keypress', function(o) { + if (o.key_pressed == o.ENTER_KEY) { + ref.identity_select(o); + } + }) .init() .focus(); @@ -430,9 +468,10 @@ this.identity_list.highlight_row(this.env.iid); } else if (this.gui_objects.sectionslist) { - this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false}); + this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true}); this.sections_list - .addEventListener('select', function(o) { p.section_select(o); }) + .addEventListener('select', function(o) { ref.section_select(o); }) + .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); }) .init() .focus(); } @@ -440,14 +479,14 @@ this.init_subscription_list(); } else if (this.gui_objects.responseslist) { - this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:false}); + this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true}); this.responses_list .addEventListener('select', function(list) { var win, id = list.get_single_selection(); - p.enable_command('delete', !!id && $.inArray(id, p.env.readonly_responses) < 0); - if (id && (win = p.get_frame_window(p.env.contentframe))) { - p.set_busy(true); - p.location_href({ _action:'edit-response', _key:id, _framed:1 }, win); + ref.enable_command('delete', !!id && $.inArray(id, ref.env.readonly_responses) < 0); + if (id && (win = ref.get_frame_window(ref.env.contentframe))) { + ref.set_busy(true); + ref.location_href({ _action:'edit-response', _key:id, _framed:1 }, win); } }) .init() @@ -466,7 +505,7 @@ $('#rcmloginpwd').focus(); // detect client timezone - if (window.jstz && !bw.ie6) { + if (window.jstz) { var timezone = jstz.determine(); if (timezone.name()) $('#rcmlogintz').val(timezone.name()); @@ -534,6 +573,18 @@ .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false); } + // catch document (and iframe) mouse clicks + var body_mouseup = function(e){ return ref.doc_mouse_up(e); }; + $(document.body) + .bind('mouseup', body_mouseup) + .bind('keydown', function(e){ return ref.doc_keypress(e); }); + + $('iframe').load(function(e) { + try { $(this.contentDocument || this.contentWindow).on('mouseup', body_mouseup); } + catch (e) {/* catch possible "Permission denied" error in IE */ } + }) + .contents().on('mouseup', body_mouseup); + // trigger init event hook this.triggerEvent('init', { task:this.task, action:this.env.action }); @@ -564,12 +615,13 @@ // execute a specific command on the web client this.command = function(command, props, obj, event) { - var ret, uid, cid, url, flag; + var ret, uid, cid, url, flag, aborted = false; - if (obj && obj.blur) + if (obj && obj.blur && !(event || rcube_event.is_keyboard(event))) obj.blur(); - if (this.busy) + // do nothing if interface is locked by other command (with exception for searching reset) + if (this.busy && !(command == 'reset-search' && this.last_command == 'search')) return false; // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab) @@ -595,6 +647,8 @@ this.remove_compose_data(this.env.compose_id); } + this.last_command = command; + // process external commands if (typeof this.command_handlers[command] === 'function') { ret = this.command_handlers[command](props, obj); @@ -606,8 +660,8 @@ } // trigger plugin hooks - this.triggerEvent('actionbefore', {props:props, action:command}); - ret = this.triggerEvent('before'+command, props); + this.triggerEvent('actionbefore', {props:props, action:command, originalEvent:event}); + ret = this.triggerEvent('before'+command, props || event); if (ret !== undefined) { // abort if one of the handlers returned false if (ret === false) @@ -682,14 +736,20 @@ var mimetype = this.env.attachments[props.id]; this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0); } + this.show_menu(props, props.show || undefined, event); + break; + + case 'menu-close': + this.hide_menu(props, event); + break; case 'menu-save': - this.triggerEvent(command, {props:props}); + this.triggerEvent(command, {props:props, originalEvent:event}); return false; case 'open': if (uid = this.get_single_uid()) { - obj.href = this.url('show', {_mbox: this.env.mailbox, _uid: uid}); + obj.href = this.url('show', {_mbox: this.get_message_mailbox(uid), _uid: uid}); return true; } break; @@ -700,8 +760,17 @@ break; case 'list': - if (props && props != '') - this.reset_qsearch(); + // re-send search query for the selected folder + if (props && props != '' && this.env.search_request && this.gui_objects.qsearchbox.value) { + var oldmbox = this.env.search_scope == 'all' ? '*' : this.env.mailbox; + this.env.search_mods[props] = this.env.search_mods[oldmbox]; // copy search mods from active search + this.env.mailbox = props; + this.env.search_scope = 'sub'; + this.qsearch(this.gui_objects.qsearchbox.value); + this.select_folder(this.env.mailbox, '', true); + break; + } + if (this.env.action == 'compose' && this.env.extwin) window.close(); else if (this.task == 'mail') { @@ -710,6 +779,10 @@ } else if (this.task == 'addressbook') this.list_contacts(props); + break; + + case 'set-listmode': + this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0); break; case 'sort': @@ -792,9 +865,9 @@ this.load_contact(cid, 'edit'); else if (this.task == 'settings' && props) this.load_identity(props, 'edit-identity'); - else if (this.task == 'mail' && (cid = this.get_single_uid())) { - url = { _mbox: this.env.mailbox }; - url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = cid; + else if (this.task == 'mail' && (uid = this.get_single_uid())) { + url = { _mbox: this.get_message_mailbox(uid) }; + url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid; this.open_compose_step(url); } break; @@ -856,14 +929,14 @@ case 'move': case 'moveto': // deprecated if (this.task == 'mail') - this.move_messages(props, obj); + this.move_messages(props, event); else if (this.task == 'addressbook') this.move_contacts(props); break; case 'copy': if (this.task == 'mail') - this.copy_messages(props, obj); + this.copy_messages(props, event); else if (this.task == 'addressbook') this.copy_contacts(props); break; @@ -1054,7 +1127,11 @@ // Reset the auto-save timer clearTimeout(this.save_timer); - this.upload_file(props || this.gui_objects.uploadform, 'upload'); + if (!(flag = this.upload_file(props || this.gui_objects.uploadform, 'upload'))) { + if (flag !== false) + alert(this.get_label('selectimportfile')); + aborted = true; + } break; case 'insert-sig': @@ -1074,7 +1151,7 @@ case 'reply-list': case 'reply': if (uid = this.get_single_uid()) { - url = {_reply_uid: uid, _mbox: this.env.mailbox}; + url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid)}; if (command == 'reply-all') // do reply-list, when list is detected and popup menu wasn't used url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all'); @@ -1090,7 +1167,7 @@ case 'forward': var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []); if (uids.length) { - url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox }; + url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox, _search: this.env.search_request }; if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1) url._attachment = 1; this.open_compose_step(url); @@ -1102,7 +1179,7 @@ this.gui_objects.messagepartframe.contentWindow.print(); } else if (uid = this.get_single_uid()) { - ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''), true, true); + ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.get_message_mailbox(uid))+(this.env.safemode ? '&_safe=1' : ''), true, true); if (this.printwin) { if (this.env.action != 'show') this.mark_message('read', uid); @@ -1119,8 +1196,9 @@ if (this.env.action == 'get') { location.href = location.href.replace(/_frame=/, '_download='); } - else if (uid = this.get_single_uid()) - this.goto_url('viewsource', { _uid: uid, _mbox: this.env.mailbox, _save: 1 }); + else if (uid = this.get_single_uid()) { + this.goto_url('viewsource', { _uid: uid, _mbox: this.get_message_mailbox(uid), _save: 1 }); + } break; // quicksearch @@ -1176,9 +1254,17 @@ break; case 'import-messages': - var form = props || this.gui_objects.importform; - $('input[name="_unlock"]', form).val(this.set_busy(true, 'importwait')); - this.upload_file(form, 'import'); + var form = props || this.gui_objects.importform, + importlock = this.set_busy(true, 'importwait'); + + $('input[name="_unlock"]', form).val(importlock); + + if (!(flag = this.upload_file(form, 'import'))) { + this.set_busy(false, null, importlock); + if (flag !== false) + alert(this.get_label('selectimportfile')); + aborted = true; + } break; case 'import': @@ -1186,6 +1272,7 @@ var file = document.getElementById('rcmimportfile'); if (file && !file.value) { alert(this.get_label('selectimportfile')); + aborted = true; break; } this.gui_objects.importform.submit(); @@ -1237,9 +1324,9 @@ break; } - if (this.triggerEvent('after'+command, props) === false) + if (!aborted && this.triggerEvent('after'+command, props) === false) ret = false; - this.triggerEvent('actionafter', {props:props, action:command}); + this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted }); return ret === false ? false : obj ? false : true; }; @@ -1265,6 +1352,11 @@ } } }; + + this.command_enabled = function(cmd) + { + return this.commands[cmd]; + } // lock/unlock interface this.set_busy = function(a, message, id) @@ -1406,7 +1498,8 @@ if (menu && modkey == SHIFT_KEY && this.commands['copy']) { var pos = rcube_event.get_mouse_pos(e); this.env.drag_target = target; - $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'}).show(); + this.show_menu(this.gui_objects.dragmenu.id, true, e); + $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'}); return true; } @@ -1522,17 +1615,22 @@ } }; + // global mouse-click handler to cleanup some UI elements this.doc_mouse_up = function(e) { - var list, id; + var list, id, target = rcube_event.get_target(e); // ignore event if jquery UI dialog is open - if ($(rcube_event.get_target(e)).closest('.ui-dialog, .ui-widget-overlay').length) + if ($(target).closest('.ui-dialog, .ui-widget-overlay').length) return; - list = this.message_list || this.contact_list; - if (list && !rcube_mouse_is_over(e, list.list.parentNode)) - list.blur(); + // remove focus from list widgets + if (window.rcube_list_widget && rcube_list_widget._instances.length) { + $.each(rcube_list_widget._instances, function(i,list){ + if (list && !rcube_mouse_is_over(e, list.list.parentNode)) + list.blur(); + }); + } // reset 'pressed' buttons if (this.buttons_sel) { @@ -1541,7 +1639,77 @@ this.button_out(this.buttons_sel[id], id); this.buttons_sel = {}; } + + // reset popup menus; delayed to have updated menu_stack data + window.setTimeout(function(e){ + var obj, skip, config, id, i, parents = $(target).parents(); + for (i = ref.menu_stack.length - 1; i >= 0; i--) { + id = ref.menu_stack[i]; + obj = $('#' + id); + + if (obj.is(':visible') + && target != obj.data('opener') + && target != obj.get(0) // check if scroll bar was clicked (#1489832) + && !parents.is(obj.data('opener')) + && id != skip + && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length) + && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0))) + ) { + ref.hide_menu(id, e); + } + skip = obj.data('parent'); + } + }, 10); }; + + // global keypress event handler + this.doc_keypress = function(e) + { + // Helper method to move focus to the next/prev active menu item + var focus_menu_item = function(dir) { + var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first'; + if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) { + item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit](); + if (!item.length) + item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit](); + return item.focus().length; + } + + return 0; + }; + + var target = e.target || {}, + keyCode = rcube_event.get_keycode(e); + + if (e.keyCode != 27 && (!this.menu_keyboard_active || 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 + focus_menu_item(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1); + break; + + case 9: // tab + if (this.focused_menu) { + var mod = rcube_event.get_modifier(e); + if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) { + this.hide_menu(this.focused_menu, e); + } + } + return rcube_event.cancel(e); + + case 27: // esc + if (this.menu_stack.length) + this.hide_menu(this.menu_stack[this.menu_stack.length-1], e); + break; + } + + return true; + } this.click_on_list = function(e) { @@ -1549,9 +1717,9 @@ this.gui_objects.qsearchbox.blur(); if (this.message_list) - this.message_list.focus(); + this.message_list.focus(e); else if (this.contact_list) - this.contact_list.focus(); + this.contact_list.focus(e); return true; }; @@ -1621,7 +1789,7 @@ var uid = list.get_single_selection(); - if (uid && this.env.mailbox == this.env.drafts_mailbox) + if (uid && (this.env.messages[uid].mbox || this.env.mailbox) == this.env.drafts_mailbox) this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox }); else if (uid) this.show_message(uid, false, false); @@ -1662,28 +1830,30 @@ { var i, found, name, cols = list.thead.rows[0].cells; - this.env.coltypes = []; + this.env.listcols = []; for (i=0; i<cols.length; i++) if (cols[i].id && cols[i].id.startsWith('rcm')) { name = cols[i].id.slice(3); - this.env.coltypes.push(name); + this.env.listcols.push(name); } - if ((found = $.inArray('flag', this.env.coltypes)) >= 0) + if ((found = $.inArray('flag', this.env.listcols)) >= 0) this.env.flagged_col = found; - if ((found = $.inArray('subject', this.env.coltypes)) >= 0) + if ((found = $.inArray('subject', this.env.listcols)) >= 0) this.env.subject_col = found; - this.command('save-pref', { name: 'list_cols', value: this.env.coltypes, session: 'list_attrib/columns' }); + this.command('save-pref', { name: 'list_cols', value: this.env.listcols, session: 'list_attrib/columns' }); }; this.check_droptarget = function(id) { switch (this.task) { case 'mail': - return (this.env.mailboxes[id] && this.env.mailboxes[id].id != this.env.mailbox && !this.env.mailboxes[id].virtual) ? 1 : 0; + return (this.env.mailboxes[id] + && !this.env.mailboxes[id].virtual + && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0; case 'settings': return id != this.env.mailbox ? 1 : 0; @@ -1752,31 +1922,31 @@ this.init_message_row = function(row) { - var i, fn = {}, self = this, uid = row.uid, - status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.uid; + var i, fn = {}, uid = row.uid, + status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id; if (uid && this.env.messages[uid]) $.extend(row, this.env.messages[uid]); // set eventhandler to status icon if (row.icon = document.getElementById(status_icon)) { - fn.icon = function(e) { self.command('toggle_status', uid); }; + fn.icon = function(e) { ref.command('toggle_status', uid); }; } // save message icon position too if (this.env.status_col != null) - row.msgicon = document.getElementById('msgicn'+row.uid); + row.msgicon = document.getElementById('msgicn'+row.id); else row.msgicon = row.icon; // set eventhandler to flag icon - if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.uid))) { - fn.flagicon = function(e) { self.command('toggle_flag', uid); }; + if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) { + fn.flagicon = function(e) { ref.command('toggle_flag', uid); }; } // set event handler to thread expand/collapse icon - if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.uid))) { - fn.expando = function(e) { self.expand_message_row(e, uid); }; + if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) { + fn.expando = function(e) { ref.expand_message_row(e, uid); }; } // attach events @@ -1822,11 +1992,12 @@ selected: this.select_all_mode || this.message_list.in_selection(uid), ml: flags.ml?1:0, ctype: flags.ctype, + mbox: flags.mbox, // flags from plugins flags: flags.extra_flags }); - var c, n, col, html, css_class, + var c, n, col, html, css_class, label, status_class = '', status_label = '', tree = '', expando = '', list = this.message_list, rows = list.rows, @@ -1836,23 +2007,32 @@ + (flags.deleted ? ' deleted' : '') + (flags.flagged ? ' flagged' : '') + (message.selected ? ' selected' : ''), - row = { cols:[], style:{}, id:'rcmrow'+uid }; + row = { cols:[], style:{}, id:'rcmrow'+this.html_identifier(uid,true), uid:uid }; // message status icons css_class = 'msgicon'; if (this.env.status_col === null) { css_class += ' status'; - if (flags.deleted) - css_class += ' deleted'; - else if (!flags.seen) - css_class += ' unread'; - else if (flags.unread_children > 0) - css_class += ' unreadchildren'; + if (flags.deleted) { + status_class += ' deleted'; + status_label += this.get_label('deleted') + ' '; + } + else if (!flags.seen) { + status_class += ' unread'; + status_label += this.get_label('unread') + ' '; + } + else if (flags.unread_children > 0) { + status_class += ' unreadchildren'; + } } - if (flags.answered) - css_class += ' replied'; - if (flags.forwarded) - css_class += ' forwarded'; + if (flags.answered) { + status_class += ' replied'; + status_label += this.get_label('replied') + ' '; + } + if (flags.forwarded) { + status_class += ' forwarded'; + status_label += this.get_label('replied') + ' '; + } // update selection if (message.selected && !list.in_selection(uid)) @@ -1862,7 +2042,7 @@ if (this.env.threading) { if (message.depth) { // This assumes that div width is hardcoded to 15px, - tree += '<span id="rcmtab' + uid + '" class="branch" style="width:' + (message.depth * 15) + 'px;"> </span>'; + tree += '<span id="rcmtab' + row.id + '" class="branch" style="width:' + (message.depth * 15) + 'px;"> </span>'; if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false) || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) && @@ -1881,7 +2061,7 @@ message.expanded = true; } - expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '"> </div>'; + expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '"> </div>'; row_class += ' thread' + (message.expanded? ' expanded' : ''); } @@ -1889,60 +2069,78 @@ row_class += ' unroot'; } - tree += '<span id="msgicn'+uid+'" class="'+css_class+'"> </span>'; + tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>'; row.className = row_class; - // build subject link + // build subject link if (cols.subject) { - var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show'; - var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid'; - cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+uid+'"'+ - ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')"><span>'+cols.subject+'</span></a>'; + var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show', + uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid', + query = { _mbox: flags.mbox }; + query[uid_param] = uid; + cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' + + ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>'; } // add each submitted col - for (n in this.env.coltypes) { - c = this.env.coltypes[n]; - col = { className: String(c).toLowerCase() }; + for (n in this.env.listcols) { + c = this.env.listcols[n]; + col = {className: String(c).toLowerCase(), events:{}}; + + if (this.env.coltypes[c] && this.env.coltypes[c].hidden) { + col.className += ' hidden'; + } if (c == 'flag') { css_class = (flags.flagged ? 'flagged' : 'unflagged'); - html = '<span id="flagicn'+uid+'" class="'+css_class+'"> </span>'; + label = this.get_label(css_class); + html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>'; } else if (c == 'attachment') { - if (/application\/|multipart\/(m|signed)/.test(flags.ctype)) - html = '<span class="attachment"> </span>'; + label = this.get_label('withattachment'); + if (flags.attachmentClass) + html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>'; + else if (/application\/|multipart\/(m|signed)/.test(flags.ctype)) + html = '<span class="attachment" title="'+label+'"></span>'; else if (/multipart\/report/.test(flags.ctype)) - html = '<span class="report"> </span>'; - else + html = '<span class="report"></span>'; + else html = ' '; } else if (c == 'status') { - if (flags.deleted) + label = ''; + if (flags.deleted) { css_class = 'deleted'; - else if (!flags.seen) + label = this.get_label('deleted'); + } + else if (!flags.seen) { css_class = 'unread'; - else if (flags.unread_children > 0) + label = this.get_label('unread'); + } + else if (flags.unread_children > 0) { css_class = 'unreadchildren'; + } else css_class = 'msgicon'; - html = '<span id="statusicn'+uid+'" class="'+css_class+'"> </span>'; + html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>'; } else if (c == 'threads') html = expando; else if (c == 'subject') { - if (bw.ie) { - col.onmouseover = function() { rcube_webmail.long_subject_title_ex(this, message.depth+1); }; - if (bw.ie8) - tree = '<span></span>' + tree; // #1487821 - } + if (bw.ie) + col.events.mouseover = function() { rcube_webmail.long_subject_title_ex(this); }; html = tree + cols[c]; } else if (c == 'priority') { - if (flags.prio > 0 && flags.prio < 6) - html = '<span class="prio'+flags.prio+'"> </span>'; + if (flags.prio > 0 && flags.prio < 6) { + label = this.get_label('priority') + ' ' + flags.prio; + html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>'; + } else html = ' '; + } + else if (c == 'folder') { + html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>'; } else html = cols[c]; @@ -1993,7 +2191,7 @@ if (cols && cols.length) { // make sure new columns are added at the end of the list - var i, idx, name, newcols = [], oldcols = this.env.coltypes; + var i, idx, name, newcols = [], oldcols = this.env.listcols; for (i=0; i<oldcols.length; i++) { name = oldcols[i]; idx = $.inArray(name, cols); @@ -2024,7 +2222,7 @@ var win, target = window, action = preview ? 'preview': 'show', - url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.env.mailbox); + url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.get_message_mailbox(id)); if (preview && (win = this.get_frame_window(this.env.contentframe))) { target = win; @@ -2141,7 +2339,7 @@ var lock = this.set_busy(true, 'checkingmail'), params = this.check_recent_params(); - this.http_request('check-recent', params, lock); + this.http_post('check-recent', params, lock); }; // list messages of a specific mailbox using filter @@ -2153,11 +2351,20 @@ // reset vars this.env.current_page = 1; + this.env.search_filter = filter; this.http_request('search', this.search_params(false, filter), lock); }; + // reload the current message listing + this.refresh_list = function() + { + this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true); + if (this.message_list) + this.message_list.clear_selection(); + }; + // list messages of a specific mailbox - this.list_mailbox = function(mbox, page, sort, url) + this.list_mailbox = function(mbox, page, sort, url, update_only) { var win, target = window; @@ -2182,15 +2389,17 @@ this.select_all_mode = false; } - // unselect selected messages and clear the list and message data - this.clear_message_list(); + if (!update_only) { + // unselect selected messages and clear the list and message data + this.clear_message_list(); - if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort)) - url._refresh = 1; + if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort)) + url._refresh = 1; - this.select_folder(mbox, '', true); - this.unmark_folder(mbox, 'recent', '', true); - this.env.mailbox = mbox; + this.select_folder(mbox, '', true); + this.unmark_folder(mbox, 'recent', '', true); + this.env.mailbox = mbox; + } // load message list remotely if (this.gui_objects.messagelist) { @@ -2216,7 +2425,6 @@ this.clear_message_list = function() { this.env.messages = {}; - this.last_selected = 0; this.show_contentframe(false); if (this.message_list) @@ -2224,20 +2432,18 @@ }; // send remote request to load message list - this.list_mailbox_remote = function(mbox, page, post_data) + this.list_mailbox_remote = function(mbox, page, url) { - // clear message list first - this.message_list.clear(); - var lock = this.set_busy(true, 'loading'); - if (typeof post_data != 'object') - post_data = {}; - post_data._mbox = mbox; + if (typeof url != 'object') + url = {}; + url._mbox = mbox; if (page) - post_data._page = page; + url._page = page; - this.http_request('list', post_data, lock); + this.http_request('list', url, lock); + this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) }); }; // removes messages that doesn't exists from list selection array @@ -2389,7 +2595,7 @@ } if (html) - $('#rcmtab'+uid).html(html); + $('#rcmtab'+this.html_identifier(uid, true)).html(html); }; // update parent in a thread @@ -2453,14 +2659,14 @@ r.depth--; // move left // reset width and clear the content of a tab, icons will be added later - $('#rcmtab'+r.uid).width(r.depth * 15).html(''); + $('#rcmtab'+r.id).width(r.depth * 15).html(''); if (!r.depth) { // a new root count++; // increase roots count r.parent_uid = 0; if (r.has_children) { // replace 'leaf' with 'collapsed' - $('#rcmrow'+r.uid+' '+'.leaf:first') - .attr('id', 'rcmexpando' + r.uid) + $('#'+r.id+' .leaf:first') + .attr('id', 'rcmexpando' + r.id) .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed')) .bind('mousedown', {uid:r.uid, p:this}, function(e) { return e.data.p.expand_message_row(e, e.data.uid); }); @@ -2511,7 +2717,7 @@ // set message icon this.set_message_icon = function(uid) { - var css_class, + var css_class, label = '', row = this.message_list.rows[uid]; if (!row) @@ -2519,38 +2725,55 @@ if (row.icon) { css_class = 'msgicon'; - if (row.deleted) + if (row.deleted) { css_class += ' deleted'; - else if (row.unread) + label += this.get_label('deleted') + ' '; + } + else if (row.unread) { css_class += ' unread'; + label += this.get_label('unread') + ' '; + } else if (row.unread_children) css_class += ' unreadchildren'; if (row.msgicon == row.icon) { - if (row.replied) + if (row.replied) { css_class += ' replied'; - if (row.forwarded) + label += this.get_label('replied') + ' '; + } + if (row.forwarded) { css_class += ' forwarded'; + label += this.get_label('forwarded') + ' '; + } css_class += ' status'; } - row.icon.className = css_class; + $(row.icon).attr('class', css_class).attr('title', label); } if (row.msgicon && row.msgicon != row.icon) { + label = ''; css_class = 'msgicon'; - if (!row.unread && row.unread_children) + if (!row.unread && row.unread_children) { css_class += ' unreadchildren'; - if (row.replied) + } + if (row.replied) { css_class += ' replied'; - if (row.forwarded) + label += this.get_label('replied') + ' '; + } + if (row.forwarded) { css_class += ' forwarded'; + label += this.get_label('forwarded') + ' '; + } - row.msgicon.className = css_class; + $(row.msgicon).attr('class', css_class).attr('title', label); } if (row.flagicon) { css_class = (row.flagged ? 'flagged' : 'unflagged'); - row.flagicon.className = css_class; + label = this.get_label(css_class); + $(row.flagicon).attr('class', css_class) + .attr('aria-label', label) + .attr('title', label); } }; @@ -2624,12 +2847,12 @@ }; // copy selected messages to the specified mailbox - this.copy_messages = function(mbox, obj) + this.copy_messages = function(mbox, event) { if (mbox && typeof mbox === 'object') mbox = mbox.id; else if (!mbox) - return this.folder_selector(obj, function(folder) { ref.command('copy', folder); }); + return this.folder_selector(event, function(folder) { ref.command('copy', folder); }); // exit if current or no mailbox specified if (!mbox || mbox == this.env.mailbox) @@ -2646,15 +2869,15 @@ }; // move selected messages to the specified mailbox - this.move_messages = function(mbox, obj) + this.move_messages = function(mbox, event) { if (mbox && typeof mbox === 'object') mbox = mbox.id; else if (!mbox) - return this.folder_selector(obj, function(folder) { ref.command('move', folder); }); + return this.folder_selector(event, function(folder) { ref.command('move', folder); }); // exit if current or no mailbox specified - if (!mbox || mbox == this.env.mailbox) + if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing())) return; var lock = false, post_data = this.selection_post_data({_target_mbox: mbox}); @@ -2722,7 +2945,8 @@ // @private this._with_selected_messages = function(action, post_data, lock) { - var count = 0, msg; + var count = 0, msg, + remove = (action == 'delete' || !this.is_multifolder_listing()); // update the list (remove rows, clear selection) if (this.message_list) { @@ -2739,10 +2963,11 @@ roots.push(root); } } - this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1)); + if (remove) + this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1)); } // make sure there are no selected rows - if (!this.env.display_next) + if (!this.env.display_next && remove) this.message_list.clear_selection(); // update thread tree icons for (n=0, len=roots.length; n<len; n++) { @@ -2753,8 +2978,11 @@ if (count < 0) post_data._count = (count*-1); // remove threads from the end of the list - else if (count > 0) + else if (count > 0 && remove) this.delete_excessive_thread_rows(); + + if (!remove) + post_data._refresh = 1; if (!lock) { msg = action == 'move' ? 'movingmessage' : 'deletingmessage'; @@ -2965,7 +3193,8 @@ var icn_src, uid, i, len, rows = this.message_list ? this.message_list.rows : {}; - uids = String(uids).split(','); + if (typeof uids == 'string') + uids = String(uids).split(','); for (i=0, len=uids.length; i<len; i++) { uid = uids[i]; @@ -2978,7 +3207,7 @@ // with select_all mode checking this.uids_to_list = function(uids) { - return this.select_all_mode ? '*' : uids.join(','); + return this.select_all_mode ? '*' : (uids.length <= 1 ? uids.join(',') : uids); }; // Sets title of the delete button @@ -3051,8 +3280,8 @@ // handler for keyboard events on the _user field this.login_user_keyup = function(e) { - var key = rcube_event.get_keycode(e); - var passwd = $('#rcmloginpwd'); + var key = rcube_event.get_keycode(e), + passwd = $('#rcmloginpwd'); // enter if (key == 13 && passwd.length && !passwd.val()) { @@ -3210,7 +3439,9 @@ this.env.recipients_delimiter = this.env.recipients_separator + ' '; obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); }) - .attr('autocomplete', 'off'); + .attr('autocomplete', 'off') + .attr('aria-autocomplete', 'list') + .attr('aria-expanded', 'false'); }; this.submit_messageform = function(draft) @@ -3281,6 +3512,8 @@ input.val(oldval + recipients.join(delim + ' ') + delim + ' '); this.triggerEvent('add-recipient', { field:field, recipients:recipients }); } + + return recipients.length; }; // checks the input fields before sending a message @@ -3508,15 +3741,18 @@ $('<a>').addClass('insertresponse active') .attr('href', '#') .attr('rel', key) + .attr('tabindex', '0') .html(this.quote_html(response.name)) .appendTo(li) .mousedown(function(e){ return rcube_event.cancel(e); }) - .mouseup(function(e){ - ref.command('insert-response', key); - $(document.body).trigger('mouseup'); // hides the menu - return rcube_event.cancel(e); + .bind('mouseup keypress', function(e){ + if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { + ref.command('insert-response', $(this).attr('rel')); + $(document.body).trigger('mouseup'); // hides the menu + return rcube_event.cancel(e); + } }); } }; @@ -3629,9 +3865,12 @@ $("input[name='_draft_saveid']").val(id); // reset history of hidden iframe used for saving draft (#1489643) - if (window.frames['savetarget'] && window.frames['savetarget'].history) { + // but don't do this on timer-triggered draft-autosaving (#1489789) + if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit) { window.frames['savetarget'].history.back(); } + + this.draft_autosave_submit = false; } // always remove local copy upon saving as draft @@ -3641,7 +3880,11 @@ this.auto_save_start = function() { if (this.env.draft_autosave) - this.save_timer = setTimeout(function(){ ref.command("savedraft"); }, this.env.draft_autosave * 1000); + this.draft_autosave_submit = false; + this.save_timer = setTimeout(function(){ + ref.draft_autosave_submit = true; // set auto-saved flag (#1489789) + ref.command("savedraft"); + }, this.env.draft_autosave * 1000); // save compose form content to local storage every 5 seconds if (!this.local_save_timer && window.localStorage) { @@ -3787,9 +4030,9 @@ this.clear_compose_data = function() { if (window.localStorage) { - var index = this.local_storage_get_item('compose.index', []); + var i, index = this.local_storage_get_item('compose.index', []); - for (var i=0; i < index.length; i++) { + for (i=0; i < index.length; i++) { this.local_storage_remove_item('compose.' + index[i]); } this.local_storage_remove_item('compose.index'); @@ -3839,7 +4082,7 @@ // cleanup rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g'); - input_val = input_val.replace(rx, delim); + input_val = String(input_val).replace(rx, delim); rx = new RegExp('^[\\s' + rx_delim + ']+'); input_val = input_val.replace(rx, ''); @@ -3960,7 +4203,7 @@ this.upload_file = function(form, action) { if (!form) - return false; + return; // count files and size on capable browser var size = 0, numfiles = 0; @@ -3981,7 +4224,7 @@ if (numfiles) { if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) { this.display_message(this.env.filesizeerror, 'error'); - return; + return false; } var frame_name = this.async_upload_form(form, action || 'upload', function(e) { @@ -4015,11 +4258,11 @@ if (this.env.upload_progress_time) { this.upload_progress_start('upload', ts); } - } - // set reference to the form object - this.gui_objects.attachmentform = form; - return true; + // set reference to the form object + this.gui_objects.attachmentform = form; + return true; + } }; // add file name to attachment list @@ -4041,7 +4284,7 @@ li.attr('id', name) .addClass(att.classname) .html(att.html) - .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this, 0); }); + .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); }); // replace indicator's li if (upload_id && (indicator = document.getElementById(upload_id))) { @@ -4061,8 +4304,10 @@ this.remove_from_attachment_list = function(name) { - delete this.env.attachments[name]; - $('#'+name).remove(); + if (this.env.attachments) { + delete this.env.attachments[name]; + $('#'+name).remove(); + } }; this.remove_attachment = function(name) @@ -4135,15 +4380,32 @@ r = this.http_request(action, url, lock); this.env.qsearch = {lock: lock, request: r}; + this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base'); + + return true; } + + return false; + }; + + this.continue_search = function(request_id) + { + var lock = ref.set_busy(true, 'stillsearching'); + + setTimeout(function(){ + var url = ref.search_params(); + url._continue = request_id; + ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) }; + }, 100); }; // build URL params for search - this.search_params = function(search, filter) + this.search_params = function(search, filter, smods) { var n, url = {}, mods_arr = [], mods = this.env.search_mods, - mbox = this.env.mailbox; + scope = this.env.search_scope || 'base', + mbox = scope == 'all' ? '*' : this.env.mailbox; if (!filter && this.gui_objects.search_filter) filter = this.gui_objects.search_filter.value; @@ -4157,17 +4419,19 @@ if (search) { url._q = search; - if (mods && this.message_list) - mods = mods[mbox] ? mods[mbox] : mods['*']; + if (!smods && mods && this.message_list) + smods = mods[mbox] || mods['*']; - if (mods) { - for (n in mods) + if (smods) { + for (n in smods) mods_arr.push(n); url._headers = mods_arr.join(','); } } - if (mbox) + if (scope) + url._scope = scope; + if (mbox && scope != 'all') url._mbox = mbox; return url; @@ -4185,7 +4449,43 @@ this.env.qsearch = null; this.env.search_request = null; this.env.search_id = null; + + this.enable_command('set-listmode', this.env.threads); }; + + this.set_searchscope = function(scope) + { + var old = this.env.search_scope; + this.env.search_scope = scope; + + // re-send search query with new scope + if (scope != old && this.env.search_request) { + if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL') + this.filter_mailbox(this.env.search_filter); + if (scope != 'all') + this.select_folder(this.env.mailbox, '', true); + } + }; + + this.set_searchmods = function(mods) + { + var mbox = rcmail.env.mailbox, + scope = this.env.search_scope || 'base'; + + if (scope == 'all') + mbox = '*'; + + if (!this.env.search_mods) + this.env.search_mods = {}; + + this.env.search_mods[mbox] = mods; + }; + + this.is_multifolder_listing = function() + { + return typeof this.env.multifolder_listing != 'undefined' ? this.env.multifolder_listing : + (this.env.search_request && (this.env.search_scope || 'base') != 'base'); + } this.sent_successfully = function(type, msg, folders) { @@ -4233,7 +4533,7 @@ var dir = key==38 ? 1 : 0; - highlight = document.getElementById('rcmksearchSelected'); + highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected); if (!highlight) highlight = this.ksearch_pane.__ul.firstChild; @@ -4281,14 +4581,14 @@ this.ksearch_select = function(node) { - var current = $('#rcmksearchSelected'); - if (current[0] && node) { - current.removeAttr('id').removeClass('selected'); + if (this.ksearch_pane && node) { + this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected'); } if (node) { - $(node).attr('id', 'rcmksearchSelected').addClass('selected'); + $(node).addClass('selected').removeAttr('aria-selected', 'true'); this.ksearch_selected = node._rcm_id; + $(this.ksearch_input).attr('aria-activedecendant', 'rcmkSearchItem' + this.ksearch_selected); } }; @@ -4310,10 +4610,14 @@ this.ksearch_destroy(); // insert all members of a group - if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) { + if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group') { insert += this.env.contacts[id].name + this.env.recipients_delimiter; this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]); this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false); + } + else if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].name) { + insert = this.env.contacts[id].name + this.env.recipients_delimiter; + trigger = true; } else if (typeof this.env.contacts[id] === 'string') { insert = this.env.contacts[id] + this.env.recipients_delimiter; @@ -4328,7 +4632,7 @@ this.ksearch_input.setSelectionRange(cpos, cpos); if (trigger) { - this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert }); + this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id] }); this.compose_type_activity++; } }; @@ -4359,7 +4663,7 @@ p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1), q = inp_value.substring(p+1, cpos), min = this.env.autocomplete_min_length, - ac = this.ksearch_data; + data = this.ksearch_data; // trim query string q = $.trim(q); @@ -4386,34 +4690,26 @@ return; // ...new search value contains old one and previous search was not finished or its result was empty - if (old_value && old_value.length && q.startsWith(old_value) && (!ac || ac.num <= 0) && this.env.contacts && !this.env.contacts.length) + if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length) return; - var i, lock, source, xhr, reqid = new Date().getTime(), - post_data = {_search: q, _id: reqid}, - threads = props && props.threads ? props.threads : 1, - sources = props && props.sources ? props.sources : [], - action = props && props.action ? props.action : 'mail/autocomplete'; + var sources = props && props.sources ? props.sources : ['']; + var reqid = this.multi_thread_http_request({ + items: sources, + threads: props && props.threads ? props.threads : 1, + action: props && props.action ? props.action : 'mail/autocomplete', + postdata: { _search:q, _source:'%s' }, + lock: this.display_message(this.get_label('searching'), 'loading') + }); - this.ksearch_data = {id: reqid, sources: sources.slice(), action: action, - locks: [], requests: [], num: sources.length}; - - for (i=0; i<threads; i++) { - source = this.ksearch_data.sources.shift(); - if (threads > 1 && source === undefined) - break; - - post_data._source = source ? source : ''; - lock = this.display_message(this.get_label('searching'), 'loading'); - xhr = this.http_post(action, post_data, lock); - - this.ksearch_data.locks.push(lock); - this.ksearch_data.requests.push(xhr); - } + this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length }; }; this.ksearch_query_results = function(results, search, reqid) { + // trigger multi-thread http response callback + this.multi_thread_http_response(results, reqid); + // search stopped in meantime? if (!this.ksearch_value) return; @@ -4423,17 +4719,20 @@ return; // display search results - var i, len, ul, li, text, init, + var i, id, len, ul, text, type, init, value = this.ksearch_value, - data = this.ksearch_data, maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15; // create results pane if not present if (!this.ksearch_pane) { ul = $('<ul>'); - this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane') + this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox') .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body); this.ksearch_pane.__ul = ul[0]; + + // register (delegate) event handlers + ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); }) + .on('onmouseup', 'li', function(e){ ref.ksearch_click(e.target); }) } ul = this.ksearch_pane.__ul; @@ -4457,12 +4756,14 @@ if (results && (len = results.length)) { for (i=0; i < len && maxlen > 0; i++) { text = typeof results[i] === 'object' ? results[i].name : results[i]; - li = document.createElement('LI'); - li.innerHTML = text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%').replace(/</g, '<').replace(/>/g, '>').replace(/##([^%]+)%%/g, '<b>$1</b>'); - li.onmouseover = function(){ ref.ksearch_select(this); }; - li.onmouseup = function(){ ref.ksearch_click(this) }; - li._rcm_id = this.env.contacts.length + i; - ul.appendChild(li); + type = typeof results[i] === 'object' ? results[i].type : ''; + id = i + this.env.contacts.length; + $('<li>').attr('id', 'rcmkSearchItem' + id) + .attr('role', 'option') + .html(this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>')) + .addClass(type || '') + .appendTo(ul) + .get(0)._rcm_id = id; maxlen -= 1; } } @@ -4471,35 +4772,21 @@ this.ksearch_pane.show(); // select the first if (!this.env.contacts.length) { - $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected'); - this.ksearch_selected = 0; + this.ksearch_select($('li:first', ul).get(0)); } + + // set the right aria-* attributes to the input field + $(this.ksearch_input) + .attr('aria-haspopup', 'true') + .attr('aria-expanded', 'true') + .attr('aria-owns', 'rcmKSearchpane') } if (len) this.env.contacts = this.env.contacts.concat(results); - // run next parallel search - if (data.id == reqid) { - data.num--; - if (maxlen > 0 && data.sources.length) { - var lock, xhr, source = data.sources.shift(), post_data; - if (source) { - post_data = {_search: value, _id: reqid, _source: source}; - lock = this.display_message(this.get_label('searching'), 'loading'); - xhr = this.http_post(data.action, post_data, lock); - - this.ksearch_data.locks.push(lock); - this.ksearch_data.requests.push(xhr); - } - } - else if (!maxlen) { - if (!this.ksearch_msg) - this.ksearch_msg = this.display_message(this.get_label('autocompletemore')); - // abort pending searches - this.ksearch_abort(); - } - } + if (this.ksearch_data.id == reqid) + this.ksearch_data.num--; }; this.ksearch_click = function(node) @@ -4528,13 +4815,20 @@ if (this.ksearch_pane) this.ksearch_pane.hide(); + $(this.ksearch_input) + .attr('aria-haspopup', 'false') + .attr('aria-expanded', 'false') + .removeAttr('aria-activedecendant') + .removeAttr('aria-owns'); + this.ksearch_destroy(); }; // Clears autocomplete data/requests this.ksearch_destroy = function() { - this.ksearch_abort(); + if (this.ksearch_data) + this.multi_thread_request_abort(this.ksearch_data.id); if (this.ksearch_info) this.hide_message(this.ksearch_info); @@ -4545,18 +4839,6 @@ this.ksearch_data = null; this.ksearch_info = null; this.ksearch_msg = null; - } - - // Aborts pending autocomplete requests - this.ksearch_abort = function() - { - var i, len, ac = this.ksearch_data; - - if (!ac) - return; - - for (i=0, len=ac.locks.length; i<len; i++) - this.abort_request({request: ac.requests[i], lock: ac.locks[i]}); }; @@ -4575,7 +4857,7 @@ if (this.preview_timer) clearTimeout(this.preview_timer); - var n, id, sid, contact, ref = this, writable = false, + var n, id, sid, contact, writable = false, source = this.env.source ? this.env.address_sources[this.env.source] : null; // we don't have dblclick handler here, so use 200 instead of this.dblclick_time @@ -4743,6 +5025,7 @@ // add link to pop back to parent group if (this.env.address_group_stack.length > 1) { $('<a href="#list">...</a>') + .attr('title', this.gettext('uponelevel')) .addClass('poplink') .appendTo(boxtitle) .click(function(e){ return ref.command('popgroup','',this); }); @@ -4992,7 +5275,7 @@ this.init_contact_form = function() { - var ref = this, col; + var col; if (this.env.coltypes) { this.set_photo_actions($('#ff_photo').val()); @@ -5640,25 +5923,26 @@ this.init_subscription_list = function() { - var p = this, delim = RegExp.escape(this.env.delimiter); + var delim = RegExp.escape(this.env.delimiter); this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$'); this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist, - {multiselect:false, draggable:true, keyboard:false, toggleselect:true}); + {multiselect:false, draggable:true, keyboard:true, toggleselect:true}); this.subscription_list - .addEventListener('select', function(o){ p.subscription_select(o); }) - .addEventListener('dragstart', function(o){ p.drag_active = true; }) - .addEventListener('dragend', function(o){ p.subscription_move_folder(o); }) + .addEventListener('select', function(o){ ref.subscription_select(o); }) + .addEventListener('dragstart', function(o){ ref.drag_active = true; }) + .addEventListener('dragend', function(o){ ref.subscription_move_folder(o); }) .addEventListener('initrow', function (row) { - row.obj.onmouseover = function() { p.focus_subscription(row.id); }; - row.obj.onmouseout = function() { p.unfocus_subscription(row.id); }; + row.obj.onmouseover = function() { ref.focus_subscription(row.id); }; + row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); }; }) - .init(); + .init() + .focus(); $('#mailboxroot') - .mouseover(function(){ p.focus_subscription(this.id); }) - .mouseout(function(){ p.unfocus_subscription(this.id); }) + .mouseover(function(){ ref.focus_subscription(this.id); }) + .mouseout(function(){ ref.unfocus_subscription(this.id); }) }; this.focus_subscription = function(id) @@ -6072,22 +6356,19 @@ init_button(cmd, this.buttons[cmd][i]); } } - - // set active task button - this.set_button(this.task, 'sel'); }; // set button to a specific state this.set_button = function(command, state) { - var n, button, obj, a_buttons = this.buttons[command], + var n, button, obj, $obj, a_buttons = this.buttons[command], len = a_buttons ? a_buttons.length : 0; for (n=0; n<len; n++) { button = a_buttons[n]; obj = document.getElementById(button.id); - if (!obj) + if (!obj || button.status == state) continue; // get default/passive setting of the button @@ -6113,7 +6394,17 @@ // disable/enable input buttons if (button.type == 'input') { button.status = state; - obj.disabled = !state; + obj.disabled = state == 'pas'; + } + else if (button.type == 'uibutton') { + button.status = state; + $(obj).button('option', 'disabled', state == 'pas'); + } + else { + $obj = $(obj); + $obj + .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0')) + .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false'); } } }; @@ -6199,8 +6490,7 @@ type = type ? type : 'notice'; - var ref = this, - key = this.html_identifier(msg), + var key = this.html_identifier(msg), date = new Date(), id = type + date.getTime(); @@ -6239,7 +6529,8 @@ this.messages[key].labels = [{'id': id, 'msg': msg}]; } else { - obj.click(function() { return ref.hide_message(obj); }); + obj.click(function() { return ref.hide_message(obj); }) + .attr('role', 'alert'); } this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj }); @@ -6369,10 +6660,8 @@ this.treelist.select(name); } else if (this.gui_objects.folderlist) { - $('li.selected', this.gui_objects.folderlist) - .removeClass('selected').addClass('unfocused'); - $(this.get_folder_li(name, prefix, encode)) - .removeClass('unfocused').addClass('selected'); + $('li.selected', this.gui_objects.folderlist).removeClass('selected'); + $(this.get_folder_li(name, prefix, encode)).addClass('selected'); // trigger event hook this.triggerEvent('selectfolder', { folder:name, prefix:prefix }); @@ -6407,18 +6696,18 @@ // for reordering column array (Konqueror workaround) // and for setting some message list global variables - this.set_message_coltypes = function(coltypes, repl, smart_col) + this.set_message_coltypes = function(listcols, repl, smart_col) { var list = this.message_list, thead = list ? list.thead : null, - cell, col, n, len, th, tr; + repl, cell, col, n, len, tr; - this.env.coltypes = coltypes; + this.env.listcols = listcols; // replace old column headers if (thead) { if (repl) { - th = document.createElement('thead'); + thead.innerHTML = ''; tr = document.createElement('tr'); for (c=0, len=repl.length; c < len; c++) { @@ -6428,20 +6717,13 @@ if (repl[c].className) cell.className = repl[c].className; tr.appendChild(cell); } - th.appendChild(tr); - thead.parentNode.replaceChild(th, thead); - list.thead = thead = th; + thead.appendChild(tr); } - for (n=0, len=this.env.coltypes.length; n<len; n++) { - col = this.env.coltypes[n]; + for (n=0, len=this.env.listcols.length; n<len; n++) { + col = this.env.listcols[n]; if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) { - cell.id = 'rcm'+col; - $('span,a', cell).text(this.get_label(col == 'fromto' ? smart_col : col)); - // if we have links for sorting, it's a bit more complicated... - $('a', cell).click(function(){ - return rcmail.command('sort', this.id.replace(/^rcm/, ''), this); - }); + $(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col)); } } } @@ -6450,18 +6732,23 @@ this.env.flagged_col = null; this.env.status_col = null; - if ((n = $.inArray('subject', this.env.coltypes)) >= 0) { + if (this.env.coltypes.folder) + this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base'; + + if ((n = $.inArray('subject', this.env.listcols)) >= 0) { this.env.subject_col = n; if (list) list.subject_col = n; } - if ((n = $.inArray('flag', this.env.coltypes)) >= 0) + if ((n = $.inArray('flag', this.env.listcols)) >= 0) this.env.flagged_col = n; - if ((n = $.inArray('status', this.env.coltypes)) >= 0) + if ((n = $.inArray('status', this.env.listcols)) >= 0) this.env.status_col = n; - if (list) + if (list) { + list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', this.env.listcols) < 0); list.init_header(); + } }; // replace content of row count display @@ -6590,8 +6877,9 @@ // fetch headers only once if (!this.gui_objects.all_headers_box.innerHTML) { - var lock = this.display_message(this.get_label('loading'), 'loading'); - this.http_post('headers', {_uid: this.env.uid}, lock); + this.http_post('headers', {_uid: this.env.uid, _mbox: this.env.mailbox}, + this.display_message(this.get_label('loading'), 'loading') + ); } }; @@ -6607,17 +6895,15 @@ }; // create folder selector popup, position and display it - this.folder_selector = function(obj, callback) + this.folder_selector = function(event, callback) { var container = this.folder_selector_element; if (!container) { var rows = [], delim = this.env.delimiter, - ul = $('<ul class="toolbarmenu iconized">'), - li = document.createElement('li'), - link = document.createElement('a'), - span = document.createElement('span'); + ul = $('<ul class="toolbarmenu">'), + link = document.createElement('a'); container = $('<div id="folder-selector" class="popupmenu"></div>'); link.href = '#'; @@ -6625,33 +6911,30 @@ // loop over sorted folders list $.each(this.env.mailboxes_list, function() { - var tmp, n = 0, s = 0, + var n = 0, s = 0, folder = ref.env.mailboxes[this], id = folder.id, - a = link.cloneNode(false), row = li.cloneNode(false); + a = $(link.cloneNode(false)), + row = $('<li>'); if (folder.virtual) - a.className += ' virtual'; - else { - a.className += ' active'; - a.onclick = function() { container.hide().data('callback')(folder.id); }; - } + a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1'); + else + a.addClass('active').data('id', folder.id); if (folder['class']) - a.className += ' ' + folder['class']; + a.addClass(folder['class']); // calculate/set indentation level while ((s = id.indexOf(delim, s)) >= 0) { n++; s++; } - a.style.paddingLeft = n ? (n * 16) + 'px' : 0; + a.css('padding-left', n ? (n * 16) + 'px' : 0); // add folder name element - tmp = span.cloneNode(false); - $(tmp).text(folder.name); - a.appendChild(tmp); + a.append($('<span>').text(folder.name)); - row.appendChild(a); + row.append(a); rows.push(row); }); @@ -6663,22 +6946,162 @@ // set max-height if the list is long if (rows.length > 10) - container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9) + container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9); + // register delegate event handler for folder item clicks + container.on('click', 'a.active', function(e){ + container.data('callback')($(this).data('id')); + return false; + }); +/* // hide selector on click out of selector element var fn = function(e) { if (e.target != container.get(0)) container.hide(); }; $(document.body).on('mouseup', fn); $('iframe').contents().on('mouseup', fn) .load(function(e) { try { $(this).contents().on('mouseup', fn); } catch(e) {}; }); - +*/ this.folder_selector_element = container; } - // position menu on the screen - this.element_position(container, obj); + container.data('callback', callback); - container.show().data('callback', callback); + // position menu on the screen + this.show_menu('folder-selector', true, event); }; + + + /***********************************************/ + /********* popup menu functions *********/ + /***********************************************/ + + // Show/hide a specific popup menu + this.show_menu = function(prop, show, event) + { + var name = typeof prop == 'object' ? prop.menu : prop, + obj = $('#'+name), + ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'), + keyboard = rcube_event.is_keyboard(event), + align = obj.attr('data-align') || '', + stack = false; + + // find "real" button element + if (ref.get(0).tagName != 'A' && ref.closest('a').length) + ref = ref.closest('a'); + + if (typeof prop == 'string') + prop = { menu:name }; + + // let plugins or skins provide the menu element + if (!obj.length) { + obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event }); + } + + if (!obj || !obj.length) { + // just delegate the action to subscribers + return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event }); + } + + // move element to top for proper absolute positioning + obj.appendTo(document.body); + + if (typeof show == 'undefined') + show = obj.is(':visible') ? false : true; + + if (show && ref.length) { + var win = $(window), + pos = ref.offset(), + above = align.indexOf('bottom') >= 0; + + stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0; + + ref.offsetWidth = ref.outerWidth(); + ref.offsetHeight = ref.outerHeight(); + if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) { + above = true; + } + if (align.indexOf('right') >= 0) { + pos.left = pos.left + ref.outerWidth() - obj.width(); + } + else if (stack) { + pos.left = pos.left + ref.offsetWidth - 5; + pos.top -= ref.offsetHeight; + } + if (pos.left + obj.width() > win.width()) { + pos.left = win.width() - obj.width() - 12; + } + pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight)); + obj.css({ left:pos.left+'px', top:pos.top+'px' }); + } + + // add menu to stack + if (show) { + // truncate stack down to the one containing the ref link + for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) { + if (!$(ref).parents('#'+this.menu_stack[i]).length) + this.hide_menu(this.menu_stack[i]); + } + if (stack && this.menu_stack.length) { + obj.data('parent', $.last(this.menu_stack)); + obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1); + } + else if (!stack && this.menu_stack.length) { + this.hide_menu(this.menu_stack[0], event); + } + + obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0)); + this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event }); + this.menu_stack.push(name); + + this.menu_keyboard_active = show && keyboard; + if (this.menu_keyboard_active) { + this.focused_menu = name; + obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus(); + } + } + else { // close menu + this.hide_menu(name, event); + } + + return show; + }; + + // hide the given popup menu (and it's childs) + this.hide_menu = function(name, event) + { + if (!this.menu_stack.length) { + // delegate to subscribers + this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event }); + return; + } + + var obj, keyboard = rcube_event.is_keyboard(event); + for (var j=this.menu_stack.length-1; j >= 0; j--) { + obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false); + this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event }); + if (this.menu_stack[j] == name) { + j = -1; // stop loop + if (obj.data('opener')) { + $(obj.data('opener')).attr('aria-expanded', 'false'); + if (keyboard) + obj.data('opener').focus(); + } + } + this.menu_stack.pop(); + } + + // focus previous menu in stack + if (this.menu_stack.length && keyboard) { + this.menu_keyboard_active = true; + this.focused_menu = $.last(this.menu_stack); + if (!obj || !obj.data('opener')) + $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus(); + } + else { + this.focused_menu = null; + this.menu_keyboard_active = false; + } + } + // position a menu element on the screen in relation to other object this.element_position = function(element, obj) @@ -6720,15 +7143,14 @@ this.html2plain = function(htmlText, id) { - var rcmail = this, - url = '?_task=utils&_action=html2text', + var url = '?_task=utils&_action=html2text', lock = this.set_busy(true, 'converting'); this.log('HTTP POST: ' + url); $.ajax({ type: 'POST', url: url, data: htmlText, contentType: 'application/octet-stream', - error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }, - success: function(data) { rcmail.set_busy(false, null, lock); $('#'+id).val(data); rcmail.log(data); } + error: function(o, status, err) { ref.http_error(o, status, err, lock); }, + success: function(data) { ref.set_busy(false, null, lock); $('#'+id).val(data); ref.log(data); } }); }; @@ -6759,13 +7181,13 @@ if (action) query._action = action; - else + else if (this.env.action) query._action = this.env.action; var base = this.env.comm_path, k, param = {}; // overwrite task name - if (query._action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) { + if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) { query._action = RegExp.$2; base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1); } @@ -6776,7 +7198,7 @@ param[k] = query[k]; } - return base + '&' + $.param(param) + querystring; + return base + (base.indexOf('?') > -1 ? '&' : '?') + $.param(param) + querystring; }; this.redirect = function(url, lock) @@ -6800,7 +7222,7 @@ this.goto_url = function(action, query, lock) { - this.redirect(this.url(action, query)); + this.redirect(this.url(action, query), lock); }; this.location_href = function(url, target, frame) @@ -6819,6 +7241,13 @@ // reset keep-alive interval this.start_keepalive(); + }; + + // update browser location to remember current view + this.update_state = function(query) + { + if (window.history.replaceState) + window.history.replaceState({}, document.title, rcmail.url('', query)); }; // send a http request to the server @@ -6997,12 +7426,17 @@ this.env.qsearch = null; case 'list': if (this.task == 'mail') { + var is_multifolder = this.is_multifolder_listing(); this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0); - this.enable_command('expunge', this.env.exists); - this.enable_command('purge', this.purge_mailbox_test()); - this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount); + this.enable_command('expunge', this.env.exists && !is_multifolder); + this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder); + this.enable_command('import-messages', !is_multifolder); + this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder); + this.enable_command('set-listmode', this.env.threads && !is_multifolder); if ((response.action == 'list' || response.action == 'search') && this.message_list) { + if (this.message_list.rowcount > 0) + this.message_list.focus(); this.msglist_select(this.message_list); this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount }); } @@ -7014,9 +7448,17 @@ this.enable_command('search-create', this.env.source == ''); this.enable_command('search-delete', this.env.search_id); this.update_group_commands(); + if (this.contact_list.rowcount > 0) + this.contact_list.focus(); this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount }); } } + break; + + case 'list-contacts': + case 'search-contacts': + if (this.contact_list && this.contact_list.rowcount > 0) + this.contact_list.focus(); break; } @@ -7047,7 +7489,7 @@ else if (status == 'timeout') this.display_message(this.get_label('requesttimedout'), 'error'); else if (request.status == 0 && status != 'abort') - this.display_message(this.get_label('servererror') + ' (No connection)', 'error'); + this.display_message(this.get_label('connerror'), 'error'); // redirect to url specified in location header if not empty var location_url = request.getResponseHeader("Location"); @@ -7088,6 +7530,130 @@ if (this.submit_timer) clearTimeout(this.submit_timer); + }; + + /** + Send multi-threaded parallel HTTP requests to the server for a list if items. + The string '%' in either a GET query or POST parameters will be replaced with the respective item value. + This is the argument object expected: { + items: ['foo','bar','gna'], // list of items to send requests for + action: 'task/some-action', // Roudncube action to call + query: { q:'%s' }, // GET query parameters + postdata: { source:'%s' }, // POST data (sends a POST request if present) + threads: 3, // max. number of concurrent requests + onresponse: function(data){ }, // Callback function called for every response received from server + whendone: function(alldata){ } // Callback function called when all requests have been sent + } + */ + this.multi_thread_http_request = function(prop) + { + var i, item, reqid = new Date().getTime(), + threads = prop.threads || 1; + + prop.reqid = reqid; + prop.running = 0; + prop.requests = []; + prop.result = []; + prop._items = $.extend([], prop.items); // copy items + + if (!prop.lock) + prop.lock = this.display_message(this.get_label('loading'), 'loading'); + + // add the request arguments to the jobs pool + this.http_request_jobs[reqid] = prop; + + // start n threads + for (i=0; i < threads; i++) { + item = prop._items.shift(); + if (item === undefined) + break; + + prop.running++; + prop.requests.push(this.multi_thread_send_request(prop, item)); + } + + return reqid; + }; + + // helper method to send an HTTP request with the given iterator value + this.multi_thread_send_request = function(prop, item) + { + var postdata, query; + + // replace %s in post data + if (prop.postdata) { + postdata = {}; + for (var k in prop.postdata) { + postdata[k] = String(prop.postdata[k]).replace('%s', item); + } + postdata._reqid = prop.reqid; + } + // replace %s in query + else if (typeof prop.query == 'string') { + query = prop.query.replace('%s', item); + query += '&_reqid=' + prop.reqid; + } + else if (typeof prop.query == 'object' && prop.query) { + query = {}; + for (var k in prop.query) { + query[k] = String(prop.query[k]).replace('%s', item); + } + query._reqid = prop.reqid; + } + + // send HTTP GET or POST request + return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query); + }; + + // callback function for multi-threaded http responses + this.multi_thread_http_response = function(data, reqid) + { + var prop = this.http_request_jobs[reqid]; + if (!prop || prop.running <= 0 || prop.cancelled) + return; + + prop.running--; + + // trigger response callback + if (prop.onresponse && typeof prop.onresponse == 'function') { + prop.onresponse(data); + } + + prop.result = $.extend(prop.result, data); + + // send next request if prop.items is not yet empty + var item = prop._items.shift(); + if (item !== undefined) { + prop.running++; + prop.requests.push(this.multi_thread_send_request(prop, item)); + } + // trigger whendone callback and mark this request as done + else if (prop.running == 0) { + if (prop.whendone && typeof prop.whendone == 'function') { + prop.whendone(prop.result); + } + + this.set_busy(false, '', prop.lock); + + // remove from this.http_request_jobs pool + delete this.http_request_jobs[reqid]; + } + }; + + // abort a running multi-thread request with the given identifier + this.multi_thread_request_abort = function(reqid) + { + var prop = this.http_request_jobs[reqid]; + if (prop) { + for (var i=0; prop.running > 0 && i < prop.requests.length; i++) { + if (prop.requests[i].abort) + prop.requests[i].abort(); + } + + prop.running = 0; + prop.cancelled = true; + this.set_busy(false, '', prop.lock); + } }; // post the given form to a hidden iframe @@ -7305,7 +7871,7 @@ this.env.lastrefresh = new Date(); // plugins should bind to 'requestrefresh' event to add own params - this.http_request('refresh', params, lock); + this.http_post('refresh', params, lock); }; // returns check-recent request parameters @@ -7366,6 +7932,13 @@ { return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null); }; + + // get the IMP mailbox of the message with the given UID + this.get_message_mailbox = function(uid) + { + var msg = this.env.messages ? this.env.messages[uid] : {}; + return msg.mbox || this.env.mailbox; + } // gets cursor position this.get_caret_pos = function(obj) @@ -7494,20 +8067,28 @@ try { window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name); } - catch(e) {}; + catch(e) { + this.display_message(String(e), 'error'); + }; }; this.check_protocol_handler = function(name, elem) { var nav = window.navigator; - if (!nav - || (typeof nav.registerProtocolHandler != 'function') - || ((typeof nav.isProtocolHandlerRegistered == 'function') - && nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri()) == 'registered') - ) - $(elem).addClass('disabled'); - else - $(elem).click(function() { rcmail.register_protocol_handler(name); return false; }); + if (!nav || (typeof nav.registerProtocolHandler != 'function')) { + $(elem).addClass('disabled').click(function(){ return false; }); + } + else { + var status = null; + if (typeof nav.isProtocolHandlerRegistered == 'function') { + status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri()); + if (status) + $(elem).parent().find('.mailtoprotohandler-status').html(status); + } + else { + $(elem).click(function() { rcmail.register_protocol_handler(name); return false; }); + } + } }; // Checks browser capabilities eg. PDF support, TIF support @@ -7620,7 +8201,6 @@ // wrapper for localStorage.getItem(key) this.local_storage_get_item = function(key, deflt, encrypted) { - // TODO: add encryption var item = localStorage.getItem(this.get_local_storage_prefix() + key); return item !== null ? JSON.parse(item) : (deflt || null); @@ -7647,12 +8227,12 @@ { if (!elem.title) { var $elem = $(elem); - if ($elem.width() + indent * 15 > $elem.parent().width()) + if ($elem.width() + (indent || 0) * 15 > $elem.parent().width()) elem.title = $elem.text(); } }; -rcube_webmail.long_subject_title_ex = function(elem, indent) +rcube_webmail.long_subject_title_ex = function(elem) { if (!elem.title) { var $elem = $(elem), @@ -7664,7 +8244,7 @@ w = tmp.width(); tmp.remove(); - if (w + indent * 15 > $elem.width()) + if (w + $('span.branch', $elem).width() * 15 > $elem.width()) elem.title = txt; } }; -- Gitblit v1.9.1