Thomas
2013-10-17 b0b2fea4f88a8bdd92f6339a51481ead8317aa93
program/js/app.js
@@ -251,12 +251,15 @@
          }
        }
        else if (this.env.action == 'compose') {
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
          this.env.address_group_stack = [];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
            'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin',
            'insert-response', 'save-response'];
          if (this.env.drafts_mailbox)
            this.env.compose_commands.push('savedraft')
          this.enable_command(this.env.compose_commands, 'identities', true);
          this.enable_command(this.env.compose_commands, 'identities', 'responses', true);
          // add more commands (not enabled)
          $.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
@@ -265,6 +268,23 @@
            this.env.spellcheck.spelling_state_observer = function(s) { ref.spellcheck_state(); };
            this.env.compose_commands.push('spellcheck')
            this.enable_command('spellcheck', true);
          }
          // init canned response functions
          if (this.gui_objects.responseslist) {
            $('a.insertresponse', this.gui_objects.responseslist)
              .attr('unselectable', 'on')
              .mousedown(function(e){ return rcube_event.cancel(e); })
              .mouseup(function(e){
                ref.command('insert-response', $(this).attr('rel'));
                $(document.body).trigger('mouseup');  // hides the menu
                return rcube_event.cancel(e);
              });
              // avoid textarea loosing focus when hitting the save-response button/link
              for (var i=0; this.buttons['save-response'] && i < this.buttons['save-response'].length; i++) {
                $('#'+this.buttons['save-response'][i].id).mousedown(function(e){ return rcube_event.cancel(e); })
              }
          }
          document.onmouseup = function(e){ return p.doc_mouse_up(e); };
@@ -318,11 +338,13 @@
        break;
      case 'addressbook':
        this.env.address_group_stack = [];
        if (this.gui_objects.folderlist)
          this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
        this.enable_command('add', 'import', this.env.writable_source);
        this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true);
        this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true);
        if (this.gui_objects.contactslist) {
          this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
@@ -371,7 +393,7 @@
        break;
      case 'settings':
        this.enable_command('preferences', 'identities', 'save', 'folders', true);
        this.enable_command('preferences', 'identities', 'responses', 'save', 'folders', true);
        if (this.env.action == 'identities') {
          this.enable_command('add', this.env.identities_level < 2);
@@ -392,6 +414,9 @@
          parent.rcmail.enable_command('purge', this.env.messagecount);
          $("input[type='text']").first().select();
        }
        else if (this.env.action == 'responses') {
          this.enable_command('add', true);
        }
        if (this.gui_objects.identitieslist) {
          this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, {multiselect:false, draggable:false, keyboard:false});
@@ -408,8 +433,22 @@
          this.sections_list.init();
          this.sections_list.focus();
        }
        else if (this.gui_objects.subscriptionlist)
        else if (this.gui_objects.subscriptionlist) {
          this.init_subscription_list();
        }
        else if (this.gui_objects.responseslist) {
          this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:false});
          this.responses_list.addEventListener('select', function(list){
            var win, id = list.get_single_selection();
            p.enable_command('delete', !!id);
            if (id && (win = p.get_frame_window(p.env.contentframe))) {
              p.set_busy(true);
              p.location_href({ _action:'edit-response', _key:id, _framed:1 }, win);
            }
          });
          this.responses_list.init();
          this.responses_list.focus();
        }
        break;
@@ -696,6 +735,13 @@
      case 'add':
        if (this.task == 'addressbook')
          this.load_contact(0, 'add');
        else if (this.task == 'settings' && this.env.action == 'responses') {
          var frame;
          if ((frame = this.get_frame_window(this.env.contentframe))) {
            this.set_busy(true);
            this.location_href({ _action:'add-response', _framed:1 }, frame);
          }
        }
        else if (this.task == 'settings') {
          this.identity_list.clear_selection();
          this.load_identity(0, 'add-identity');
@@ -759,7 +805,10 @@
        // addressbook task
        else if (this.task == 'addressbook')
          this.delete_contacts();
        // user settings task
        // settings: canned response
        else if (this.task == 'settings' && this.env.action == 'responses')
          this.delete_response();
        // settings: user identities
        else if (this.task == 'settings')
          this.delete_identity();
        break;
@@ -769,7 +818,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;
@@ -1080,9 +1129,23 @@
        }
        break;
      case 'pushgroup':
        // add group ID to stack
        this.env.address_group_stack.push(props.id);
        if (obj && event)
          rcube_event.cancel(event);
      case 'listgroup':
        this.reset_qsearch();
        this.list_contacts(props.source, props.id);
        break;
      case 'popgroup':
        if (this.env.address_group_stack.length > 1) {
          this.env.address_group_stack.pop();
          this.reset_qsearch();
          this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
        }
        break;
      case 'import':
