From 686ff4b538b3fc4f0c632b1d35a123b2ee3770cc Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Fri, 23 Jan 2015 03:49:47 -0500
Subject: [PATCH] Fix keyboard navigation and css in datepicker widget across many Firefox versions

---
 program/js/app.js |  269 +++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 197 insertions(+), 72 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 2018cfb..1028b77 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,7 +558,7 @@
 
     // 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) {
@@ -572,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' }) });
     }
 
@@ -630,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)
@@ -1051,12 +1055,9 @@
         url = {};
 
         if (this.task == 'mail') {
-          url._mbox = this.env.mailbox;
+          url = {_mbox: this.env.mailbox, _search: this.env.search_request};
           if (props)
             url._to = props;
-          // also send search request so we can go back to search result after message is sent
-          if (this.env.search_request)
-            url._search = this.env.search_request;
         }
         // modify url if we're in addressbook
         else if (this.task == 'addressbook') {
@@ -1151,7 +1152,7 @@
       case 'reply-list':
       case 'reply':
         if (uid = this.get_single_uid()) {
-          url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid)};
+          url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
           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');
@@ -1212,7 +1213,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;
@@ -1221,7 +1223,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') {
@@ -1405,8 +1407,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);
   };
@@ -1416,7 +1420,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)
@@ -1947,7 +1954,7 @@
     // attach events
     $.each(fn, function(i, f) {
       row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
-      if (bw.touch) {
+      if (bw.touch && row[i].addEventListener) {
         row[i].addEventListener('touchend', function(e) {
           if (e.changedTouches.length == 1) {
             f(e);
@@ -2027,7 +2034,7 @@
     }
     if (flags.forwarded) {
       status_class += ' forwarded';
-      status_label += this.get_label('replied') + ' ';
+      status_label += this.get_label('forwarded') + ' ';
     }
 
     // update selection
@@ -2251,7 +2258,7 @@
       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_unread_message(id, ref.env.mailbox);
-          ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1});
+          ref.http_post('mark', {_uid: id, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
         }, this.env.preview_pane_mark_read * 1000);
       }
     }
@@ -2477,6 +2484,16 @@
         selection.push(selected[i]);
 
     this.message_list.selection = selection;
+
+    // reset preview frame, if currently previewed message is not selected (has been removed)
+    try {
+      var win = this.get_frame_window(this.env.contentframe),
+        id = win.rcmail.env.uid;
+
+      if (id && $.inArray(id, selection) < 0)
+        this.show_contentframe(false);
+    }
+    catch (e) {};
   };
 
   // expand all threads with unread children
@@ -3319,7 +3336,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),
@@ -3353,11 +3370,18 @@
     }
 
     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);
       }
     }
 
@@ -3419,6 +3443,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
@@ -3428,6 +3453,7 @@
           },
           {
             text: this.get_label('delete'),
+            'class': 'delete',
             click: function(){
               ref.remove_compose_data(key);
               $(this).dialog('close');
@@ -3617,11 +3643,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) {
@@ -3648,7 +3678,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>' +
@@ -3676,7 +3706,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();
@@ -3758,14 +3788,13 @@
 
   this.set_draft_id = function(id)
   {
-    var rc;
-
     if (id && id != this.env.draft_id) {
-      if (rc = this.opener()) {
-        // refresh the drafts folder in opener window
-        if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox)
-          rc.command('checkmail');
-      }
+      var filter = {task: 'mail', action: ''},
+        rc = this.opener(false, filter) || this.opener(true, filter);
+
+      // refresh the drafts folder in the opener window
+      if (rc && rc.env.mailbox == this.env.drafts_mailbox)
+        rc.command('checkmail');
 
       this.env.draft_id = id;
       $("input[name='_draft_saveid']").val(id);
@@ -3836,7 +3865,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)
@@ -3924,7 +3953,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});
       }
     }
   };
@@ -3952,7 +3981,6 @@
     this.local_storage_remove_item('compose.index');
   };
 
-
   this.change_identity = function(obj, show_sig)
   {
     if (!obj || !obj.options)
@@ -3960,6 +3988,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) {
@@ -3969,11 +4010,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() {
@@ -4007,14 +4043,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;
@@ -4321,6 +4349,7 @@
       (this.env.search_request && (this.env.search_scope || 'base') != 'base');
   };
 
+  // action executed after mail is sent
   this.sent_successfully = function(type, msg, folders)
   {
     this.display_message(msg, type);
@@ -4329,11 +4358,13 @@
     if (this.env.extwin) {
       this.lock_form(this.gui_objects.messageform);
 
-      var rc = this.opener();
+      var filter = {task: 'mail', action: ''},
+        rc = this.opener(false, filter) || this.opener(true, filter);
+
       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) {
+        if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
           rc.command('checkmail');
         }
       }
@@ -4693,7 +4724,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);
@@ -4750,6 +4781,9 @@
 
     if (!src)
       src = this.env.source;
+
+    if (refresh)
+      group = this.env.group;
 
     if (page && this.current_page == page && src == this.env.source && group == this.env.group)
       return false;
