alecpl
2011-07-25 0213f8d4dddf88b1a3eec91ff0f95832ebac3008
- Added optional "multithreading" autocomplete feature


6 files modified
236 ■■■■ changed files
CHANGELOG 1 ●●●● patch | view | raw | blame | history
config/main.inc.php.dist 9 ●●●●● patch | view | raw | blame | history
program/include/main.inc 26 ●●●●● patch | view | raw | blame | history
program/js/app.js 171 ●●●● patch | view | raw | blame | history
program/steps/mail/autocomplete.inc 21 ●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 8 ●●●●● patch | view | raw | blame | history
CHANGELOG
@@ -1,6 +1,7 @@
CHANGELOG Roundcube Webmail
===========================
- Added optional "multithreading" autocomplete feature
- Plugin API: Added 'config_get' hook
- Fixed new_user_identity plugin to work with updated rcube_ldap class (#1487994)
- Plugin API: added folder_delete and folder_rename hooks
config/main.inc.php.dist
@@ -571,6 +571,15 @@
// may need to do lengthy results building given overly-broad searches
$rcmail_config['autocomplete_min_length'] = 1;
// Number of parallel autocomplete requests.
// If there's more than one address book, n parallel (async) requests will be created,
// where each request will search in one address book. By default (0), all address
// books are searched in one request.
$rcmail_config['autocomplete_threads'] = 0;
// Max. numer of entries in autocomplete popup. Default: 15.
$rcmail_config['autocomplete_max'] = 15;
// show address fields in this order
// available placeholders: {street}, {locality}, {zipcode}, {country}, {region}
$rcmail_config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}';
program/include/main.inc
@@ -2122,3 +2122,29 @@
        }
    }
}
/**
 * Initializes client-side autocompletion
 */
function rcube_autocomplete_init()
{
    global $RCMAIL;
    static $init;
    if ($init)
        return;
    $init = 1;
    if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
      $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
      if (count($book_types) > 1) {
        $RCMAIL->output->set_env('autocomplete_threads', $threads);
        $RCMAIL->output->set_env('autocomplete_sources', $book_types);
      }
    }
    $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
    $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
    $RCMAIL->output->add_label('autocompletechars');
}
program/js/app.js
@@ -2871,12 +2871,21 @@
      input_subject = $("input[name='_subject']"),
      input_message = $("[name='_message']").get(0),
      html_mode = $("input[name='_is_html']").val() == '1',
      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'];
      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
      ac_props;
    // configure parallel autocompletion
    if (this.env.autocomplete_threads > 0) {
      ac_props = {
        threads: this.env.autocomplete_threads,
        sources: this.env.autocomplete_sources,
      };
    }
    // init live search events
    this.init_address_input_events(input_to);
    this.init_address_input_events(input_to, ac_props);
    for (var i in ac_fields) {
      this.init_address_input_events($("[name='_"+ac_fields[i]+"']"));
      this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
    }
    if (!html_mode) {
@@ -2904,9 +2913,9 @@
    this.auto_save_start();
  };
  this.init_address_input_events = function(obj, action)
  this.init_address_input_events = function(obj, props)
  {
    obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e) { return ref.ksearch_keydown(e, this, action); })
    obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e) { return ref.ksearch_keydown(e, this, props); })
      .attr('autocomplete', 'off');
  };
@@ -3441,7 +3450,7 @@
  /*********************************************************/
  // handler for keyboard events on address-fields
  this.ksearch_keydown = function(e, obj, action)
  this.ksearch_keydown = function(e, obj, props)
  {
    if (this.ksearch_timer)
      clearTimeout(this.ksearch_timer);
@@ -3471,8 +3480,8 @@
        if (mod == SHIFT_KEY)
          break;
     case 13:  // enter
        if (this.ksearch_selected === null || !this.ksearch_input || !this.ksearch_value)
      case 13:  // enter
        if (this.ksearch_selected === null || !this.ksearch_value)
          break;
        // insert selected address and hide ksearch pane
@@ -3492,7 +3501,7 @@
    }
    // start timer
    this.ksearch_timer = window.setTimeout(function(){ ref.ksearch_get_results(action); }, 200);
    this.ksearch_timer = window.setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
    this.ksearch_input = obj;
    return true;
