From a02c77c584906f629d382409e76f0df4d2cfaf01 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Fri, 15 Mar 2013 05:30:53 -0400
Subject: [PATCH] Add ability to toggle between view as HTML and text while viewing a message (#1486939)

---
 program/js/app.js |  422 +++++++++++++++++++++-------------------------------
 1 files changed, 173 insertions(+), 249 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 6d5bdfe..d3c319e 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -4,7 +4,7 @@
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
  | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
- | Copyright (C) 2011-2012, Kolab Systems AG                             |
+ | Copyright (C) 2011-2013, Kolab Systems AG                             |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -179,7 +179,8 @@
     }
 
     // enable general commands
-    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', 'compose', 'undo', 'about', 'switch-task', true);
+    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
+      'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true);
 
     if (this.env.permaurl)
       this.enable_command('permaurl', 'extwin', true);
@@ -205,12 +206,13 @@
           this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
           this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
           this.message_list.addEventListener('column_replace', function(e){ p.msglist_set_coltypes(e); });
+          this.message_list.addEventListener('listupdate', function(e){ p.triggerEvent('listupdate', e); });
 
           document.onmouseup = function(e){ return p.doc_mouse_up(e); };
           this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
 
           this.message_list.init();
-          this.enable_command('toggle_status', 'toggle_flag', 'menu-open', 'menu-save', 'sort', true);
+          this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
 
           // load messages
           this.command('list');
@@ -226,8 +228,8 @@
 
         this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
           'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
-          'print', 'load-attachment', 'show-headers', 'hide-headers', 'download',
-          'forward', 'forward-inline', 'forward-attachment'];
+          'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
+          'forward', 'forward-inline', 'forward-attachment', 'change-format'];
 
         if (this.env.action == 'show' || this.env.action == 'preview') {
           this.enable_command(this.env.message_commands, this.env.uid);
@@ -459,8 +461,22 @@
       this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
 
     // map implicit containers
-    if (this.gui_objects.folderlist)
+    if (this.gui_objects.folderlist) {
       this.gui_containers.foldertray = $(this.gui_objects.folderlist);
+
+      // init treelist widget
+      if (window.rcube_treelist_widget) {
+        this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
+          id_prefix: 'rcmli',
+          id_encode: this.html_identifier_encode,
+          id_decode: this.html_identifier_decode,
+          check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) }
+        });
+        this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
+        this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
+        this.treelist.addEventListener('select', function(node){ ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
+      }
+    }
 
     // activate html5 file drop feature (if browser supports it and if configured)
     if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
