From ceee7eb110df884155af024f3b2b7eebb8dd3378 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <bruederli@kolabsys.com> Date: Thu, 18 Dec 2014 07:13:07 -0500 Subject: [PATCH] Prevent folder selection changes if app is busy (#1490158) --- program/js/app.js | 256 ++++++++++++++++++++++++++++++++++++++++---------- 1 files changed, 204 insertions(+), 52 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 346c4f7..3d714b9 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -58,7 +58,6 @@ request_timeout: 180, // seconds draft_autosave: 0, // seconds comm_path: './', - blankpage: 'program/resources/blank.gif', recipients_separator: ',', recipients_delimiter: ', ', popup_width: 1150, @@ -162,6 +161,9 @@ this.goto_url('error', '_code=0x199'); return; } + + if (!this.env.blankpage) + this.env.blankpage = this.assets_path('program/resources/blank.gif'); // find all registered gui containers for (n in this.gui_containers) @@ -556,13 +558,14 @@ // show message if (this.pending_message) - this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); + this.display_message.apply(this, this.pending_message); // init treelist widget if (this.gui_objects.folderlist && window.rcube_treelist_widget) { this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { selectable: true, id_prefix: 'rcmli', + parent_focus: true, id_encode: this.html_identifier_encode, id_decode: this.html_identifier_decode, check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) } @@ -571,6 +574,7 @@ this.treelist .addEventListener('collapse', function(node) { ref.folder_collapsed(node) }) .addEventListener('expand', function(node) { ref.folder_collapsed(node) }) + .addEventListener('beforeselect', function(node) { return !ref.busy; }) .addEventListener('select', function(node) { ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) }); } @@ -629,8 +633,9 @@ if (obj && obj.blur && !(event && rcube_event.is_keyboard(event))) obj.blur(); - // do nothing if interface is locked by other command (with exception for searching reset) - if (this.busy && !(command == 'reset-search' && this.last_command == 'search')) + // do nothing if interface is locked by another command + // with exception for searching reset and menu + if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/)) return false; // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab) @@ -1211,7 +1216,8 @@ // reset quicksearch case 'reset-search': - var n, s = this.env.search_request || this.env.qsearch; + var n, s = this.env.search_request || this.env.qsearch, + ss = this.gui_objects.qsearchbox && this.gui_objects.qsearchbox.value != ''; this.reset_qsearch(); this.select_all_mode = false; @@ -1220,7 +1226,7 @@ if (this.contact_list) this.list_contacts_clear(); } - else if (s && this.env.mailbox) { + else if (s && ss && this.env.mailbox) { this.list_mailbox(this.env.mailbox, 1); } else if (s && this.task == 'addressbook') { @@ -1404,8 +1410,10 @@ if (task == 'mail') url += '&_mbox=INBOX'; - else if (task == 'logout' && !this.env.server_error) + else if (task == 'logout' && !this.env.server_error) { + url += '&_token=' + this.env.request_token; this.clear_compose_data(); + } this.redirect(url); }; @@ -1415,7 +1423,10 @@ if (!url) url = this.env.comm_path; - return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task); + if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/)) + return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task); + else + return url.replace(/\?.*$/, '') + '?_task=' + task; }; this.reload = function(delay) @@ -1659,7 +1670,7 @@ } skip = obj.data('parent'); } - }, 10); + }, 10, e); }; // global keypress event handler @@ -2026,7 +2037,7 @@ } if (flags.forwarded) { status_class += ' forwarded'; - status_label += this.get_label('replied') + ' '; + status_label += this.get_label('forwarded') + ' '; } // update selection @@ -2481,7 +2492,7 @@ // expand all threads with unread children this.expand_unread = function() { - var r, tbody = this.gui_objects.messagelist.tBodies[0], + var r, tbody = this.message_list.tbody, new_row = tbody.firstChild; while (new_row) { @@ -3318,7 +3329,7 @@ if (!this.gui_objects.messageform) return false; - var i, input_from = $("[name='_from']"), + var i, pos, input_from = $("[name='_from']"), input_to = $("[name='_to']"), input_subject = $("input[name='_subject']"), input_message = $("[name='_message']").get(0), @@ -3352,16 +3363,24 @@ } if (!html_mode) { - this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length); + pos = this.env.top_posting ? 0 : input_message.value.length; + this.set_caret_pos(input_message, pos); + // add signature according to selected identity // if we have HTML editor, signature is added in callback if (input_from.prop('type') == 'select-one') { this.change_identity(input_from[0]); } + + // scroll to the bottom of the textarea (#1490114) + if (pos) { + $(input_message).scrollTop(input_message.scrollHeight); + } } // check for locally stored compose data - this.compose_restore_dialog(0, html_mode) + if (this.env.save_localstorage) + this.compose_restore_dialog(0, html_mode) if (input_to.val() == '') input_to.focus(); @@ -3417,6 +3436,7 @@ this.get_label('restoremessage'), [{ text: this.get_label('restore'), + 'class': 'mainaction', click: function(){ ref.restore_compose_form(key, html_mode); ref.remove_compose_data(key); // remove old copy @@ -3426,6 +3446,7 @@ }, { text: this.get_label('delete'), + 'class': 'delete', click: function(){ ref.remove_compose_data(key); $(this).dialog('close'); @@ -3615,11 +3636,15 @@ this.toggle_editor = function(props, obj, e) { // @todo: this should work also with many editors on page - var result = this.editor.toggle(props.html); + var result = this.editor.toggle(props.html, props.noconvert || false); + + // satisfy the expectations of aftertoggle-editor event subscribers + props.mode = props.html ? 'html' : 'plain'; if (!result && e) { // fix selector value if operation failed - $(e.target).filter('select').val(props.html ? 'plain' : 'html'); + props.mode = props.html ? 'plain' : 'html'; + $(e.target).filter('select').val(props.mode); } if (result) { @@ -3646,7 +3671,7 @@ this.save_response = function() { // show dialog to enter a name and to modify the text to be saved - var buttons = {}, text = this.editor.get_content(true, true), + var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}), html = '<form class="propform">' + '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' + '<input type="text" name="name" id="ffresponsename" size="40" /></div>' + @@ -3674,7 +3699,7 @@ $(this).dialog('close'); }; - this.show_popup_dialog(html, this.gettext('newresponse'), buttons); + this.show_popup_dialog(html, this.gettext('newresponse'), buttons, {button_classes: ['mainaction']}); $('#ffresponsetext').val(text); $('#ffresponsename').select(); @@ -3793,7 +3818,7 @@ } // save compose form content to local storage every 5 seconds - if (!this.local_save_timer && window.localStorage) { + if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) { // track typing activity and only save on changes this.compose_type_activity = this.compose_type_activity_last = 0; $(document).bind('keypress', function(e){ ref.compose_type_activity++; }); @@ -3834,7 +3859,7 @@ if (val = $('[name="_' + hash_fields[i] + '"]').val()) str += val + ':'; - str += this.editor.get_content(); + str += this.editor.get_content({refresh: false}); if (this.env.attachments) for (id in this.env.attachments) @@ -3849,6 +3874,10 @@ // store the contents of the compose form to localstorage this.save_compose_form_local = function() { + // feature is disabled + if (!this.env.save_localstorage) + return; + var formdata = { session:this.env.session_id, changed:new Date().getTime() }, ed, empty = true; @@ -3918,7 +3947,7 @@ // initialize HTML editor if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) { - this.command('toggle-editor', {id: this.env.composebody, html: !html_mode}); + this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true}); } } }; @@ -3955,6 +3984,19 @@ if (!show_sig) show_sig = this.env.show_sig; + var id = obj.options[obj.selectedIndex].value, + sig = this.env.identity, + delim = this.env.recipients_separator, + rx_delim = RegExp.escape(delim); + + // enable manual signature insert + if (this.env.signatures && this.env.signatures[id]) { + this.enable_command('insert-sig', true); + this.env.compose_commands.push('insert-sig'); + } + else + this.enable_command('insert-sig', false); + // first function execution if (!this.env.identities_initialized) { this.env.identities_initialized = true; @@ -3963,11 +4005,6 @@ if (this.env.opened_extwin) return; } - - var id = obj.options[obj.selectedIndex].value, - sig = this.env.identity, - delim = this.env.recipients_separator, - rx_delim = RegExp.escape(delim); // update reply-to/bcc fields with addresses defined in identities $.each(['replyto', 'bcc'], function() { @@ -4001,14 +4038,6 @@ if (old_val || new_val) input.val(input_val).change(); }); - - // enable manual signature insert - if (this.env.signatures && this.env.signatures[id]) { - this.enable_command('insert-sig', true); - this.env.compose_commands.push('insert-sig'); - } - else - this.enable_command('insert-sig', false); this.editor.change_signature(id, show_sig); this.env.identity = id; @@ -4439,7 +4468,7 @@ this.ksearch_destroy(); // insert all members of a group - if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group') { + if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) { 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); @@ -4583,7 +4612,7 @@ 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>')) + .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>')) .addClass(type || '') .appendTo(ul) .mouseover(function() { ref.ksearch_select(this); }) @@ -4687,7 +4716,7 @@ 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()) + if (this.env.contentframe && (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); @@ -4739,6 +4768,7 @@ this.list_contacts = function(src, group, page) { var win, folder, url = {}, + refresh = src === undefined && group === undefined && page === undefined, target = window; if (!src) @@ -4751,7 +4781,7 @@ page = this.env.current_page = 1; this.reset_qsearch(); } - else if (group != this.env.group) + else if (!refresh && group != this.env.group) page = this.env.current_page = 1; if (this.env.search_id) @@ -4889,6 +4919,9 @@ if (action && (cid || action == 'add') && !this.drag_active) { if (this.env.group) url._gid = this.env.group; + + if (this.env.search_request) + url._search = this.env.search_request; url._action = action; url._source = this.env.source; @@ -5152,6 +5185,7 @@ this.show_popup_dialog(content, this.get_label('newgroup'), [{ text: this.get_label('save'), + 'class': 'mainaction', click: function() { var name; @@ -5179,6 +5213,7 @@ this.show_popup_dialog(content, this.get_label('grouprename'), [{ text: this.get_label('save'), + 'class': 'mainaction', click: function() { var name; @@ -5542,6 +5577,7 @@ this.show_popup_dialog(content, this.get_label('searchsave'), [{ text: this.get_label('save'), + 'class': 'mainaction', click: function() { var name; @@ -5741,6 +5777,8 @@ this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, { selectable: true, + tabexit: false, + parent_focus: true, id_prefix: 'rcmli', id_encode: this.html_identifier_encode, id_decode: this.html_identifier_decode, @@ -5834,6 +5872,12 @@ if (!this.gui_objects.subscriptionlist) return false; + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); + } + // disable drag-n-drop temporarily this.subscription_list.draggable('destroy').droppable('destroy'); @@ -5852,8 +5896,9 @@ row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name}); if (!refrow || !refrow.length) { - // remove old subfolders and toggle + // remove old data, subfolders and toggle $('ul,div.treetoggle', row).remove(); + row.removeData('filtered'); } // set folder name @@ -5980,7 +6025,7 @@ this.subscription_list.expand(this.folder_id2name(parent.id)); } - row = row.get(0); + row = row.show().get(0); if (row.scrollIntoView) row.scrollIntoView(); @@ -5997,6 +6042,12 @@ } return false; + } + + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); } var subfolders = {}, @@ -6044,6 +6095,12 @@ // remove the table row of a specific mailbox from the table this.remove_folder_row = function(folder) { + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); + } + var list = [], row = this.subscription_list.get_item(folder, true); // get subfolders if any @@ -6112,6 +6169,37 @@ $('#folder-size').replaceWith(size); }; + // filter folders by namespace + this.folder_filter = function(prefix) + { + this.subscription_list.reset_search(); + + this.subscription_list.container.children('li').each(function() { + var i, folder = ref.folder_id2name(this.id); + // show all folders + if (prefix == '---') { + } + // got namespace prefix + else if (prefix) { + if (folder !== prefix) { + $(this).data('filtered', true).hide(); + return + } + } + // no namespace prefix, filter out all other namespaces + else { + // first get all namespace roots + for (i in ref.env.ns_roots) { + if (folder === ref.env.ns_roots[i]) { + $(this).data('filtered', true).hide(); + return; + } + } + } + + $(this).removeData('filtered').show(); + }); + }; /*********************************************************/ /********* GUI functionality *********/ @@ -6274,7 +6362,7 @@ }; // display a system message, list of types in common.css (below #message definition) - this.display_message = function(msg, type, timeout) + this.display_message = function(msg, type, timeout, key) { // pass command to parent window if (this.is_framed()) @@ -6283,18 +6371,34 @@ if (!this.gui_objects.message) { // save message in order to display after page loaded if (type != 'loading') - this.pending_message = [msg, type, timeout]; + this.pending_message = [msg, type, timeout, key]; return 1; } - type = type ? type : 'notice'; + if (!type) + type = 'notice'; - var key = this.html_identifier(msg), - date = new Date(), + if (!key) + key = this.html_identifier(msg); + + var date = new Date(), id = type + date.getTime(); - if (!timeout) - timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1); + if (!timeout) { + switch (type) { + case 'error': + case 'warning': + timeout = this.message_time * 2; + break; + + case 'uploading': + timeout = 0; + break; + + default: + timeout = this.message_time; + } + } if (type == 'loading') { key = 'loading'; @@ -6327,7 +6431,7 @@ if (type == 'loading') { this.messages[key].labels = [{'id': id, 'msg': msg}]; } - else { + else if (type != 'uploading') { obj.click(function() { return ref.hide_message(obj); }) .attr('role', 'alert'); } @@ -6336,6 +6440,7 @@ if (timeout > 0) setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout); + return id; }; @@ -6414,6 +6519,35 @@ this.messages = {}; }; + // display uploading message with progress indicator + // data should contain: name, total, current, percent, text + this.display_progress = function(data) + { + if (!data || !data.name) + return; + + var msg = this.messages['progress' + data.name]; + + if (!data.label) + data.label = this.get_label('uploadingmany'); + + if (!msg) { + if (!data.percent || data.percent < 100) + this.display_message(data.label, 'uploading', 0, 'progress' + data.name); + return; + } + + if (!data.total || data.percent >= 100) { + this.hide_message(msg.obj); + return; + } + + if (data.text) + data.label += ' ' + data.text; + + msg.obj.text(data.label); + }; + // open a jquery UI dialog with the given content this.show_popup_dialog = function(content, title, buttons, options) { @@ -6445,6 +6579,11 @@ popup.dialog('option', { height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)), width: Math.min(w - 20, width + 36) + }); + + // assign special classes to dialog buttons + $.each(options.button_classes || [], function(i, v) { + if (v) $($('.ui-dialog-buttonpane button.ui-button', popup.parent()).get(i)).addClass(v); }); return popup; @@ -6840,7 +6979,7 @@ // 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]); + this.hide_menu(this.menu_stack[i], event); } if (stack && this.menu_stack.length) { obj.data('parent', $.last(this.menu_stack)); @@ -7764,13 +7903,17 @@ // and return the message uid this.get_single_uid = function() { - return this.env.uid ? this.env.uid : (this.message_list ? this.message_list.get_single_selection() : null); + var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null); + var result = ref.triggerEvent('get_single_uid', { uid: uid }); + return result || uid; }; // same as above but for contacts this.get_single_cid = function() { - return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null); + var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null); + var result = ref.triggerEvent('get_single_cid', { cid: cid }); + return result || cid; }; // get the IMP mailbox of the message with the given UID @@ -7905,7 +8048,7 @@ img.onload = function() { ref.env.browser_capabilities.tif = 1; }; img.onerror = function() { ref.env.browser_capabilities.tif = 0; }; - img.src = 'program/resources/blank.tif'; + img.src = this.assets_path('program/resources/blank.tif'); }; this.pdf_support_check = function() @@ -7962,6 +8105,15 @@ return 0; }; + this.assets_path = function(path) + { + if (this.env.assets_path && !path.startsWith(this.env.assets_path)) { + path = this.env.assets_path + path; + } + + return path; + }; + // Cookie setter this.set_cookie = function(name, value, expires) { -- Gitblit v1.9.1