From 007f1b443888a17b5156ef871e5548d28a3a0130 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Sun, 20 Jul 2014 08:00:58 -0400 Subject: [PATCH] Warn for unsent/unsaved message when closing compose window; remove localStorage copy if page was left intentionally but not on session errors (#1489818) --- program/js/app.js | 363 ++++++++++++++++++++++++++++++++++----------------- 1 files changed, 239 insertions(+), 124 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 3c54756..69cbca8 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -142,7 +142,7 @@ 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))) { this.goto_url('error', '_code=0x199'); return; } @@ -564,7 +564,7 @@ // 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) obj.blur(); @@ -593,6 +593,7 @@ // remove copy from local storage if compose screen is left intentionally this.remove_compose_data(this.env.compose_id); + this.compose_skip_unsavedcheck = true; } // process external commands @@ -650,11 +651,17 @@ var form = this.gui_objects.messageform, win = this.open_window(''); - this.save_compose_form_local(); - $("input[name='_action']", form).val('compose'); - form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); - form.target = win.name; - form.submit(); + if (win) { + this.save_compose_form_local(); + this.compose_skip_unsavedcheck = true; + $("input[name='_action']", form).val('compose'); + form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); + form.target = win.name; + form.submit(); + } + else { + // this.display_message(this.get_label('windowopenerror'), 'error'); + } } else { this.open_window(this.env.permaurl, true); @@ -1049,7 +1056,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': @@ -1171,9 +1182,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': @@ -1181,6 +1200,7 @@ var file = document.getElementById('rcmimportfile'); if (file && !file.value) { alert(this.get_label('selectimportfile')); + aborted = true; break; } this.gui_objects.importform.submit(); @@ -1232,9 +1252,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; }; @@ -1307,7 +1327,7 @@ var url = this.get_task_url(task); if (task == 'mail') url += '&_mbox=INBOX'; - else if (task == 'logout') + else if (task == 'logout' && !this.env.server_error) this.clear_compose_data(); this.redirect(url); @@ -1434,11 +1454,31 @@ this.drag_end = function(e) { - this.drag_active = false; - this.env.last_folder_target = null; + var list, model; if (this.treelist) this.treelist.drag_end(); + + // execute drag & drop action when mouse was released + if (list = this.message_list) + model = this.env.mailboxes; + else if (list = this.contact_list) + model = this.env.contactfolders; + + if (this.drag_active && model && this.env.last_folder_target) { + var target = model[this.env.last_folder_target]; + list.draglayer.hide(); + + 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); + } + + this.drag_active = false; + this.env.last_folder_target = null; }; this.drag_move = function(e) @@ -1499,37 +1539,15 @@ this.doc_mouse_up = function(e) { - var model, list, id; + var list, id; // ignore event if jquery UI dialog is open if ($(rcube_event.get_target(e)).closest('.ui-dialog, .ui-widget-overlay').length) return; - if (list = this.message_list) - model = this.env.mailboxes; - else if (list = this.contact_list) - model = this.env.contactfolders; - else if (this.ksearch_value) - this.ksearch_blur(); - + list = this.message_list || this.contact_list; if (list && !rcube_mouse_is_over(e, list.list.parentNode)) list.blur(); - - // handle mouse release when dragging - if (this.drag_active && model && this.env.last_folder_target) { - var target = model[this.env.last_folder_target]; - - this.env.last_folder_target = null; - list.draglayer.hide(); - this.drag_end(e); - - 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 if (this.buttons_sel) { @@ -1889,18 +1907,18 @@ tree += '<span id="msgicn'+uid+'" class="'+css_class+'"> </span>'; row.className = row_class; - // build subject link - if (!bw.ie && cols.subject) { + // 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)+')">'+cols.subject+'</a>'; + ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+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() }; + col = {className: String(c).toLowerCase(), events:{}}; if (c == 'flag') { css_class = (flags.flagged ? 'flagged' : 'unflagged'); @@ -1928,11 +1946,8 @@ 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') { @@ -2051,18 +2066,35 @@ this.location_href(this.env.comm_path+url, target, true); // mark as read and change mbox unread counter - if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) { + if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read > 0) { this.preview_read_timer = setTimeout(function() { - ref.set_message(id, 'unread', false); - ref.update_thread_root(id, 'read'); - if (ref.env.unread_counts[ref.env.mailbox]) { - ref.env.unread_counts[ref.env.mailbox] -= 1; - ref.set_unread_count(ref.env.mailbox, ref.env.unread_counts[ref.env.mailbox], ref.env.mailbox == 'INBOX'); - } - if (ref.env.preview_pane_mark_read > 0) - ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1}); + ref.set_unread_message(id, ref.env.mailbox); + ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1}); }, this.env.preview_pane_mark_read * 1000); } + } + }; + + // update message status and unread counter after marking a message as read + this.set_unread_message = function(id, folder) + { + var self = this; + + // find window with messages list + if (!self.message_list) + self = self.opener(); + + if (!self && window.parent) + self = parent.rcmail; + + if (!self || !self.message_list) + return; + + self.set_message(id, 'unread', false); + + if (self.env.unread_counts[folder] > 0) { + self.env.unread_counts[folder] -= 1; + self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX'); } }; @@ -2139,7 +2171,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 @@ -2560,8 +2592,11 @@ if (!row) return false; - if (flag == 'unread') + if (flag == 'unread') { + if (row.unread != status) + this.update_thread_root(uid, status ? 'unread' : 'read'); row.unread = status; + } else if(flag == 'deleted') row.deleted = status; else if (flag == 'replied') @@ -2849,9 +2884,6 @@ this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false)); this.http_post('mark', post_data, lock); - - for (i=0; i<len; i++) - this.update_thread_root(a_uids[i], flag); }; // set image to flagged or unflagged @@ -3097,7 +3129,12 @@ // close compose step in opener if (opener_rc && opener_rc.env.action == 'compose') { - setTimeout(function(){ opener.history.back(); }, 100); + setTimeout(function(){ + if (opener.history.length > 1) + opener.history.back(); + else + opener_rc.redirect(opener_rc.get_task_url('mail')); + }, 100); this.env.opened_extwin = true; } @@ -3140,6 +3177,10 @@ } // skip records from 'other' drafts if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) { + continue; + } + // skip records on reply + if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) { continue; } // show dialog asking to restore the message @@ -3222,6 +3263,7 @@ form._draft.value = draft ? '1' : ''; form.action = this.add_url(form.action, '_unlock', msgid); form.action = this.add_url(form.action, '_lang', lang); + form.action = this.add_url(form.action, '_framed', 1); // register timer to notify about connection timeout this.submit_timer = setTimeout(function(){ @@ -3368,17 +3410,8 @@ $(tinyMCE.get(props.id).getBody()).css('font-family', rcmail.env.default_font); }, 500); } - else { - var thisMCE = tinyMCE.get(props.id), existingHtml; - - if (existingHtml = thisMCE.getContent()) { - if (!confirm(this.get_label('editorwarning'))) { - return false; - } - this.html2plain(existingHtml, props.id); - } + else if (this.html2plain(tinyMCE.get(props.id).getContent(), props.id)) tinyMCE.execCommand('mceRemoveControl', false, props.id); - } return true; }; @@ -3393,7 +3426,7 @@ if ($("input[name='_is_html']").val() == '1') { var editor = tinyMCE.get(this.env.composebody); editor.getWin().focus(); // correct focus in IE & Chrome - editor.selection.setContent(insert, { format:'text' }); + editor.selection.setContent(this.quote_html(insert).replace(/\r?\n/g, '<br/>'), { format:'text' }); } // replace selection in compose textarea else { @@ -3525,10 +3558,7 @@ // submit delete request if (key && confirm(this.get_label('deleteresponseconfirm'))) { this.http_post('settings/delete-response', { _key: key }, false); - return true; } - - return false; }; this.stop_spellchecking = function() @@ -3617,14 +3647,29 @@ this.env.draft_id = id; $("input[name='_draft_saveid']").val(id); - this.remove_compose_data(this.env.compose_id); + // reset history of hidden iframe used for saving draft (#1489643) + // 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 + this.remove_compose_data(this.env.compose_id); + this.compose_skip_unsavedcheck = false; }; this.auto_save_start = function() { - if (this.env.draft_autosave) - this.save_timer = setTimeout(function(){ ref.command("savedraft"); }, this.env.draft_autosave * 1000); + if (this.env.draft_autosave) { + 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) { @@ -3638,6 +3683,21 @@ ref.compose_type_activity_last = ref.compose_type_activity; } }, 5000); + + $(window).unload(function() { + // remove copy from local storage if compose screen is left after warning + if (!ref.env.server_error) + ref.remove_compose_data(ref.env.compose_id); + }); + } + + // check for unsaved changes before leaving the compose page + if (!window.onbeforeunload) { + window.onbeforeunload = function() { + if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) { + return ref.get_label('notsentwarning'); + } + }; } // Unlock interface now that saving is complete @@ -3681,6 +3741,9 @@ if (this.env.draft_id) { formdata.draft_id = this.env.draft_id; + } + if (this.env.reply_msgid) { + formdata.reply_msgid = this.env.reply_msgid; } $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) { @@ -3940,7 +4003,7 @@ this.upload_file = function(form, action) { if (!form) - return false; + return; // count files and size on capable browser var size = 0, numfiles = 0; @@ -3961,7 +4024,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) { @@ -3995,11 +4058,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 @@ -4021,7 +4084,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))) { @@ -4041,8 +4104,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) @@ -4170,23 +4235,25 @@ this.sent_successfully = function(type, msg, folders) { this.display_message(msg, type); + this.compose_skip_unsavedcheck = true; if (this.env.extwin) { - var rc = this.opener(); this.lock_form(this.gui_objects.messageform); + + var rc = this.opener(); if (rc) { rc.display_message(msg, type); // 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'); + rc.command('checkmail'); } } - setTimeout(function(){ window.close() }, 1000); + + setTimeout(function() { window.close(); }, 1000); } else { // before redirect we need to wait some time for Chrome (#1486177) - setTimeout(function(){ ref.list_mailbox(); }, 500); + setTimeout(function() { ref.list_mailbox(); }, 500); } }; @@ -4303,9 +4370,7 @@ this.ksearch_input.value = pre + insert + end; // set caret to insert pos - cpos = p+insert.length; - if (this.ksearch_input.setSelectionRange) - this.ksearch_input.setSelectionRange(cpos, cpos); + this.set_caret_pos(this.ksearch_input, p + insert.length); if (trigger) { this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert }); @@ -5564,10 +5629,8 @@ id = this.env.iid ? this.env.iid : selection[0]; // submit request with appended token - if (confirm(this.get_label('deleteidentityconfirm'))) - this.goto_url('delete-identity', { _iid: id, _token: this.env.request_token }, true); - - return true; + if (id && confirm(this.get_label('deleteidentityconfirm'))) + this.http_post('settings/delete-identity', { _iid: id }, true); }; this.update_identity_row = function(id, name, add) @@ -5611,6 +5674,23 @@ frame.location.href = this.env.blankpage; } } + + this.enable_command('delete', false); + }; + + this.remove_identity = function(id) + { + var frame, list = this.identity_list, + rid = this.html_identifier(id); + + if (list && id) { + list.remove_row(rid); + if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) { + frame.location.href = this.env.blankpage; + } + } + + this.enable_command('delete', false); }; @@ -6093,7 +6173,10 @@ // disable/enable input buttons if (button.type == 'input') { button.status = state; - obj.disabled = !state; + obj.disabled = state == 'pas'; + } + else if (button.type == 'uibutton') { + $(obj).button('option', 'disabled', state == 'pas'); } } }; @@ -6309,7 +6392,7 @@ { // forward call to parent window if (this.is_framed()) { - return parent.rcmail.show_popup_dialog(html, title, buttons); + return parent.rcmail.show_popup_dialog(html, title, buttons, options); } var popup = $('<div class="popup">') @@ -6329,7 +6412,7 @@ popup.dialog('option', { height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)), - width: Math.min(w - 20, width + 20) + width: Math.min(w - 20, width + 36) }); return popup; @@ -6363,12 +6446,14 @@ this.mark_folder = function(name, class_name, prefix, encode) { $(this.get_folder_li(name, prefix, encode)).addClass(class_name); + this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true}); }; // adds a class to selected folder this.unmark_folder = function(name, class_name, prefix, encode) { $(this.get_folder_li(name, prefix, encode)).removeClass(class_name); + this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false}); }; // helper method to find a folder list item @@ -6568,8 +6653,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') + ); } }; @@ -6662,14 +6748,22 @@ this.element_position = function(element, obj) { var obj = $(obj), win = $(window), - width = obj.width(), - height = obj.height(), + width = obj.outerWidth(), + height = obj.outerHeight(), + menu_pos = obj.data('menu-pos'), win_height = win.height(), elem_height = $(element).height(), elem_width = $(element).width(), pos = obj.offset(), top = pos.top, left = pos.left + width; + + if (menu_pos == 'bottom') { + top += height; + left -= width; + } + else + left -= 5; if (top + elem_height > win_height) { top -= elem_height - height; @@ -6690,6 +6784,16 @@ this.html2plain = function(htmlText, id) { + // warn the user (if converted content is not empty) + if (!htmlText || !(htmlText.replace(/<[^>]+>| |\s/g, '')).length) { + // without setTimeout() here, textarea is filled with initial (onload) content + setTimeout(function() { $('#'+id).val(''); }, 50); + return true; + } + + if (!confirm(this.get_label('editorwarning'))) + return false; + var rcmail = this, url = '?_task=utils&_action=html2text', lock = this.set_busy(true, 'converting'); @@ -6700,6 +6804,8 @@ 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); } }); + + return true; }; this.plain2html = function(plain, id) @@ -6746,7 +6852,7 @@ param[k] = query[k]; } - return base + '&' + $.param(param) + querystring; + return base + (base.indexOf('?') > -1 ? '&' : '?') + $.param(param) + querystring; }; this.redirect = function(url, lock) @@ -6770,7 +6876,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) @@ -7017,7 +7123,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"); @@ -7045,6 +7151,7 @@ // save message in local storage and do not redirect if (this.env.action == 'compose') { this.save_compose_form_local(); + this.compose_skip_unsavedcheck = true; } else if (redirect_url) { window.setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000); @@ -7275,7 +7382,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 @@ -7464,20 +7571,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 @@ -7622,7 +7737,7 @@ } }; -rcube_webmail.long_subject_title_ex = function(elem, indent) +rcube_webmail.long_subject_title_ex = function(elem) { if (!elem.title) { var $elem = $(elem), @@ -7634,7 +7749,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