@@ -580,18 +596,35 @@
 
       case 'extwin':
         if (this.env.action == 'compose') {
-          var prevstate = this.env.compose_extwin;
-          $("input[name='_action']", this.gui_objects.messageform).val('compose');
-          this.gui_objects.messageform.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
-          this.gui_objects.messageform.target = this.open_window('', 1100, 900);
-          this.gui_objects.messageform.submit();
+          var form = this.gui_objects.messageform;
+
+          $("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.submit();
         }
         else {
           this.open_window(this.env.permaurl, 900, 900);
         }
         break;
 
+      case 'change-format':
+        url = this.env.permaurl + '&_format=' + props;
+
+        if (this.env.action == 'preview')
+          url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
+        if (this.env.extwin)
+          url += '&_extwin=1';
+
+        location.href = url;
+        break;
+
       case 'menu-open':
+        if (props && props.menu == 'attachmentmenu') {
+          var mimetype = this.env.attachments[props.id];
+          this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
+        }
+
       case 'menu-save':
         this.triggerEvent(command, {props:props});
         return false;
@@ -756,7 +789,7 @@
       case 'moveto':
         if (this.task == 'mail')
           this.move_messages(props);
-        else if (this.task == 'addressbook' && this.drag_active)
+        else if (this.task == 'addressbook')
           this.copy_contact(null, props);
         break;
 
@@ -817,11 +850,14 @@
         break;
 
       case 'load-attachment':
-        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props.part;
+      case 'open-attachment':
+      case 'download-attachment':
+        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
+          mimetype = this.env.attachments[props];
 
         // open attachment in frame if it's of a supported mimetype
-        if (this.env.uid && props.mimetype && this.env.mimetypes && $.inArray(props.mimetype, this.env.mimetypes) >= 0) {
-          var attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', 'rcubemailattachment'+this.env.uid+props.part);
+        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', 'rcubemailattachment'+this.env.uid+props);
           if (attachment_win) {
             setTimeout(function(){ attachment_win.focus(); }, 10);
             break;
@@ -864,7 +900,7 @@
 
       case 'nextmessage':
         if (this.env.next_uid)
-          this.show_message(this.env.next_uid, false, this.env.action=='preview');
+          this.show_message(this.env.next_uid, false, this.env.action == 'preview');
         break;
 
       case 'lastmessage':
@@ -1210,7 +1246,7 @@
     if (!url)
       url = this.env.comm_path;
 
-    return url.replace(/_task=[a-z]+/, '_task='+task);
+    return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
   };
 
   this.reload = function(delay)
@@ -1263,11 +1299,12 @@
 
   this.html_identifier = function(str, encode)
   {
-    str = String(str);
-    if (encode)
-      return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
-    else
-      return str.replace(this.identifier_expr, '_');
+    return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
+  };
+
+  this.html_identifier_encode = function(str)
+  {
+    return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
   };
 
   this.html_identifier_decode = function(str)
@@ -1320,29 +1357,9 @@
     if (this.preview_read_timer)
       clearTimeout(this.preview_read_timer);
 
-    // save folderlist and folders location/sizes for droptarget calculation in drag_move()
-    if (this.gui_objects.folderlist && model) {
-      this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset;
-      this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop;
-
-      var k, li, height,
-        list = $(this.gui_objects.folderlist);
-        pos = list.offset();
-
-      this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() };
-
-      this.env.folder_coords = [];
-      for (k in model) {
-        if (li = this.get_folder_li(k)) {
-          // only visible folders
-          if (height = li.firstChild.offsetHeight) {
-            pos = $(li.firstChild).offset();
-            this.env.folder_coords[k] = { x1:pos.left, y1:pos.top,
-              x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 };
-          }
-        }
-      }
-    }
+    // prepare treelist widget for dragging interactions
+    if (this.treelist)
+      this.treelist.drag_start();
   };
 
   this.drag_end = function(e)
@@ -1350,87 +1367,28 @@
     this.drag_active = false;
     this.env.last_folder_target = null;
 
