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