diff --git a/config/defaults.inc.php b/config/defaults.inc.php
index 8a6ecec47..a1a784bc5 100644
--- a/config/defaults.inc.php
+++ b/config/defaults.inc.php
@@ -1290,10 +1290,11 @@ $config['addressbook_search_mode'] = 0;
// Warning: These are field names not LDAP attributes (see 'fieldmap' setting)!
$config['contactlist_fields'] = ['name', 'firstname', 'surname', 'email'];
-// Template of contact entry on the autocompletion list.
-// You can use contact fields as: name, email, organization, department, etc.
-// See program/actions/contacts/index.php for a list
-$config['contact_search_name'] = '{name} <{email}>';
+// Template of contact entry on contacts and autocompletion list.
+// You can use any field listed in contactlist_fields.
+// Example: '{name} ({organization})'
+// Default: '{name}'.
+$config['contactlist_name_template'] = '{name}';
// Contact mode. If your contacts are mostly business, switch it to 'business'.
// This will prioritize form fields related to 'work' (instead of 'home').
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 8d4729f63..75718e70c 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -101,10 +101,10 @@ class acl extends rcube_plugin
}
if ($user) {
- $display = rcube_addressbook::compose_search_name($record);
- $user = ['name' => $user, 'display' => $display];
+ $fields = rcube_addressbook::compose_search_fields($record);
+ $user = ['name' => $user, 'fields' => $fields];
$users[] = $user;
- $keys[] = $display ?: $user['name'];
+ $keys[] = $fields['name'] ?? $user['name'];
}
}
@@ -118,7 +118,7 @@ class acl extends rcube_plugin
$group_id = is_array($record[$group_field]) ? $record[$group_field][0] : $record[$group_field];
if ($group) {
- $users[] = ['name' => ($prefix ?: '') . $group_id, 'display' => $group, 'type' => 'group'];
+ $users[] = ['name' => ($prefix ?: '') . $group_id, 'fields' => ['name' => $group], 'type' => 'group'];
$keys[] = $group;
}
}
diff --git a/program/actions/mail/autocomplete.php b/program/actions/mail/autocomplete.php
index ed70e6978..184b220da 100644
--- a/program/actions/mail/autocomplete.php
+++ b/program/actions/mail/autocomplete.php
@@ -83,18 +83,14 @@ class rcmail_action_mail_autocomplete extends rcmail_action
'source' => $abook_id,
];
- $display = rcube_addressbook::compose_search_name($record, $email, $name);
-
- if ($display && $display != $contact['name']) {
- $contact['display'] = $display;
- }
+ $contact['fields'] = rcube_addressbook::compose_search_fields($record, $email, $name);
// groups with defined email address will not be expanded to its members' addresses
if ($contact['type'] == 'group') {
$contact['email'] = $email;
}
- $name = !empty($contact['display']) ? $contact['display'] : $name;
+ $name = !empty($contact['fields']['name']) ? $contact['fields']['name'] : $name;
$contacts[$index] = $contact;
$sort_keys[$index] = sprintf('%s %03d', $name, $idx++);
@@ -130,6 +126,7 @@ class rcmail_action_mail_autocomplete extends rcmail_action
$contacts[$index] = [
'name' => $index,
'email' => $email,
+ 'fields' => ['name' => $index, 'email' => $email],
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
@@ -147,6 +144,7 @@ class rcmail_action_mail_autocomplete extends rcmail_action
$sort_keys[$group['name']] = $group['name'];
$contacts[$group['name']] = [
'name' => $group['name'] . ' (' . intval($result->count) . ')',
+ 'fields' => ['name' => $group['name'] . ' (' . intval($result->count) . ')'],
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
diff --git a/program/js/app.js b/program/js/app.js
index 018335249..4affe0d98 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -6345,10 +6345,11 @@ function rcube_webmail() {
if (results && (len = results.length)) {
for (i = 0; i < len && maxlen > 0; i++) {
text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
+ fields = typeof results[i] === 'object' && results[i].fields ? results[i].fields : { name: text };
type = typeof results[i] === 'object' ? results[i].type : '';
id = i + this.env.contacts.length;
$('
').attr({ id: 'rcmkSearchItem' + id, role: 'option' })
- .html('' + this.quote_html(text.replace(new RegExp('(' + RegExp.escape(value) + ')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '$1'))
+ .html(this.ksearch_results_display(fields, value))
.addClass(type || '')
.appendTo(ul)
.mouseover(function () {
@@ -6384,6 +6385,24 @@ function rcube_webmail() {
}
};
+ this.ksearch_results_display = function (fields, search_term) {
+ line = "{name} <{email}>";
+
+ $.each(fields, function (key, data) {
+ line = line.replace('{' + key + '}', data ? ref.ksearch_results_highlight(data, search_term) : '');
+ });
+ line = line.replace(/\{[a-z]+\}/ug, '');
+ line = line.replace(/\s*<>/ug, '');
+ line = line.replace(/\s+/ug, ' ');
+ line = line.trim();
+
+ return line;
+ };
+
+ this.ksearch_results_highlight = function (haystack, needle) {
+ return this.quote_html(haystack.replace(new RegExp('(' + RegExp.escape(needle) + ')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '$1');
+ };
+
// Getter for input value
// returns a string from the last comma to current cursor position
this.ksearch_input_get = function () {
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 3afc9537e..f583a6ccb 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/Roundcube/rcube_addressbook.php
@@ -679,7 +679,7 @@ abstract class rcube_addressbook
*/
public static function compose_list_name($contact)
{
- static $compose_mode;
+ static $compose_mode, $template;
if (!isset($compose_mode)) {
$compose_mode = (int) rcube::get_instance()->config->get('addressbook_name_listing', 0);
@@ -745,6 +745,16 @@ abstract class rcube_addressbook
}
}
+ if ($fn !== '') {
+ if (!isset($template)) { // cache this
+ $template = rcube::get_instance()->config->get('contactlist_name_template', '{name}');
+ }
+
+ if ($template !== '{name}') {
+ $fn = self::compose_search_name($contact, null, $fn, $template);
+ }
+ }
+
return $fn;
}
@@ -754,64 +764,76 @@ abstract class rcube_addressbook
* @param array $contact Hash array with contact data as key-value pairs
* @param string $email Optional email address
* @param string $name Optional name (self::compose_list_name() result)
- * @param string $templ Optional template to use (defaults to the 'contact_search_name' config option)
+ * @param string $templ Optional template to use (defaults to '{name} <{email}>')
*
* @return string Display name
*/
- public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
+ public static function compose_search_name($contact, $email = null, $name = null, $templ = '{name} <{email}>')
{
- static $template;
-
- if (empty($templ) && !isset($template)) { // cache this
- $template = rcube::get_instance()->config->get('contact_search_name');
- if (empty($template)) {
- $template = '{name} <{email}>';
+ if (preg_match_all('/\{([a-z]+)\}/', $templ, $matches)) {
+ $values = self::compose_search_fields($contact, $email, $name, $matches[1]);
+ foreach ($values as $key => $value) {
+ $templ = str_replace('{' . $key . '}', $value, $templ);
}
}
- $result = $templ ?: $template;
+ $templ = preg_replace('/\s+/u', ' ', $templ);
+ $templ = preg_replace('/\s*(<>|\(\)|\[\])/u', '', $templ);
+ $templ = trim($templ, '/ ');
- if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
- foreach ($matches[0] as $key) {
- $key = trim($key, '{}');
- $value = '';
+ return $templ;
+ }
- switch ($key) {
- case 'name':
- $value = $name ?: self::compose_list_name($contact);
+ /**
+ * Build contact display name for search result listing
+ *
+ * @param array $contact Hash array with contact data as key-value pairs
+ * @param string $email Optional email address
+ * @param string $name Optional name (self::compose_list_name() result)
+ * @param array $fields Optional fields to return (defaults to ['name', 'email'])
+ *
+ * @return array Fields
+ */
+ public static function compose_search_fields($contact, $email = null, $name = null, $fields = ['name', 'email'])
+ {
+ $result = [];
- // If name(s) are undefined compose_list_name() may return an email address
- // here we prevent from returning the same name and email
- if ($name === $email && str_contains($result, '{email}')) {
- $value = '';
- }
+ foreach ($fields as $key) {
+ $value = '';
- break;
- case 'email':
- $value = $email;
- break;
- }
+ switch ($key) {
+ case 'name':
+ $value = $name ?: self::compose_list_name($contact);
- if (empty($value)) {
- $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
- if (is_array($value) && isset($value[0])) {
- $value = $value[0];
+ // If name(s) are undefined compose_list_name() may return an email address
+ // here we prevent from returning the same name and email
+ if ($name === $email && in_array('email', $fields) !== false) {
+ $value = '';
}
- }
- if (!is_string($value)) {
- $value = '';
- }
-
- $result = str_replace('{' . $key . '}', $value, $result);
+ break;
+ case 'email':
+ $value = $email;
+ break;
}
+
+ if (empty($value)) {
+ $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
+ if (is_array($value) && isset($value[0])) {
+ $value = $value[0];
+ }
+ }
+
+ if (!is_string($value)) {
+ $value = '';
+ }
+
+ $result[$key] = $value;
}
- $result = preg_replace('/\s+/u', ' ', $result);
- $result = preg_replace('/\s*(<>|\(\)|\[\])/u', '', $result);
- $result = trim($result, '/ ');
+ $plugin = rcube::get_instance()->plugins->exec_hook('compose_search_fields', ['contact' => $contact, 'email' => $email, 'name' => $name, 'fields' => $result]);
- return $result;
+ return $plugin['fields'];
}
/**
diff --git a/skins/elastic/styles/widgets/lists.less b/skins/elastic/styles/widgets/lists.less
index b0e35ddf3..0e016df6d 100644
--- a/skins/elastic/styles/widgets/lists.less
+++ b/skins/elastic/styles/widgets/lists.less
@@ -335,12 +335,35 @@ html.touch {
&:extend(.font-icon-class);
content: @fa-var-user;
margin-left: .5rem;
+ line-height: normal;
}
&.group > i:before {
content: @fa-var-users;
}
}
+#rcmKSearchpane > ul > li {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: .5em 0;
+
+ & > span.fields {
+ display: flex;
+ flex-flow: row wrap;
+ width: auto;
+ overflow: hidden;
+ margin: 0 .25em;
+
+ & > span.field {
+ flex: 0 0 100%;
+ font-size: 1rem;
+ line-height: normal;
+ .overflow-ellipsis();
+ }
+ }
+}
+
html.ie11 .listing.iconized li a:before {
font-size: 1.25rem;
}
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index 4eee44793..0e4932ac6 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -4450,6 +4450,21 @@ if (window.rcmail) {
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', { name: name, props: { menu: name }, originalEvent: event });
};
+
+ /**
+ * Elastic version of ksearch_results_display with small screen support
+ */
+ rcmail.ksearch_results_display = function (fields, search_term) {
+ var line = $('')
+ .append($('').addClass('icon'))
+ .append($('').addClass('fields'));
+
+ $.each(fields, function (key, data) {
+ line.children('span.fields').append($('').addClass('field ' + key).html(data ? rcmail.ksearch_results_highlight(data, search_term) : ''));
+ });
+
+ return line.html();
+ };
} else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little