-    if (this.folder_auto_timer) {
-      clearTimeout(this.folder_auto_timer);
-      this.folder_auto_timer = null;
-      this.folder_auto_expand = null;
-    }
-
-    // over the folders
-    if (this.gui_objects.folderlist && this.env.folder_coords) {
-      for (var k in this.env.folder_coords) {
-        if (this.env.folder_coords[k].on)
-          $(this.get_folder_li(k)).removeClass('droptarget');
-      }
-    }
+    if (this.treelist)
+      this.treelist.drag_end();
   };
 
   this.drag_move = function(e)
   {
-    if (this.gui_objects.folderlist && this.env.folder_coords) {
-      var k, li, div, check, oldclass,
+    if (this.gui_objects.folderlist) {
+      var drag_target, oldclass,
         layerclass = 'draglayernormal',
-        mouse = rcube_event.get_mouse_pos(e),
-        pos = this.env.folderlist_coords,
-        // offsets to compensate for scrolling while dragging a message
-        boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop,
-        moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop;
+        mouse = rcube_event.get_mouse_pos(e);
 
       if (this.contact_list && this.contact_list.draglayer)
         oldclass = this.contact_list.draglayer.attr('class');
 
-      mouse.y += -moffset-boffset;
-
-      // if mouse pointer is outside of folderlist
-      if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) {
-        if (this.env.last_folder_target) {
-          $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
-          this.env.folder_coords[this.env.last_folder_target].on = 0;
-          this.env.last_folder_target = null;
-        }
-        if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
-          this.contact_list.draglayer.attr('class', layerclass);
-        return;
+      // mouse intersects a valid drop target on the treelist
+      if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
+        this.env.last_folder_target = drag_target;
+        layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
       }
-
-      // over the folders
-      for (k in this.env.folder_coords) {
-        pos = this.env.folder_coords[k];
-        if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) {
-          if (check = this.check_droptarget(k)) {
-            li = this.get_folder_li(k);
-            div = $(li.getElementsByTagName('div')[0]);
-
-            // if the folder is collapsed, expand it after 1sec and restart the drag & drop process.
-            if (div.hasClass('collapsed')) {
-              if (this.folder_auto_timer)
-                clearTimeout(this.folder_auto_timer);
-
-              this.folder_auto_expand = this.env.mailboxes[k].id;
-              this.folder_auto_timer = setTimeout(function() {
-                rcmail.command('collapse-folder', rcmail.folder_auto_expand);
-                rcmail.drag_start(null);
-              }, 1000);
-            }
-            else if (this.folder_auto_timer) {
-              clearTimeout(this.folder_auto_timer);
-              this.folder_auto_timer = null;
-              this.folder_auto_expand = null;
-            }
-
-            $(li).addClass('droptarget');
-            this.env.folder_coords[k].on = 1;
-            this.env.last_folder_target = k;
-            layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal');
-          }
-          // Clear target, otherwise drag end will trigger move into last valid droptarget
-          else
-            this.env.last_folder_target = null;
-        }
-        else if (pos.on) {
-          $(this.get_folder_li(k)).removeClass('droptarget');
-          this.env.folder_coords[k].on = 0;
-        }
+      else {
+        // Clear target, otherwise drag end will trigger move into last valid droptarget
+        this.env.last_folder_target = null;
       }
 
       if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
@@ -1440,40 +1398,33 @@
 
   this.collapse_folder = function(name)
   {
-    var li = this.get_folder_li(name, '', true),
-      div = $('div:first', li),
-      ul = $('ul:first', li);
+    if (this.treelist)
+      this.treelist.toggle(name);
+  };
 
-    if (div.hasClass('collapsed')) {
-      ul.show();
-      div.removeClass('collapsed').addClass('expanded');
-      var reg = new RegExp('&'+urlencode(name)+'&');
-      this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, '');
-    }
-    else if (div.hasClass('expanded')) {
-      ul.hide();
-      div.removeClass('expanded').addClass('collapsed');
-      this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&';
+  this.folder_collapsed = function(node)
+  {
+    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
+
+    if (node.collapsed) {
+      this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
 
       // select the folder if one of its childs is currently selected
       // don't select if it's virtual (#1488346)
-      if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual'))
+      if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual)
         this.command('list', name);
     }
-    else
-      return;
-
-    // Work around a bug in IE6 and IE7, see #1485309
-    if (bw.ie6 || bw.ie7) {
-      var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null;
-      if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') {
-        li.style.display = 'none';
-        li.style.display = '';
-      }
+    else {
+      var reg = new RegExp('&'+urlencode(node.id)+'&');
+      this.env[prefname] = this.env[prefname].replace(reg, '');
     }
 
-    this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
-    this.set_unread_count_display(name, false);
+    if (!this.drag_active) {
+      this.command('save-pref', { name: prefname, value: this.env[prefname] });
+
+      if (this.env.unread_counts)
+        this.set_unread_count_display(node.id, false);
+    }
   };
 
   this.doc_mouse_up = function(e)
@@ -1498,9 +1449,9 @@
     if (this.drag_active && model && this.env.last_folder_target) {
       var target = model[this.env.last_folder_target];
 
-      $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
       this.env.last_folder_target = null;
       list.draglayer.hide();
+      this.drag_end(e);
 
       if (!this.drag_menu(e, target))
         this.command('moveto', target);
@@ -3044,10 +2995,10 @@
       input_message = $("[name='_message']").get(0),
       html_mode = $("input[name='_is_html']").val() == '1',
       ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
-      ac_props;
+      ac_props, opener_rc = this.opener();
 
     // close compose step in opener
-    if (window.opener && !window.opener.closed && opener.rcmail && opener.rcmail.env.action == 'compose') {
+    if (opener_rc && opener_rc.env.action == 'compose') {
       setTimeout(function(){ opener.history.back(); }, 100);
       this.env.opened_extwin = true;
     }
@@ -3726,9 +3677,10 @@
     this.display_message(msg, type);
 
     if (this.env.extwin) {
+      var opener_rc = this.opener();
       this.lock_form(this.gui_objects.messageform);
-      if (window.opener && !window.opener.closed && opener.rcmail)
-        opener.rcmail.display_message(msg, type);
+      if (opener_rc)
+        opener_rc.display_message(msg, type);
       setTimeout(function(){ window.close() }, 1000);
     }
     else {
@@ -4164,7 +4116,7 @@
     else if (!this.env.search_request)
       folder = group ? 'G'+src+group : src;
 
-    this.select_folder(folder);
+    this.select_folder(folder, '', true);
 
     this.env.source = src;
     this.env.group = group;
@@ -4295,7 +4247,7 @@
         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: source, _to: dest, _togid: to.id, _gid: group};
+          post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
 
         this.http_post('copy', post_data, lock);
       }
@@ -4303,7 +4255,7 @@
     // target is an addressbook
     else if (to.id != source) {
       var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
-        post_data = {_cid: cid, _source: source, _to: to.id, _gid: group};
+        post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
 
       this.http_post('copy', post_data, lock);
     }
@@ -4468,7 +4420,7 @@
       this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
       this.env.group_renaming = true;
 
-      var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG');
+      var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
       if (li && (link = li.firstChild)) {
         $(link).hide().before(this.name_input);
       }
@@ -4488,11 +4440,8 @@
   // callback from server upon group-delete command
   this.remove_group_item = function(prop)
   {
-    var li, key = 'G'+prop.source+prop.id;
-    if ((li = this.get_folder_li(key))) {
-      this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
-
-      li.parentNode.removeChild(li);
+    var key = 'G'+prop.source+prop.id;
+    if (this.treelist.remove(key)) {
       delete this.env.contactfolders[key];
       delete this.env.contactgroups[key];
     }
@@ -4511,8 +4460,11 @@
       this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
       this.name_input_li = $('<li>').addClass(type).append(this.name_input);
 
-      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source);
-      this.name_input_li.insertAfter(li);
+      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : $('ul.groups li:last', this.get_folder_li(this.env.source,'',true));
+      if (li.length)
+        this.name_input_li.insertAfter(li);
+      else
+        this.name_input_li.appendTo(type == 'contactsearch' ? this.gui_objects.folderlist : $('ul.groups', this.get_folder_li(this.env.source,'',true)));
     }
 
     this.name_input.select().focus();
@@ -4596,14 +4548,12 @@
       link = $('<a>').attr('href', '#')
         .attr('rel', prop.source+':'+prop.id)
         .click(function() { return rcmail.command('listgroup', prop, this); })
-        .html(prop.name),
-      li = $('<li>').attr({id: 'rcmli'+this.html_identifier(key), 'class': 'contactgroup'})
-        .append(link);
+        .html(prop.name);
 
     this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
-    this.add_contact_group_row(prop, li);
+    this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true);
 
-    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
+    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
   };
 
   // callback for renaming a contact group