@@ -3522,10 +3531,11 @@
      p = inp_value.lastIndexOf(this.ksearch_value, cpos),
      trigger = false,
      insert = '',
      // replace search string with full address
      pre = inp_value.substring(0, p),
      end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
    this.ksearch_destroy();
    // insert all members of a group
    if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) {
@@ -3560,7 +3570,7 @@
  };
  // address search processor
  this.ksearch_get_results = function(action)
  this.ksearch_get_results = function(props)
  {
    var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
@@ -3605,59 +3615,102 @@
    if (old_value && old_value.length && this.env.contacts && !this.env.contacts.length && q.indexOf(old_value) == 0)
      return;
    var lock = this.display_message(this.get_label('searching'), 'loading');
    this.http_post(action ? action : 'mail/autocomplete', '_search='+urlencode(q), lock);
    this.ksearch_destroy();
    var i, lock, source, xhr, reqid = new Date().getTime(),
      threads = props && props.threads ? props.threads : 1,
      sources = props && props.sources ? props.sources : [],
      action = props && props.action ? props.action : 'mail/autocomplete';
    this.ksearch_data = {id: reqid, sources: sources.slice(), action: action, locks: [], requests: []};
    for (i=0; i<threads; i++) {
      source = this.ksearch_data.sources.shift();
      if (threads > 1 && source === null)
        break;
      lock = this.display_message(this.get_label('searching'), 'loading');
      xhr = this.http_post(action, '_search='+urlencode(q)+'&_id='+reqid
        + (source ? '&_source='+urlencode(source) : ''), lock);
      this.ksearch_data.locks.push(lock);
      this.ksearch_data.requests.push(xhr);
    }
  };
  this.ksearch_query_results = function(results, search)
  this.ksearch_query_results = function(results, search, reqid)
  {
    // ignore this outdated search response
    if (this.ksearch_value && search != this.ksearch_value)
    if (this.ksearch_input && this.ksearch_value && search != this.ksearch_value)
      return;
    this.env.contacts = results ? results : [];
    this.ksearch_display_results(this.env.contacts);
  };
  this.ksearch_display_results = function (a_results)
  {
    // display search results
    if (a_results.length && this.ksearch_input && this.ksearch_value) {
      var p, ul, li, text, s_val = this.ksearch_value;
    var p, ul, li, text, init, s_val = this.ksearch_value,
      maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
      // create results pane if not present
      if (!this.ksearch_pane) {
        ul = $('<ul>');
        this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
        this.ksearch_pane.__ul = ul[0];
      }
    // create results pane if not present
    if (!this.ksearch_pane) {
      ul = $('<ul>');
      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane')
        .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
      this.ksearch_pane.__ul = ul[0];
    }
      // remove all search results
      ul = this.ksearch_pane.__ul;
    ul = this.ksearch_pane.__ul;
    // remove all search results or add to existing list if parallel search
    if (reqid && this.ksearch_pane.data('reqid') == reqid) {
      maxlen -= ul.childNodes.length;
    }
    else {
      this.ksearch_pane.data('reqid', reqid);
      init = 1;
      // reset content
      ul.innerHTML = '';
      this.env.contacts = [];
      // move the results pane right under the input box
      var pos = $(this.ksearch_input).offset();
      this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
    }
      // add each result line to list
      for (i=0; i < a_results.length; i++) {
        text = typeof a_results[i] === 'object' ? a_results[i].name : a_results[i];
    // add each result line to list
    if (results && results.length) {
      for (i=0; i < results.length && maxlen > 0; i++) {
        text = typeof results[i] === 'object' ? results[i].name : results[i];
        li = document.createElement('LI');
        li.innerHTML = text.replace(new RegExp('('+RegExp.escape(s_val)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
        li.onmouseover = function(){ ref.ksearch_select(this); };
        li.onmouseup = function(){ ref.ksearch_click(this) };
        li._rcm_id = i;
        ul.appendChild(li);
        maxlen -= 1;
      }
      // select the first
      $(ul.firstChild).attr('id', 'rcmksearchSelected').addClass('selected');
      this.ksearch_selected = 0;
      // move the results pane right under the input box and make it visible
      var pos = $(this.ksearch_input).offset();
      this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px' }).show();
    }
    // hide results pane
    else
      this.ksearch_hide();
    if (ul.childNodes.length) {
      this.ksearch_pane.show();
      // select the first
      if (!this.env.contacts.length) {
        $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected');
        this.ksearch_selected = 0;
      }
    }
    if (results && results.length)
      this.env.contacts = this.env.contacts.concat(results);
    // run next parallel search
    if (maxlen > 0 && this.ksearch_data.id == reqid && this.ksearch_data.sources.length) {
      var lock, xhr, props = this.ksearch_data, source = props.sources.shift();
      if (source) {
        lock = this.display_message(this.get_label('searching'), 'loading');
        xhr = this.http_post(props.action, '_search='+urlencode(s_val)+'&_id='+reqid
          +'&_source='+urlencode(source), lock);
        this.ksearch_data.locks.push(lock);
        this.ksearch_data.requests.push(xhr);
      }
    }
  };
  this.ksearch_click = function(node)
@@ -3674,20 +3727,34 @@
    if (this.ksearch_timer)
      clearTimeout(this.ksearch_timer);
    this.ksearch_value = '';
    this.ksearch_input = null;
    this.ksearch_hide();
  };
  this.ksearch_hide = function()
  {
    this.ksearch_selected = null;
    this.ksearch_value = '';
    if (this.ksearch_pane)
      this.ksearch_pane.hide();
   };
  // Aborts pending autocomplete requests
  this.ksearch_destroy = function()
  {
    var i, len, ac = this.ksearch_data;
    if (!ac)
      return;
    for (i=0, len=ac.locks.length; i<len; i++) {
      this.hide_message(ac.locks[i]); // hide loading message
      ac.requests[i].abort(); // abort ajax request
    }
    this.ksearch_data = null;
  }
  /*********************************************************/
  /*********         address book methods          *********/
@@ -4533,7 +4600,7 @@
      reg = new RegExp('[^'+delim+']*['+delim+']', 'g');
      var basename = this.env.mailbox.replace(reg, ''),
        newname = this.env.dstfolder==this.env.delimiter ? basename : this.env.dstfolder+this.env.delimiter+basename;
      if (newname != this.env.mailbox) {
        this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.mailbox)+'&_folder_newname='+urlencode(newname), this.set_busy(true, 'foldermoving'));
        this.subscription_list.draglayer.hide();
@@ -5455,7 +5522,7 @@
  /********************************************************/
  /*********        remote request methods        *********/
  /********************************************************/
  // compose a valid url with the given parameters
  this.url = function(action, query)
  {
@@ -5534,7 +5601,8 @@
    // send request
    console.log('HTTP GET: ' + url);
    $.ajax({
    return $.ajax({
      type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
@@ -5565,7 +5633,8 @@
    // send request
    console.log('HTTP POST: ' + url);
    $.ajax({
    return $.ajax({
      type: 'POST', url: url, data: postdata, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
program/steps/mail/autocomplete.inc
@@ -19,9 +19,6 @@
*/
$MAXNUM = 15;
$book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
if ($RCMAIL->action == 'group-expand') {
  $abook = $RCMAIL->get_address_book(get_input_value('_source', RCUBE_INPUT_GPC));
  if ($gid = get_input_value('_gid', RCUBE_INPUT_GPC)) {
@@ -36,8 +33,22 @@
    $OUTPUT->command('replace_group_recipients', $gid, join(', ', $members));
  }
  $OUTPUT->send();
}
else if ($book_types && ($search = get_input_value('_search', RCUBE_INPUT_GPC, true))) {
$MAXNUM = (int)$RCMAIL->config->get('autocomplete_max', 15);
$search = get_input_value('_search', RCUBE_INPUT_GPC, true);
$source = get_input_value('_source', RCUBE_INPUT_GPC);
$sid    = get_input_value('_id', RCUBE_INPUT_GPC);
if (strlen($source))
  $book_types = array($source);
else
  $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
if (!empty($book_types) && strlen($search)) {
  $contacts = array();
  $books_num = count($book_types);
@@ -87,7 +98,7 @@
  usort($contacts, 'contact_results_sort');
}
$OUTPUT->command('ksearch_query_results', $contacts, $search);
$OUTPUT->command('ksearch_query_results', $contacts, $search, $sid);
$OUTPUT->send();
program/steps/mail/compose.inc
@@ -111,7 +111,7 @@
$OUTPUT->add_label('nosubject', 'nosenderwarning', 'norecipientwarning', 'nosubjectwarning', 'cancel',
    'nobodywarning', 'notsentwarning', 'notuploadedwarning', 'savingmessage', 'sendingmessage', 
    'messagesaved', 'converting', 'editorwarning', 'searching', 'uploading', 'uploadingmany',
    'fileuploaderror', 'autocompletechars');
    'fileuploaderror');
$OUTPUT->set_env('compose_id', $COMPOSE_ID);
@@ -124,7 +124,6 @@
$OUTPUT->set_env('mailbox', $IMAP->get_mailbox_name());
$OUTPUT->set_env('sig_above', $CONFIG['sig_above']);
$OUTPUT->set_env('top_posting', $CONFIG['top_posting']);
$OUTPUT->set_env('autocomplete_min_length', $CONFIG['autocomplete_min_length']);
// get reference message and set compose mode
if ($msg_uid = $_SESSION['compose']['param']['draft_uid']) {
@@ -466,10 +465,13 @@
    $input = new $field_type($field_attrib);
    $out = $input->show($MESSAGE->compose[$param]);
  }
  if ($form_start)
    $out = $form_start.$out;
  // configure autocompletion
  rcube_autocomplete_init();
  return $out;
}