@@ -5139,10 +5173,10 @@
         dateFormat: this.env.date_format,
         changeMonth: true,
         changeYear: true,
-        yearRange: '-100:+10',
+        yearRange: '-120:+10',
         showOtherMonths: true,
         selectOtherMonths: true,
-        onSelect: function(dateText) { $(this).focus().val(dateText) }
+//        onSelect: function(dateText) { $(this).focus().val(dateText); }
       });
       $('input.datepicker').datepicker();
     }
@@ -5162,6 +5196,7 @@
     this.show_popup_dialog(content, this.get_label('newgroup'),
       [{
         text: this.get_label('save'),
+        'class': 'mainaction',
         click: function() {
           var name;
 
@@ -5189,6 +5224,7 @@
     this.show_popup_dialog(content, this.get_label('grouprename'),
       [{
         text: this.get_label('save'),
+        'class': 'mainaction',
         click: function() {
           var name;
 
@@ -5552,6 +5588,7 @@
     this.show_popup_dialog(content, this.get_label('searchsave'),
       [{
         text: this.get_label('save'),
+        'class': 'mainaction',
         click: function() {
           var name;
 
@@ -5770,6 +5807,9 @@
         // on the list when dragging starts (and stops), this is slow, but
         // I didn't find a method to check droptarget on over event
         accept: function(node) {
+          if (!$(node).is('.mailbox'))
+            return false;
+
           var source_folder = ref.folder_id2name($(node).attr('id')),
             dest_folder = ref.folder_id2name(this.id),
             source = ref.env.subscriptionrows[source_folder],
@@ -5790,7 +5830,7 @@
 
   this.folder_id2name = function(id)
   {
-    return ref.html_identifier_decode(id.replace(/^rcmli/, ''));
+    return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
   };
 
   this.subscription_select = function(id)
@@ -6336,7 +6376,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())
@@ -6345,18 +6385,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';
@@ -6389,7 +6445,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');
     }
@@ -6398,6 +6454,7 @@
 
     if (timeout > 0)
       setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
+
     return id;
   };
 
@@ -6476,6 +6533,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)
   {
@@ -6491,14 +6577,16 @@
     else
       popup.html(content);
 
-    popup.dialog($.extend({
+    options = $.extend({
         title: title,
         buttons: buttons,
         modal: true,
         resizable: true,
         width: 500,
         close: function(event, ui) { $(this).remove(); }
-      }, options || {}));
+      }, options || {});
+
+    popup.dialog(options);
 
     // resize and center popup
     var win = $(window), w = win.width(), h = win.height(),
@@ -6507,6 +6595,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;
@@ -6645,7 +6738,7 @@
   this.set_quota = function(content)
   {
     if (this.gui_objects.quotadisplay && content && content.type == 'text')
-      $(this.gui_objects.quotadisplay).html(content.percent+'%').attr('title', content.title);
+      $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title);
 
     this.triggerEvent('setquota', content);
     this.env.quota_content = content;
@@ -7812,12 +7905,24 @@
   };
 
   // get window.opener.rcmail if available
-  this.opener = function()
+  this.opener = function(deep, filter)
   {
+    var i, win = window.opener;
+
     // catch Error: Permission denied to access property rcmail
     try {
-      if (window.opener && !opener.closed && opener.rcmail)
-        return opener.rcmail;
+      if (win && !win.closed) {
+        // try parent of the opener window, e.g. preview frame
+        if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
+          win = win.parent;
+
+        if (win.rcmail && filter)
+          for (i in filter)
+            if (win.rcmail.env[i] != filter[i])
+              return;
+
+        return win.rcmail;
+      }
     }
     catch (e) {}
   };
@@ -7826,13 +7931,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
@@ -7967,7 +8076,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()
@@ -7980,7 +8089,7 @@
     if (plugin && plugin.enabledPlugin)
         return 1;
 
-    if (window.ActiveXObject) {
+    if ('ActiveXObject' in window) {
       try {
         if (plugin = new ActiveXObject("AcroPDF.PDF"))
           return 1;
@@ -8013,7 +8122,7 @@
     if (plugin && plugin.enabledPlugin)
         return 1;
 
-    if (window.ActiveXObject) {
+    if ('ActiveXObject' in window) {
       try {
         if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
           return 1;
@@ -8022,6 +8131,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
@@ -8088,7 +8206,7 @@
   if (!elem.title) {
     var $elem = $(elem);
     if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
-      elem.title = $elem.text();
+      elem.title = rcube_webmail.subject_text(elem);
   }
 };
 
@@ -8105,10 +8223,17 @@
 
     tmp.remove();
     if (w + $('span.branch', $elem).width() * 15 > $elem.width())
-      elem.title = txt;
+      elem.title = rcube_webmail.subject_text(elem);
   }
 };
 
+rcube_webmail.subject_text = function(elem)
+{
+  var t = $(elem).clone();
+  t.find('.skip-on-drag').remove();
+  return t.text();
+};
+
 rcube_webmail.prototype.get_cookie = getCookie;
 
 // copy event engine prototype

--
Gitblit v1.9.1