@@ -4612,15 +4562,13 @@
     this.reset_add_input();
 
     var key = 'G'+prop.source+prop.id,
-      li = this.get_folder_li(key),
-      link;
+      newnode = {};
 
     // group ID has changed, replace link node and identifiers
-    if (li && prop.newid) {
+    if (prop.newid) {
       var newkey = 'G'+prop.source+prop.newid,
-        newprop = $.extend({}, prop);;
+        newprop = $.extend({}, prop);
 
-      li.id = 'rcmli' + this.html_identifier(newkey);
       this.env.contactfolders[newkey] = this.env.contactfolders[key];
       this.env.contactfolders[newkey].id = prop.newid;
       this.env.group = prop.newid;
@@ -4631,45 +4579,22 @@
       newprop.id = prop.newid;
       newprop.type = 'group';
 
-      link = $('<a>').attr('href', '#')
+      newnode.id = newkey;
+      newnode.html = $('<a>').attr('href', '#')
         .attr('rel', prop.source+':'+prop.newid)
         .click(function() { return rcmail.command('listgroup', newprop, this); })
         .html(prop.name);
-      $(li).children().replaceWith(link);
     }
     // update displayed group name
-    else if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
-      link.innerHTML = prop.name;
-
-    this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
-    this.add_contact_group_row(prop, $(li), true);
-
-    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
-  };
-
-  // add contact group row to the list, with sorting
-  this.add_contact_group_row = function(prop, li, reloc)
-  {
-    var row, name = prop.name.toUpperCase(),
-      sibling = this.get_folder_li(prop.source),
-      prefix = 'rcmliG' + this.html_identifier(prop.source);
-
-    // When renaming groups, we need to remove it from DOM and insert it in the proper place
-    if (reloc) {
-      row = li.clone(true);
-      li.remove();
+    else {
+      $(this.treelist.get_item(key)).children().first().html(prop.name);
+      this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
     }
-    else
-      row = li;
 
-    $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
-      if (name >= $(this).text().toUpperCase())
-        sibling = elem;
-      else
-        return false;
-    });
+    // update list node and re-sort it
+    this.treelist.update(key, newnode, true);
 