@@ -1117,6 +1180,7 @@
      // user settings commands
      case 'preferences':
      case 'identities':
      case 'responses':
      case 'folders':
        this.goto_url('settings/' + command);
        break;
@@ -3066,7 +3130,13 @@
  this.compose_recipient_select = function(list)
  {
    this.enable_command('add-recipient', list.selection.length > 0);
    var id, n, recipients = 0;
    for (n=0; n < list.selection.length; n++) {
      id = list.selection[n];
      if (this.env.contactdata[id])
        recipients++;
    }
    this.enable_command('add-recipient', recipients);
  };
  this.compose_add_recipient = function(field)
@@ -3207,6 +3277,154 @@
    }
    return true;
  };
  this.insert_response = function(key)
  {
    var insert = this.env.textresponses[key] ? this.env.textresponses[key].text : null;
    if (!insert)
      return false;
    // insert into tinyMCE editor
    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' });
    }
    // replace selection in compose textarea
    else {
      var textarea = rcube_find_object(this.env.composebody),
        selection = $(textarea).is(':focus') ? this.get_input_selection(textarea) : { start:0, end:0 },
        inp_value = textarea.value;
        pre = inp_value.substring(0, selection.start),
        end = inp_value.substring(selection.end, inp_value.length);
      // insert response text
      textarea.value = pre + insert + end;
      // set caret after inserted text
      this.set_caret_pos(textarea, selection.start + insert.length);
      textarea.focus();
    }
  };
  /**
   * Open the dialog to save a new canned response
   */
  this.save_response = function()
  {
    var sigstart, text = '', strip = false;
    // get selected text from tinyMCE editor
    if ($("input[name='_is_html']").val() == '1') {
      var editor = tinyMCE.get(this.env.composebody);
      editor.getWin().focus(); // correct focus in IE & Chrome
      text = editor.selection.getContent({ format:'text' });
      if (!text) {
        text = editor.getContent({ format:'text' });
        strip = true;
      }
    }
    // get selected text from compose textarea
    else {
      var textarea = rcube_find_object(this.env.composebody), sigstart;
      if (textarea && $(textarea).is(':focus')) {
        text = this.get_input_selection(textarea).text;
      }
      if (!text && textarea) {
        text = textarea.value;
        strip = true;
      }
    }
    // strip off signature
    if (strip) {
      sigstart = text.indexOf('-- \n');
      if (sigstart > 0) {
        text = text.substring(0, sigstart);
      }
    }
    // show dialog to enter a name and to modify the text to be saved
    var buttons = {},
      html = '<form class="propform">' +
      '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' +
      '<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
      '<div class="prop block"><label>' + this.get_label('responsetext') + '</label>' +
      '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
      '</form>';
    buttons[this.gettext('save')] = function(e) {
      var name = $('#ffresponsename').val(),
        text = $('#ffresponsetext').val();
      if (!text) {
        $('#ffresponsetext').select();
        return false;
      }
      if (!name)
        name = text.substring(0,40);
      var lock = ref.display_message(ref.get_label('savingresponse'), 'loading');
      ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
      $(this).dialog('close');
    };
    buttons[this.gettext('cancel')] = function() {
      $(this).dialog('close');
    };
    this.show_popup_dialog(html, this.gettext('savenewresponse'), buttons);
    $('#ffresponsetext').val(text);
    $('#ffresponsename').select();
  };
  this.add_response_item = function(response)
  {
    var key = response.key;
    this.env.textresponses[key] = response;
    // append to responses list
    if (this.gui_objects.responseslist) {
      var li = $('<li>').appendTo(this.gui_objects.responseslist);
      $('<a>').addClass('insertresponse active')
        .attr('href', '#')
        .attr('rel', key)
        .html(this.quote_html(response.name))
        .appendTo(li)
        .mousedown(function(e){
          return rcube_event.cancel(e);
        })
        .mouseup(function(e){
          ref.command('insert-response', key);
          $(document.body).trigger('mouseup');  // hides the menu
          return rcube_event.cancel(e);
        });
    }
  };
  this.edit_responses = function()
  {
    // TODO: implement inline editing of responses
  };
  this.delete_response = function(key)
  {
    if (!key && this.responses_list) {
      var selection = this.responses_list.get_selection();
      key = selection[0];
    }
    // 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()
@@ -4060,7 +4278,7 @@
    if (this.preview_timer)
      clearTimeout(this.preview_timer);
    var n, id, sid, ref = this, writable = false,
    var n, id, sid, contact, ref = this, writable = false,
      source = this.env.source ? this.env.address_sources[this.env.source] : null;
    // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
@@ -4070,33 +4288,39 @@
      this.show_contentframe(false);
    if (list.selection.length) {
      list.draggable = false;
      // no source = search result, we'll need to detect if any of
      // selected contacts are in writable addressbook to enable edit/delete
      // we'll also need to know sources used in selection for copy
      // and group-addmember operations (drag&drop)
      this.env.selection_sources = [];
      if (!source) {
        for (n in list.selection) {
      if (source)
        this.env.selection_sources.push(this.env.source);
      for (n in list.selection) {
        contact = list.data[list.selection[n]];
        if (!source) {
          sid = String(list.selection[n]).replace(/^[^-]+-/, '');
          if (sid && this.env.address_sources[sid]) {
            writable = writable || !this.env.address_sources[sid].readonly;
            writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
            this.env.selection_sources.push(sid);
          }
        }
        this.env.selection_sources = $.unique(this.env.selection_sources);
        else
          writable = writable || (!source.readonly && !contact.readonly);
      }
      else {
        this.env.selection_sources.push(this.env.source);
        writable = !source.readonly;
      }
      this.env.selection_sources = $.unique(this.env.selection_sources);
    }
    // if a group is currently selected, and there is at least one contact selected
    // thend we can enable the group-remove-selected command
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
    this.enable_command('compose', this.env.group || list.selection.length > 0);
    this.enable_command('edit', id && writable);
    this.enable_command('delete', list.selection.length && writable);
    this.enable_command('delete', list.selection.length > 0 && writable);
    return false;
  };
@@ -4124,10 +4348,28 @@
    else if (!this.env.search_request)
      folder = group ? 'G'+src+group : src;
    this.select_folder(folder, '', true);
    this.env.source = src;
    this.env.group = group;
    // truncate groups listing stack
    var index = $.inArray(this.env.group, this.env.address_group_stack);
    if (index < 0)
      this.env.address_group_stack = [];
    else
      this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
    // make sure the current group is on top of the stack
    if (this.env.group) {
      this.env.address_group_stack.push(this.env.group);
      // mark the first group on the stack as selected in the directory list
      folder = 'G'+src+this.env.address_group_stack[0];
    }
    else if (this.gui_objects.addresslist_title) {
      $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
    }
    this.select_folder(folder, '', true);
    // load contacts remotely
    if (this.gui_objects.contactslist) {
@@ -4183,16 +4425,38 @@
  this.list_contacts_clear = function()
  {
    this.contact_list.data = {};
    this.contact_list.clear(true);
    this.show_contentframe(false);
    this.enable_command('delete', false);
    this.enable_command('compose', this.env.group ? true : false);
  };
  this.set_group_prop = function(prop)
  {
    if (this.gui_objects.addresslist_title) {
      var boxtitle = $(this.gui_objects.addresslist_title).html('');  // clear contents
      // add link to pop back to parent group
      if (this.env.address_group_stack.length > 1) {
        $('<a href="#list">...</a>')
          .addClass('poplink')
          .appendTo(boxtitle)
          .click(function(e){ return ref.command('popgroup','',this); });
        boxtitle.append('&nbsp;&raquo;&nbsp;');
      }
      boxtitle.append($('<span>'+prop.name+'</span>'));
    }
    this.triggerEvent('groupupdate', prop);
  };
  // load contact record
  this.load_contact = function(cid, action, framed)
  {
    var win, url = {}, target = window;
    var win, url = {}, target = window,
      rec = this.contact_list ? this.contact_list.data[cid] : null;
    if (win = this.get_frame_window(this.env.contentframe)) {
      url._framed = 1;
@@ -4203,7 +4467,9 @@
      if (!cid) {
        // unselect selected row(s)
        this.contact_list.clear_selection();
        this.enable_command('delete', 'compose', false);
        this.enable_command('compose', rec && rec.email);
        this.enable_command('delete', rec && rec._type != 'group');
      }
    }
    else if (framed)
@@ -4314,7 +4580,7 @@
  };
  // update a contact record in the list
  this.update_contact_row = function(cid, cols_arr, newcid, source)
  this.update_contact_row = function(cid, cols_arr, newcid, source, data)
  {
    var c, row, list = this.contact_list;
@@ -4341,11 +4607,13 @@
        list.selection[0] = newcid;
        row.style.display = '';
      }
      list.data[cid] = data;
    }
  };
  // add row to contacts list
  this.add_contact_row = function(cid, cols, classes)
  this.add_contact_row = function(cid, cols, classes, data)
  {
    if (!this.gui_objects.contactslist)
      return false;
@@ -4368,6 +4636,8 @@
      row.appendChild(col);
    }
    // store data in list member
    list.data[cid] = data;
    list.insert_row(row);
    this.enable_command('export', list.rowcount > 0);
@@ -4777,6 +5047,9 @@
          if (++colprop.count == colprop.limit && colprop.limit)
            $(menu).children('option[value="'+col+'"]').prop('disabled', true);
        }
        if (contact._type != 'group')
          list.draggable = true;
      }
    }
  };
