From d016dcc6f6a3daf8c19e2ececd3c676cd274381a Mon Sep 17 00:00:00 2001 From: Thomas <tb@woodcrest.local> Date: Wed, 09 Oct 2013 06:02:52 -0400 Subject: [PATCH] Refactor multi-threaded autocomple contact searching to make it available for other purposes, too --- program/js/app.js | 751 +++++++++++++++++++++++++++++++++++++++++---------------- 1 files changed, 535 insertions(+), 216 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index b982724..43ab7be 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -31,6 +31,7 @@ this.onloads = []; this.messages = {}; this.group2expand = {}; + this.http_request_jobs = {}; // webmail client settings this.dblclick_time = 500; @@ -44,7 +45,9 @@ comm_path: './', blankpage: 'program/resources/blank.gif', recipients_separator: ',', - recipients_delimiter: ', ' + recipients_delimiter: ', ', + popup_width: 1150, + popup_width_small: 900 }; // create protected reference to myself @@ -189,7 +192,7 @@ case 'mail': // enable mail commands - this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', true); + this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true); if (this.gui_objects.messagelist) { this.message_list = new rcube_list_widget(this.gui_objects.messagelist, { @@ -227,7 +230,7 @@ this.set_button_titles(); this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list', - 'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', + 'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', 'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download', 'forward', 'forward-inline', 'forward-attachment', 'change-format']; @@ -253,7 +256,8 @@ } } else if (this.env.action == 'compose') { - this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin']; + this.env.address_group_stack = []; + this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin']; if (this.env.drafts_mailbox) this.env.compose_commands.push('savedraft') @@ -274,12 +278,15 @@ // init message compose form this.init_messageform(); } + else if (this.env.action == 'get') + this.enable_command('download', 'print', true); // show printing dialog - else if (this.env.action == 'print' && this.env.uid) + else if (this.env.action == 'print' && this.env.uid) { if (bw.safari) setTimeout('window.print()', 10); else window.print(); + } // get unread count for each mailbox if (this.gui_objects.mailboxlist) { @@ -320,11 +327,13 @@ break; case 'addressbook': + this.env.address_group_stack = []; + if (this.gui_objects.folderlist) this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups); this.enable_command('add', 'import', this.env.writable_source); - this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true); + this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true); if (this.gui_objects.contactslist) { this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, @@ -368,7 +377,7 @@ } if (this.gui_objects.qsearchbox) - this.enable_command('search', 'reset-search', 'moveto', true); + this.enable_command('search', 'reset-search', true); break; @@ -390,7 +399,7 @@ } else if (this.env.action == 'edit-folder' && this.gui_objects.editform) { this.enable_command('save', 'folder-size', true); - parent.rcmail.env.messagecount = this.env.messagecount; + parent.rcmail.env.exists = this.env.messagecount; parent.rcmail.enable_command('purge', this.env.messagecount); $("input[type='text']").first().select(); } @@ -596,15 +605,16 @@ case 'extwin': if (this.env.action == 'compose') { - var form = this.gui_objects.messageform; + var form = this.gui_objects.messageform, + win = this.open_window(''); $("input[name='_action']", form).val('compose'); form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); - form.target = this.open_window('', 1100, 900); + form.target = win.name; form.submit(); } else { - this.open_window(this.env.permaurl, 900, 900); + this.open_window(this.env.permaurl, true); } break; @@ -786,16 +796,18 @@ // mail task commands case 'move': - case 'moveto': + case 'moveto': // deprecated if (this.task == 'mail') this.move_messages(props); else if (this.task == 'addressbook') - this.copy_contact(null, props); + this.move_contacts(props); break; case 'copy': if (this.task == 'mail') this.copy_messages(props); + else if (this.task == 'addressbook') + this.copy_contacts(props); break; case 'mark': @@ -857,11 +869,8 @@ // open attachment in frame if it's of a supported mimetype if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) { - var attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', this.html_identifier('rcubemailattachment'+this.env.uid+props)); - if (attachment_win) { - setTimeout(function(){ attachment_win.focus(); }, 10); + if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1')) break; - } } this.goto_url('get', qstring+'&_download=1', false); @@ -1000,7 +1009,7 @@ // Reset the auto-save timer clearTimeout(this.save_timer); - this.upload_file(props || this.gui_objects.uploadform); + this.upload_file(props || this.gui_objects.uploadform, 'upload'); break; case 'insert-sig': @@ -1044,10 +1053,12 @@ break; case 'print': - if (uid = this.get_single_uid()) { - ref.printwin = window.open(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : '')); + if (this.env.action == 'get') { + 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); if (this.printwin) { - setTimeout(function(){ ref.printwin.focus(); }, 20); if (this.env.action != 'show') this.mark_message('read', uid); } @@ -1055,15 +1066,15 @@ break; case 'viewsource': - if (uid = this.get_single_uid()) { - ref.sourcewin = window.open(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)); - if (this.sourcewin) - setTimeout(function(){ ref.sourcewin.focus(); }, 20); - } + if (uid = this.get_single_uid()) + this.open_window(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true, true); break; case 'download': - if (uid = this.get_single_uid()) + 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 }); break; @@ -1100,9 +1111,29 @@ } break; + case 'pushgroup': + // add group ID to stack + this.env.address_group_stack.push(props.id); + if (obj && event) + rcube_event.cancel(event); + case 'listgroup': this.reset_qsearch(); this.list_contacts(props.source, props.id); + break; + + case 'popgroup': + if (this.env.address_group_stack.length > 1) { + this.env.address_group_stack.pop(); + this.reset_qsearch(); + this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]); + } + 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'); break; case 'import': @@ -1179,6 +1210,7 @@ if (typeof cmd === 'string') { this.commands[cmd] = enable; this.set_button(cmd, (enable ? 'act' : 'pas')); + this.triggerEvent('enable-command', {command: cmd, status: enable}); } // push array elements into commands array else { @@ -1321,7 +1353,7 @@ this.drag_menu = function(e, target) { var modkey = rcube_event.get_modifier(e), - menu = this.gui_objects.message_dragmenu; + menu = this.gui_objects.dragmenu; if (menu && modkey == SHIFT_KEY && this.commands['copy']) { var pos = rcube_event.get_mouse_pos(e); @@ -1335,7 +1367,7 @@ this.drag_menu_action = function(action) { - var menu = this.gui_objects.message_dragmenu; + var menu = this.gui_objects.dragmenu; if (menu) { $(menu).hide(); } @@ -1450,8 +1482,12 @@ list.draglayer.hide(); this.drag_end(e); - if (!this.drag_menu(e, target)) - this.command('moveto', target); + if (this.contact_list) { + if (!this.contacts_drag_menu(e, target)) + this.command('move', target); + } + else if (!this.drag_menu(e, target)) + this.command('move', target); } // reset 'pressed' buttons @@ -1498,7 +1534,7 @@ } } // Multi-message commands - this.enable_command('delete', 'moveto', 'copy', 'mark', 'forward', 'forward-attachment', list.selection.length > 0); + this.enable_command('delete', 'move', 'copy', 'mark', 'forward', 'forward-attachment', list.selection.length > 0); // reset all-pages-selection if (selected || (list.selection.length && list.selection.length != list.rowcount)) @@ -1506,7 +1542,7 @@ // start timer for message preview (wait for double click) if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select) - this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200); + this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time); else if (this.env.contentframe) this.show_contentframe(false); }; @@ -1522,12 +1558,13 @@ var win = this.get_frame_window(this.env.contentframe); - if (win && win.location.href.indexOf(this.env.blankpage)>=0) { + if (win && win.location.href.indexOf(this.env.blankpage) >= 0) { if (this.preview_timer) clearTimeout(this.preview_timer); if (this.preview_read_timer) clearTimeout(this.preview_read_timer); - this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200); + + this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time); } }; @@ -1535,11 +1572,11 @@ { if (this.preview_timer) clearTimeout(this.preview_timer); - if (this.preview_read_timer) clearTimeout(this.preview_read_timer); var uid = list.get_single_selection(); + if (uid && this.env.mailbox == this.env.drafts_mailbox) this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox }); else if (uid) @@ -1600,46 +1637,55 @@ this.check_droptarget = function(id) { - if (this.task == 'mail') - return (this.env.mailboxes[id] && this.env.mailboxes[id].id != this.env.mailbox && !this.env.mailboxes[id].virtual) ? 1 : 0; + 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; - if (this.task == 'settings') - return id != this.env.mailbox ? 1 : 0; + case 'settings': + return id != this.env.mailbox ? 1 : 0; - if (this.task == 'addressbook') { - if (id != this.env.source && this.env.contactfolders[id]) { - // droptarget is a group - contact add to group action - if (this.env.contactfolders[id].type == 'group') { - var target_abook = this.env.contactfolders[id].source; - if (this.env.contactfolders[id].id != this.env.group && !this.env.contactfolders[target_abook].readonly) { - // search result may contain contacts from many sources - return (this.env.selection_sources.length > 1 || $.inArray(target_abook, this.env.selection_sources) == -1) ? 2 : 1; + case 'addressbook': + var target; + if (id != this.env.source && (target = this.env.contactfolders[id])) { + // droptarget is a group + if (target.type == 'group') { + if (target.id != this.env.group && !this.env.contactfolders[target.source].readonly) { + var is_other = this.env.selection_sources.length > 1 || $.inArray(target.source, this.env.selection_sources) == -1; + return !is_other || this.commands.move ? 1 : 2; + } + } + // droptarget is a (writable) addressbook and it's not the source + else if (!target.readonly && (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1)) { + return this.commands.move ? 1 : 2; } } - // droptarget is a (writable) addressbook - contact copy action - else if (!this.env.contactfolders[id].readonly) { - // search result may contain contacts from many sources - return (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1) ? 2 : 0; - } - } } return 0; }; - this.open_window = function(url, width, height) + // open popup window + this.open_window = function(url, small, toolbar) { - var dh = (window.outerHeight || 0) - (window.innerHeight || 0), - dw = (window.outerWidth || 0) - (window.innerWidth || 0), - sh = screen.availHeight || screen.height, - sw = screen.availWidth || screen.width, - w = Math.min(width, sw), - h = Math.min(height, sh), - l = Math.max(0, (sw - w) / 2 + (screen.left || 0)), - t = Math.max(0, (sh - h) / 2 + (screen.top || 0)), - wname = 'rcmextwin' + new Date().getTime(), - extwin = window.open(url + (url.match(/\?/) ? '&' : '?') + '_extwin=1', wname, - 'width='+(w-dw)+',height='+(h-dh)+',top='+t+',left='+l+',resizable=yes,toolbar=no,status=no,location=no'); + var wname = 'rcmextwin' + new Date().getTime(); + + url += (url.match(/\?/) ? '&' : '?') + '_extwin=1'; + + if (this.env.standard_windows) + extwin = window.open(url, wname); + else { + var win = this.is_framed() ? parent.window : window, + page = $(win), + page_width = page.width(), + page_height = bw.mz ? $('body', win).height() : page.height(), + w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width), + h = page_height, // always use same height + l = (win.screenLeft || win.screenX) + 20, + t = (win.screenTop || win.screenY) + 20, + extwin = window.open(url, wname, + 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes' + +(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no')); + } // write loading... message to empty windows if (!url && extwin.document) { @@ -1649,7 +1695,7 @@ // focus window, delayed to bring to front window.setTimeout(function() { extwin.focus(); }, 10); - return wname; + return extwin; }; @@ -1686,6 +1732,14 @@ if (!row.depth && row.has_children && (expando = document.getElementById('rcmexpando'+row.uid))) { row.expando = expando; expando.onmousedown = function(e) { return self.expand_message_row(e, uid); }; + if (bw.touch) { + expando.addEventListener('touchend', function(e) { + if (e.changedTouches.length == 1) { + self.expand_message_row(e, uid); + return rcube_event.cancel(e); + } + }, false); + } } this.triggerEvent('insertrow', { uid:uid, row:row }); @@ -1731,7 +1785,6 @@ + (!flags.seen ? ' unread' : '') + (flags.deleted ? ' deleted' : '') + (flags.flagged ? ' flagged' : '') - + (flags.unread_children && flags.seen && !this.env.autoexpand_threads ? ' unroot' : '') + (message.selected ? ' selected' : ''), row = { cols:[], style:{}, id:'rcmrow'+uid }; @@ -1781,6 +1834,9 @@ expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '"> </div>'; row_class += ' thread' + (message.expanded? ' expanded' : ''); } + + if (flags.unread_children && flags.seen && !message.expanded) + row_class += ' unroot'; } tree += '<span id="msgicn'+uid+'" class="'+css_class+'"> </span>'; @@ -1826,7 +1882,7 @@ html = expando; else if (c == 'subject') { if (bw.ie) { - col.onmouseover = function() { rcube_webmail.long_subject_title_ie(this, message.depth+1); }; + col.onmouseover = function() { rcube_webmail.long_subject_title_ex(this, message.depth+1); }; if (bw.ie8) tree = '<span></span>' + tree; // #1487821 } @@ -1943,7 +1999,7 @@ } else { if (!preview && this.env.message_extwin && !this.env.extwin) - this.open_window(this.env.comm_path+url, 1000, 1200); + this.open_window(this.env.comm_path+url, true); else this.location_href(this.env.comm_path+url, target, true); @@ -1969,14 +2025,18 @@ if (name && (frame = this.get_frame_element(name))) { if (!show && (win = this.get_frame_window(name))) { - if (win.location && win.location.href.indexOf(this.env.blankpage)<0) - win.location.href = this.env.blankpage; + if (win.stop) + win.stop(); + else // IE + win.document.execCommand('Stop'); + + win.location.href = this.env.blankpage; } else if (!bw.safari && !bw.konq) $(frame)[show ? 'show' : 'hide'](); } - if (!show && this.busy) + if (!show && this.env.frame_lock) this.set_busy(false, null, this.env.frame_lock); }; @@ -2104,12 +2164,12 @@ this.clear_message_list = function() { - this.env.messages = {}; - this.last_selected = 0; + this.env.messages = {}; + this.last_selected = 0; - this.show_contentframe(false); - if (this.message_list) - this.message_list.clear(true); + this.show_contentframe(false); + if (this.message_list) + this.message_list.clear(true); }; // send remote request to load message list @@ -2554,7 +2614,7 @@ // Hide message command buttons until a message is selected this.enable_command(this.env.message_commands, false); - this._with_selected_messages('moveto', post_data, lock); + this._with_selected_messages('move', post_data, lock); }; // delete selected messages from the current mailbox @@ -2613,7 +2673,7 @@ this._with_selected_messages('delete', post_data); }; - // Send a specifc moveto/delete request with UIDs of all selected messages + // Send a specifc move/delete request with UIDs of all selected messages // @private this._with_selected_messages = function(action, post_data, lock) { @@ -2655,7 +2715,7 @@ this.delete_excessive_thread_rows(); if (!lock) { - msg = action == 'moveto' ? 'movingmessage' : 'deletingmessage'; + msg = action == 'move' ? 'movingmessage' : 'deletingmessage'; lock = this.display_message(this.get_label(msg), 'loading'); } @@ -2773,10 +2833,10 @@ { var len = a_uids.length, i, uid, all_deleted = true, - rows = this.message_list ? this.message_list.rows : []; + rows = this.message_list ? this.message_list.rows : {}; if (len == 1) { - if (!rows.length || (rows[a_uids[0]] && !rows[a_uids[0]].deleted)) + if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted)) this.flag_as_deleted(a_uids); else this.flag_as_undeleted(a_uids); @@ -2817,7 +2877,7 @@ var r_uids = [], post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}), lock = this.display_message(this.get_label('markingmessage'), 'loading'), - rows = this.message_list ? this.message_list.rows : [], + rows = this.message_list ? this.message_list.rows : {}, count = 0; for (var i=0, len=a_uids.length; i<len; i++) { @@ -2837,7 +2897,7 @@ // make sure there are no selected rows if (this.env.skip_deleted && this.message_list) { - if(!this.env.display_next) + if (!this.env.display_next) this.message_list.clear_selection(); if (count < 0) post_data._count = (count*-1); @@ -2861,7 +2921,7 @@ this.flag_deleted_as_read = function(uids) { var icn_src, uid, i, len, - rows = this.message_list ? this.message_list.rows : []; + rows = this.message_list ? this.message_list.rows : {}; uids = String(uids).split(','); @@ -2969,11 +3029,12 @@ // open new compose window if (this.env.compose_extwin && !this.env.extwin) { - this.open_window(url, 1150, 900); + this.open_window(url); } else { this.redirect(url); - window.resizeTo(Math.max(1150, $(window).width()), Math.max(900, $(window).height())); + if (this.env.extwin) + window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24); } }; @@ -3015,7 +3076,7 @@ this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length); // add signature according to selected identity // if we have HTML editor, signature is added in callback - if (input_from.prop('type') == 'select-one' && !this.env.opened_extwin) { + if (input_from.prop('type') == 'select-one') { this.change_identity(input_from[0]); } } @@ -3076,12 +3137,18 @@ this.compose_recipient_select = function(list) { - this.enable_command('add-recipient', list.selection.length > 0); + var id, n, recipients = 0; + for (n=0; n < list.selection.length; n++) { + id = list.selection[n]; + if (this.env.contactdata[id]) + recipients++; + } + this.enable_command('add-recipient', recipients); }; this.compose_add_recipient = function(field) { - var recipients = [], input = $('#_'+field); + var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter; if (this.contact_list && this.contact_list.selection.length) { for (var id, n=0; n < this.contact_list.selection.length; n++) { @@ -3100,8 +3167,10 @@ } if (recipients.length && input.length) { - var oldval = input.val(); - input.val((oldval ? oldval + this.env.recipients_delimiter : '') + recipients.join(this.env.recipients_delimiter)); + var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$'); + if (oldval && !rx.test(oldval)) + oldval += delim + ' '; + input.val(oldval + recipients.join(delim + ' ') + delim + ' '); this.triggerEvent('add-recipient', { field:field, recipients:recipients }); } }; @@ -3344,12 +3413,57 @@ if (!show_sig) show_sig = this.env.show_sig; - var cursor_pos, p = -1, + // first function execution + if (!this.env.identities_initialized) { + this.env.identities_initialized = true; + if (this.env.show_sig_later) + this.env.show_sig = true; + if (this.env.opened_extwin) + return; + } + + var i, rx, cursor_pos, p = -1, id = obj.options[obj.selectedIndex].value, input_message = $("[name='_message']"), message = input_message.val(), is_html = ($("input[name='_is_html']").val() == '1'), - sig = this.env.identity; + sig = this.env.identity, + delim = this.env.recipients_separator, + rx_delim = RegExp.escape(delim), + headers = ['replyto', 'bcc']; + + // update reply-to/bcc fields with addresses defined in identities + for (i in headers) { + var key = headers[i], + old_val = sig && this.env.identities[sig] ? this.env.identities[sig][key] : '', + new_val = id && this.env.identities[id] ? this.env.identities[id][key] : '', + input = $('[name="_'+key+'"]'), input_val = input.val(); + + // remove old address(es) + if (old_val && input_val) { + rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*'); + input_val = input_val.replace(rx, ''); + } + + // cleanup + rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g'); + input_val = input_val.replace(rx, delim); + rx = new RegExp('^[\\s' + rx_delim + ']+'); + input_val = input_val.replace(rx, ''); + + // add new address(es) + if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) { + if (input_val) { + rx = new RegExp('[' + rx_delim + '\\s]+$') + input_val = input_val.replace(rx, '') + delim + ' '; + } + + input_val += new_val + delim + ' '; + } + + if (old_val || new_val) + input.val(input_val).change(); + } // enable manual signature insert if (this.env.signatures && this.env.signatures[id]) { @@ -3449,8 +3563,8 @@ return true; }; - // upload attachment file - this.upload_file = function(form) + // upload (attachment) file + this.upload_file = function(form, action) { if (!form) return false; @@ -3477,7 +3591,7 @@ return; } - var frame_name = this.async_upload_form(form, 'upload', function(e) { + var frame_name = this.async_upload_form(form, action || 'upload', function(e) { var d, content = ''; try { if (this.contentDocument) { @@ -3529,7 +3643,12 @@ att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">' + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html; - var indicator, li = $('<li>').attr('id', name).addClass(att.classname).html(att.html); + var indicator, li = $('<li>'); + + li.attr('id', name) + .addClass(att.classname) + .html(att.html) + .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this, 0); }); // replace indicator's li if (upload_id && (indicator = document.getElementById(upload_id))) { @@ -3675,7 +3794,7 @@ this.env.search_id = null; }; - this.sent_successfully = function(type, msg, target) + this.sent_successfully = function(type, msg, folders) { this.display_message(msg, type); @@ -3684,9 +3803,11 @@ this.lock_form(this.gui_objects.messageform); if (rc) { rc.display_message(msg, type); - // refresh the folder where sent message was saved - if (target && rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == target) - rc.command('checkmail'); + // refresh the folder where sent message was saved or replied message comes from + if (folders && rc.env.task == 'mail' && rc.env.action == '' && $.inArray(rc.env.mailbox, folders) >= 0) { + // @TODO: try with 'checkmail' here when #1485186 is fixed. See also #1489249. + rc.command('list'); + } } setTimeout(function(){ window.close() }, 1000); } @@ -3843,7 +3964,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); @@ -3870,34 +3991,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.indexOf(old_value) == 0 && (!ac || ac.num <= 0) && this.env.contacts && !this.env.contacts.length) + if (old_value && old_value.length && q.indexOf(old_value) == 0 && (!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; @@ -3909,7 +4022,6 @@ // display search results var i, len, ul, li, text, 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 @@ -3963,27 +4075,8 @@ 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) @@ -4018,7 +4111,8 @@ // 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); @@ -4029,18 +4123,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]}); }; @@ -4059,43 +4141,55 @@ if (this.preview_timer) clearTimeout(this.preview_timer); - var n, id, sid, ref = this, writable = false, + var n, id, sid, contact, ref = this, 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 if (id = list.get_single_selection()) this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200); else if (this.env.contentframe) this.show_contentframe(false); if (list.selection.length) { + list.draggable = false; + // no source = search result, we'll need to detect if any of // selected contacts are in writable addressbook to enable edit/delete // we'll also need to know sources used in selection for copy // and group-addmember operations (drag&drop) this.env.selection_sources = []; - if (!source) { - for (n in list.selection) { + + if (source) { + this.env.selection_sources.push(this.env.source); + } + + for (n in list.selection) { + contact = list.data[list.selection[n]]; + if (!source) { sid = String(list.selection[n]).replace(/^[^-]+-/, ''); if (sid && this.env.address_sources[sid]) { - writable = writable || !this.env.address_sources[sid].readonly; + writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly); this.env.selection_sources.push(sid); } } - this.env.selection_sources = $.unique(this.env.selection_sources); + else { + writable = writable || (!source.readonly && !contact.readonly); + } + + if (contact._type != 'group') + list.draggable = true; } - else { - this.env.selection_sources.push(this.env.source); - writable = !source.readonly; - } + + this.env.selection_sources = $.unique(this.env.selection_sources); } // if a group is currently selected, and there is at least one contact selected // 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('group-remove-selected', this.env.group && list.selection.length > 0 && writable); this.enable_command('compose', this.env.group || list.selection.length > 0); - this.enable_command('export-selected', list.selection.length > 0); + this.enable_command('export-selected', 'copy', list.selection.length > 0); this.enable_command('edit', id && writable); - this.enable_command('delete', list.selection.length && writable); + this.enable_command('delete', 'move', list.selection.length > 0 && writable); return false; }; @@ -4123,10 +4217,28 @@ else if (!this.env.search_request) folder = group ? 'G'+src+group : src; - this.select_folder(folder, '', true); - this.env.source = src; this.env.group = group; + + // truncate groups listing stack + var index = $.inArray(this.env.group, this.env.address_group_stack); + if (index < 0) + this.env.address_group_stack = []; + else + this.env.address_group_stack = this.env.address_group_stack.slice(0,index); + + // make sure the current group is on top of the stack + if (this.env.group) { + this.env.address_group_stack.push(this.env.group); + + // mark the first group on the stack as selected in the directory list + folder = 'G'+src+this.env.address_group_stack[0]; + } + else if (this.gui_objects.addresslist_title) { + $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); + } + + this.select_folder(folder, '', true); // load contacts remotely if (this.gui_objects.contactslist) { @@ -4182,16 +4294,38 @@ this.list_contacts_clear = function() { + this.contact_list.data = {}; this.contact_list.clear(true); this.show_contentframe(false); - this.enable_command('delete', false); + this.enable_command('delete', 'move', 'copy', false); this.enable_command('compose', this.env.group ? true : false); + }; + + this.set_group_prop = function(prop) + { + if (this.gui_objects.addresslist_title) { + var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents + + // add link to pop back to parent group + if (this.env.address_group_stack.length > 1) { + $('<a href="#list">...</a>') + .addClass('poplink') + .appendTo(boxtitle) + .click(function(e){ return ref.command('popgroup','',this); }); + boxtitle.append(' » '); + } + + boxtitle.append($('<span>').text(prop.name)); + } + + this.triggerEvent('groupupdate', prop); }; // load contact record this.load_contact = function(cid, action, framed) { - var win, url = {}, target = window; + var win, url = {}, target = window, + rec = this.contact_list ? this.contact_list.data[cid] : null; if (win = this.get_frame_window(this.env.contentframe)) { url._framed = 1; @@ -4201,7 +4335,9 @@ // load dummy content, unselect selected row(s) if (!cid) this.contact_list.clear_selection(); - this.enable_command('delete', 'compose', 'export-selected', cid); + + this.enable_command('compose', rec && rec.email); + this.enable_command('export-selected', rec && rec._type != 'group'); } else if (framed) return false; @@ -4231,14 +4367,38 @@ this.http_post('group-'+what+'members', post_data, lock); }; - // copy a contact to the specified target (group or directory) - this.copy_contact = function(cid, to) + this.contacts_drag_menu = function(e, to) + { + var dest = to.type == 'group' ? to.source : to.id, + source = this.env.source; + + if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly) + return true; + + // search result may contain contacts from many sources, but if there is only one... + if (source == '' && this.env.selection_sources.length == 1) + source = this.env.selection_sources[0]; + + if (to.type == 'group' && dest == source) { + var cid = this.contact_list.get_selection().join(','); + this.group_member_change('add', cid, dest, to.id); + return true; + } + // move action is not possible, "redirect" to copy if menu wasn't requested + else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) { + this.copy_contacts(to); + return true; + } + + return this.drag_menu(e, to); + }; + + // copy contact(s) to the specified target (group or directory) + this.copy_contacts = function(to) { var n, dest = to.type == 'group' ? to.source : to.id, source = this.env.source, - group = this.env.group ? this.env.group : ''; - - if (!cid) + group = this.env.group ? this.env.group : '', cid = this.contact_list.get_selection().join(','); if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly) @@ -4251,13 +4411,12 @@ // tagret is a group if (to.type == 'group') { if (dest == source) - this.group_member_change('add', cid, dest, to.id); - else { - var lock = this.display_message(this.get_label('copyingcontact'), 'loading'), - post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group}; + return; - this.http_post('copy', post_data, lock); - } + var lock = this.display_message(this.get_label('copyingcontact'), 'loading'), + post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group}; + + this.http_post('copy', post_data, lock); } // target is an addressbook else if (to.id != source) { @@ -4268,19 +4427,53 @@ } }; - this.delete_contacts = function() + // move contact(s) to the specified target (group or directory) + this.move_contacts = function(to) { - var selection = this.contact_list.get_selection(), - undelete = this.env.source && this.env.address_sources[this.env.source].undelete; + var dest = to.type == 'group' ? to.source : to.id, + source = this.env.source, + group = this.env.group ? this.env.group : ''; - // exit if no mailbox specified or if selection is empty - if (!(selection.length || this.env.cid) || (!undelete && !confirm(this.get_label('deletecontactconfirm')))) + if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly) return; - var id, n, a_cids = [], - post_data = {_source: this.env.source, _from: (this.env.action ? this.env.action : '')}, - lock = this.display_message(this.get_label('contactdeleting'), 'loading'); + // search result may contain contacts from many sources, but if there is only one... + if (source == '' && this.env.selection_sources.length == 1) + source = this.env.selection_sources[0]; + if (to.type == 'group') { + if (dest == source) + return; + + this._with_selected_contacts('move', {_to: dest, _togid: to.id}); + } + // target is an addressbook + else if (to.id != source) + this._with_selected_contacts('move', {_to: to.id}); + }; + + // delete contact(s) + this.delete_contacts = function() + { + var undelete = this.env.source && this.env.address_sources[this.env.source].undelete; + + if (!undelete && !confirm(this.get_label('deletecontactconfirm'))) + return; + + return this._with_selected_contacts('delete'); + }; + + this._with_selected_contacts = function(action, post_data) + { + var selection = this.contact_list ? this.contact_list.get_selection() : []; + + // exit if no mailbox specified or if selection is empty + if (!selection.length && !this.env.cid) + return; + + var n, a_cids = [], + label = action == 'delete' ? 'contactdeleting' : 'movingcontact', + lock = this.display_message(this.get_label(label), 'loading'); if (this.env.cid) a_cids.push(this.env.cid); else { @@ -4295,6 +4488,11 @@ this.show_contentframe(false); } + if (!post_data) + post_data = {}; + + post_data._source = this.env.source; + post_data._from = this.env.action; post_data._cid = a_cids.join(','); if (this.env.group) @@ -4305,13 +4503,13 @@ post_data._search = this.env.search_request; // send request to server - this.http_post('delete', post_data, lock) + this.http_post(action, post_data, lock) return true; }; // update a contact record in the list - this.update_contact_row = function(cid, cols_arr, newcid, source) + this.update_contact_row = function(cid, cols_arr, newcid, source, data) { var c, row, list = this.contact_list; @@ -4325,10 +4523,11 @@ } list.update_row(cid, cols_arr, newcid, true); + list.data[cid] = data; }; // add row to contacts list - this.add_contact_row = function(cid, cols, classes) + this.add_contact_row = function(cid, cols, classes, data) { if (!this.gui_objects.contactslist) return false; @@ -4350,6 +4549,8 @@ row.cols.push(col); } + // store data in list member + list.data[cid] = data; list.insert_row(row); this.enable_command('export', list.rowcount > 0); @@ -4776,7 +4977,7 @@ this.replace_contact_photo = function(id) { var img_src = id == '-del-' ? this.env.photo_placeholder : - this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id; + this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id; this.set_photo_actions(id); $(this.gui_objects.contactphoto).children('img').attr('src', img_src); @@ -5760,14 +5961,14 @@ for (c=0, len=repl.length; c < len; c++) { cell = document.createElement('td'); - cell.innerHTML = repl[c].html; + cell.innerHTML = repl[c].html || ''; if (repl[c].id) cell.id = repl[c].id; if (repl[c].className) cell.className = repl[c].className; tr.appendChild(cell); } th.appendChild(tr); thead.parentNode.replaceChild(th, thead); - thead = th; + list.thead = thead = th; } for (n=0, len=this.env.coltypes.length; n<len; n++) { @@ -6177,7 +6378,7 @@ this.enable_command('export-selected', false); } - case 'moveto': + case 'move': if (this.env.action == 'show') { // re-enable commands on move/delete error this.enable_command(this.env.message_commands, true); @@ -6218,6 +6419,7 @@ if ((response.action == 'list' || response.action == 'search') && this.message_list) { this.msglist_select(this.message_list); + this.message_list.resize(); this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount }); } } @@ -6228,6 +6430,7 @@ this.enable_command('search-create', this.env.source == ''); this.enable_command('search-delete', this.env.search_id); this.update_group_commands(); + this.contact_list.resize(); this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount }); } } @@ -6288,6 +6491,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 reqid = new Date().getTime(); + + 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 + var item, threads = prop.threads || 1; + for (var 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 @@ -6389,9 +6716,10 @@ url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }), contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, processData: false, + timeout: 0, // disable default timeout set in ajaxSetup() data: formdata || multipart, headers: {'X-Roundcube-Request': ref.env.request_token}, - beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; }, + xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; }, success: function(data){ ref.http_response(data); }, error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); } }); @@ -6431,7 +6759,7 @@ multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf; multipart += 'Content-Length: ' + file.size + crlf; multipart += 'Content-Type: ' + file.type + crlf + crlf; - multipart += e.target.result + crlf; + multipart += reader.result + crlf; multipart += dashdash + boundary + crlf; if (j == last) // we're done, submit the data @@ -6716,15 +7044,6 @@ return 1; } - // this will detect any pdf plugin including PDF.js in Firefox - var obj = document.createElement('OBJECT'); - obj.onload = function() { rcmail.env.browser_capabilities.pdf = 1; }; - obj.onerror = function() { rcmail.env.browser_capabilities.pdf = 0; }; - obj.style.display = 'none'; - obj.type = 'application/pdf'; - obj.data = 'program/resources/blank.pdf'; - document.body.appendChild(obj); - return 0; }; @@ -6761,11 +7080,11 @@ if (!elem.title) { var $elem = $(elem); if ($elem.width() + indent * 15 > $elem.parent().width()) - elem.title = $elem.html(); + elem.title = $elem.text(); } }; -rcube_webmail.long_subject_title_ie = function(elem, indent) +rcube_webmail.long_subject_title_ex = function(elem, indent) { if (!elem.title) { var $elem = $(elem), -- Gitblit v1.9.1