-    row.insertAfter(sibling);
+    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
   };
 
   this.update_group_commands = function()
@@ -4901,45 +4826,14 @@
         .attr('rel', id)
         .click(function() { return rcmail.command('listsearch', id, this); })
         .html(name),
-      li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'})
-        .append(link),
-      prop = {name:name, id:id, li:li[0]};
+      prop = { name:name, id:id };
 
-    this.add_saved_search_row(prop, li);
-    this.select_folder('S'+id);
+    this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
+    this.select_folder(key,'',true);
     this.enable_command('search-delete', true);
     this.env.search_id = id;
 
     this.triggerEvent('abook_search_insert', prop);
-  };
-
-  // add saved search row to the list, with sorting
-  this.add_saved_search_row = function(prop, li, reloc)
-  {
-    var row, sibling, name = prop.name.toUpperCase();
-
-    // When renaming groups, we need to remove it from DOM and insert it in the proper place
-    if (reloc) {
-      row = li.clone(true);
-      li.remove();
-    }
-    else
-      row = li;
-
-    $('li[class~="contactsearch"]', this.gui_objects.folderlist).each(function(i, elem) {
-      if (!sibling)
-        sibling = this.previousSibling;
-
-      if (name >= $(this).text().toUpperCase())
-        sibling = elem;
-      else
-        return false;
-    });
-
-    if (sibling)
-      row.insertAfter(sibling);
-    else
-      row.appendTo(this.gui_objects.folderlist);
   };
 
   // creates an input for saved search name