@@ -5051,6 +5324,42 @@
      col = $('<td>').addClass('mail').html(name).appendTo(row);
      list.insert_row(row);
      list.select(rid);
    }
  };
  this.update_response_row = function(response, oldkey)
  {
    var row, col, list = this.responses_list;
    if (list && oldkey && list.rows[oldkey] && (row = list.rows[oldkey].obj)) {
      $(row.cells[0]).html(response.name);
      // update references because the key likely changed
      row.id = 'rcmrow'+response.key;
      list.init_row(row);
      list.select(response.key);
      delete list.rows[oldkey];
    }
    else if (list) {
      row = $('<tr>').attr('id', 'rcmrow'+response.key).get(0);
      col = $('<td>').addClass('name').html(response.name).appendTo(row);
      list.insert_row(row);
      list.select(response.key);
    }
  };
  this.remove_response = function(key)
  {
    var frame;
    if (this.env.textresponses) {
      delete this.env.textresponses[key];
    }
    if (this.responses_list) {
      this.responses_list.remove_row(key);
      if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
        frame.location.href = this.env.blankpage;
      }
    }
  };
@@ -5732,7 +6041,7 @@
  };
  // open a jquery UI dialog with the given content
  this.show_popup_dialog = function(html, title)
  this.show_popup_dialog = function(html, title, buttons)
  {
    // forward call to parent window
    if (this.is_framed()) {
@@ -5744,6 +6053,7 @@
      .html(html)
      .dialog({
        title: title,
        buttons: buttons,
        modal: true,
        resizable: true,
        width: 580,
@@ -5753,7 +6063,7 @@
      // resize and center popup
      var win = $(window), w = win.width(), h = win.height(),
        width = popup.width(), height = popup.height();
      popup.dialog('option', { height: Math.min(h-40, height+50), width: Math.min(w-20, width+50) })
      popup.dialog('option', { height: Math.min(h-40, height+75 + (buttons ? 50 : 0)), width: Math.min(w-20, width+50) })
        .dialog('option', 'position', ['center', 'center']);  // only works in a separate call (!?)
  };
@@ -6594,6 +6904,14 @@
  /*********            helper methods            *********/
  /********************************************************/
  /**
   * Quote html entities
   */
  this.quote_html = function(str)
  {
    return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  };
  // get window.opener.rcmail if available
  this.opener = function()
  {
@@ -6658,6 +6976,57 @@
    }
  };
  // get selected text from an input field
  // http://stackoverflow.com/questions/7186586/how-to-get-the-selected-text-in-textarea-using-jquery-in-internet-explorer-7
  this.get_input_selection = function(obj)
  {
    var start = 0, end = 0,
      normalizedValue, range,
      textInputRange, len, endRange;
    if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
      normalizedValue = obj.value;
      start = obj.selectionStart;
      end = obj.selectionEnd;
    }
    else {
      range = document.selection.createRange();
      if (range && range.parentElement() == obj) {
        len = obj.value.length;
        normalizedValue = obj.value; //.replace(/\r\n/g, "\n");
        // create a working TextRange that lives only in the input
        textInputRange = obj.createTextRange();
        textInputRange.moveToBookmark(range.getBookmark());
        // Check if the start and end of the selection are at the very end
        // of the input, since moveStart/moveEnd doesn't return what we want
        // in those cases
        endRange = obj.createTextRange();
        endRange.collapse(false);
        if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
          start = end = len;
        }
        else {
          start = -textInputRange.moveStart("character", -len);
          start += normalizedValue.slice(0, start).split("\n").length - 1;
          if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
            end = len;
          }
          else {
            end = -textInputRange.moveEnd("character", -len);
            end += normalizedValue.slice(0, end).split("\n").length - 1;
          }
        }
      }
    }
    return { start:start, end:end, text:normalizedValue.substr(start, end-start) };
  };
  // disable/enable all fields of a form
  this.lock_form = function(form, lock)
  {