summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralecpl <alec@alec.pl>2011-07-25 10:49:39 +0000
committeralecpl <alec@alec.pl>2011-07-25 10:49:39 +0000
commit0213f8d4dddf88b1a3eec91ff0f95832ebac3008 (patch)
treed221b163d8097347d666edfc5ef393e6020dda3b
parent18371736346a2f71f37f68e6fd13de8b230a8baf (diff)
- Added optional "multithreading" autocomplete feature
-rw-r--r--CHANGELOG1
-rw-r--r--config/main.inc.php.dist9
-rw-r--r--program/include/main.inc26
-rw-r--r--program/js/app.js167
-rw-r--r--program/steps/mail/autocomplete.inc21
-rw-r--r--program/steps/mail/compose.inc8
6 files changed, 175 insertions, 57 deletions
diff --git a/CHANGELOG b/CHANGELOG
index e8b6903e5..45d93053e 100644
--- a/CHANGELOG
+++ b/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
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, '&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;
}
+ }
+ 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;
}