diff options
-rw-r--r-- | CHANGELOG | 1 | ||||
-rw-r--r-- | config/main.inc.php.dist | 9 | ||||
-rw-r--r-- | program/include/main.inc | 26 | ||||
-rw-r--r-- | program/js/app.js | 167 | ||||
-rw-r--r-- | program/steps/mail/autocomplete.inc | 21 | ||||
-rw-r--r-- | program/steps/mail/compose.inc | 8 |
6 files changed, 175 insertions, 57 deletions
@@ -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 diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 4a0a9296e..db4ab92f7 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -571,6 +571,15 @@ $rcmail_config['autocomplete_addressbooks'] = array('sql'); // 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}'; diff --git a/program/include/main.inc b/program/include/main.inc index 672e00dbd..d1d9c781d 100644 --- a/program/include/main.inc +++ b/program/include/main.inc @@ -2122,3 +2122,29 @@ function rcube_upload_progress_init() } } } + +/** + * 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'); +} diff --git a/program/js/app.js b/program/js/app.js index 35136bf1a..0ec920b3a 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -2871,12 +2871,21 @@ function rcube_webmail() 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 @@ function rcube_webmail() 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 @@ function rcube_webmail() /*********************************************************/ // 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 @@ function rcube_webmail() 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 @@ function rcube_webmail() } // 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,11 +3531,12 @@ function rcube_webmail() 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) { insert += this.env.contacts[id].name + ', '; @@ -3560,7 +3570,7 @@ function rcube_webmail() }; // 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 @@ function rcube_webmail() 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; - - // 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]; - } + 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]; + } + + ul = this.ksearch_pane.__ul; - // remove all search results - 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, '<').replace(/>/g, '>').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; } + } + if (ul.childNodes.length) { + this.ksearch_pane.show(); // select the first - $(ul.firstChild).attr('id', 'rcmksearchSelected').addClass('selected'); - this.ksearch_selected = 0; + if (!this.env.contacts.length) { + $('li:first', ul).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(); + 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); + } } - // hide results pane - else - this.ksearch_hide(); }; this.ksearch_click = function(node) @@ -3674,20 +3727,34 @@ function rcube_webmail() 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 @@ function rcube_webmail() 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 @@ function rcube_webmail() /********************************************************/ /********* remote request methods *********/ /********************************************************/ - + // compose a valid url with the given parameters this.url = function(action, query) { @@ -5534,7 +5601,8 @@ function rcube_webmail() // 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 @@ function rcube_webmail() // 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); } diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc index 395c0e12b..2968f0335 100644 --- a/program/steps/mail/autocomplete.inc +++ b/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 @@ if ($RCMAIL->action == 'group-expand') { $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 @@ else if ($book_types && ($search = get_input_value('_search', RCUBE_INPUT_GPC, t usort($contacts, 'contact_results_sort'); } -$OUTPUT->command('ksearch_query_results', $contacts, $search); +$OUTPUT->command('ksearch_query_results', $contacts, $search, $sid); $OUTPUT->send(); diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 899702730..d726d8c86 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -111,7 +111,7 @@ if (!is_array($_SESSION['compose'])) $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 @@ if (!empty($CONFIG['drafts_mbox'])) { $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 @@ function rcmail_compose_headers($attrib) $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; } |