@@ -4960,10 +4854,8 @@
   this.remove_search_item = function(id)
   {
     var li, key = 'S'+id;
-    if ((li = this.get_folder_li(key))) {
+    if (this.treelist.remove(key)) {
       this.triggerEvent('search_delete', { id:id, li:li });
-
-      li.parentNode.removeChild(li);
     }
 
     this.env.search_id = null;
@@ -4982,7 +4874,7 @@
     }
 
     this.reset_qsearch();
-    this.select_folder('S'+id);
+    this.select_folder('S'+id, '', true);
 
     // reset vars
     this.env.current_page = 1;
@@ -5697,14 +5589,15 @@
     if (!this.gui_objects.message)
       return;
 
-    var k, n, i, msg, m = this.messages;
+    var k, n, i, o, m = this.messages;
 
     // Hide message by object, don't use for 'loading'!
     if (typeof obj === 'object') {
-      $(obj)[fade?'fadeOut':'hide']();
-      msg = $(obj).data('key');
-      if (this.messages[msg])
-        delete this.messages[msg];
+      o = $(obj);
+      k = o.data('key');
+      this.hide_message_object(o, fade);
+      if (m[k])
+        delete m[k];
     }
     // Hide message by id
     else {
@@ -5714,7 +5607,7 @@
             m[k].elements.splice(n, 1);
             // hide DOM element if last instance is removed
             if (!m[k].elements.length) {
-              m[k].obj[fade?'fadeOut':'hide']();
+              this.hide_message_object(m[k].obj, fade);
               delete m[k];
             }
             // set pending action label for 'loading' message
@@ -5724,15 +5617,24 @@
                   delete m[k].labels[i];
                 }
                 else {
-                  msg = m[k].labels[i].msg;
+                  o = m[k].labels[i].msg;
+                  m[k].obj.html(o);
                 }
-                m[k].obj.html(msg);
               }
             }
           }
         }
       }
     }
+  };
+
+  // hide message object and remove from the DOM
+  this.hide_message_object = function(o, fade)
+  {
+    if (fade)
+      o.fadeOut(600, function() {$(this).remove(); });
+    else
+      o.hide().remove();
   };
 
   // remove all messages immediately
@@ -5747,7 +5649,7 @@
     for (k in m)
       for (n in m[k].elements)
         if (m[k].obj)
-          m[k].obj.hide();
+          this.hide_message_object(m[k].obj);
 
     this.messages = {};
   };
@@ -5788,7 +5690,10 @@
   // mark a mailbox as selected and set environment variable
   this.select_folder = function(name, prefix, encode)
   {
-    if (this.gui_objects.folderlist) {
+    if (this.treelist) {
+      this.treelist.select(name);
+    }
+    else if (this.gui_objects.folderlist) {
       var current_li, target_li;
 
       if ((current_li = $('li.selected', this.gui_objects.folderlist))) {
@@ -6076,9 +5981,9 @@
     var base = this.env.comm_path, k, param = {};
 
     // overwrite task name
-    if (query._action.match(/([a-z]+)\/([a-z0-9-_.]+)/)) {
+    if (query._action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
       query._action = RegExp.$2;
-      base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
+      base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
     }
 
     // remove undefined values
@@ -6355,6 +6260,14 @@
     if (location_url && this.env.action != 'compose')  // don't redirect on compose screen, contents might get lost (#1488926)
       this.redirect(location_url);
 
+    // 403 Forbidden response (CSRF prevention) - reload the page.
+    // In case there's a new valid session it will be used, otherwise
+    // login form will be presented (#1488960).
+    if (request.status == 403) {
+      (this.is_framed() ? parent : window).location.reload();
+      return;
+    }
+
     // re-send keep-alive requests after 30 seconds
     if (action == 'keep-alive')
       setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
@@ -6607,6 +6520,17 @@
   /*********            helper methods            *********/
   /********************************************************/
 
+  // get window.opener.rcmail if available
+  this.opener = function()
+  {
+    // catch Error: Permission denied to access property rcmail
+    try {
+      if (window.opener && !opener.closed && opener.rcmail)
+        return opener.rcmail;
+    }
+    catch (e) {}
+  };
+
   // check if we're in show mode or if we have a unique selection
   // and return the message uid
   this.get_single_uid = function()

--
Gitblit v1.9.1