From ff4a92c8e2f11711975f9697a057cd96ce370bc5 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 14 Dec 2012 19:41:07 +0100 Subject: Fix contact copy/add-to-group operations on search result (#1488862) --- program/steps/addressbook/func.inc | 28 +++++++++++++++------------- program/steps/addressbook/groups.inc | 13 +++++++------ 2 files changed, 22 insertions(+), 19 deletions(-) (limited to 'program/steps/addressbook') diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index fded9a819..2f47483de 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -756,7 +756,7 @@ function rcmail_contact_key($row, $sort_col) * * @return array List of contact IDs per-source */ -function rcmail_get_cids() +function rcmail_get_cids($filter = null) { // contact ID (or comma-separated list of IDs) is provided in two // forms. If _source is an empty string then the ID is a string @@ -765,24 +765,25 @@ function rcmail_get_cids() $cid = get_input_value('_cid', RCUBE_INPUT_GPC); $source = (string) get_input_value('_source', RCUBE_INPUT_GPC); + if (is_array($cid)) { + return $cid; + } + if (!preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) { return array(); } - $cid = explode(',', $cid); - $got_source = strlen($source); - $result = array(); + $cid = explode(',', $cid); + $result = array(); // create per-source contact IDs array foreach ($cid as $id) { - // if _source is not specified we'll find it from decoded ID - if (!$got_source) { - if ($sep = strrpos($id, '-')) { - $contact_id = substr($id, 0, $sep); - $source_id = substr($id, $sep+1); - if (strlen($source_id)) { - $result[(string)$source_id][] = $contact_id; - } + // get source from decoded ID + if ($sep = strrpos($id, '-')) { + $contact_id = substr($id, 0, $sep); + $source_id = substr($id, $sep+1); + if (strlen($source_id)) { + $result[(string)$source_id][] = $contact_id; } } else { @@ -790,9 +791,10 @@ function rcmail_get_cids() } } - return $result; + return $filter !== null ? $result[$filter] : $result; } + // register UI objects $OUTPUT->add_handlers(array( 'directorylist' => 'rcmail_directory_list', diff --git a/program/steps/addressbook/groups.inc b/program/steps/addressbook/groups.inc index b70453889..3b9288a2b 100644 --- a/program/steps/addressbook/groups.inc +++ b/program/steps/addressbook/groups.inc @@ -20,7 +20,7 @@ */ $source = get_input_value('_source', RCUBE_INPUT_GPC); -$CONTACTS = rcmail_contact_source($source, true); +$CONTACTS = rcmail_contact_source($source); if ($CONTACTS->readonly || !$CONTACTS->groups) { $OUTPUT->show_message('sourceisreadonly', 'warning'); @@ -28,11 +28,11 @@ if ($CONTACTS->readonly || !$CONTACTS->groups) { } if ($RCMAIL->action == 'group-addmembers') { - if (($gid = get_input_value('_gid', RCUBE_INPUT_POST)) && ($ids = get_input_value('_cid', RCUBE_INPUT_POST))) { + if (($gid = get_input_value('_gid', RCUBE_INPUT_POST)) && ($ids = rcmail_get_cids($source))) { $plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array('group_id' => $gid, 'ids' => $ids, 'source' => $source)); $CONTACTS->set_group($gid); - $num2add = count(explode(',', $plugin['ids'])); + $num2add = count($plugin['ids']); if (!$plugin['abort']) { if (($maxnum = $RCMAIL->config->get('max_group_members', 0)) && ($CONTACTS->count()->count + $num2add > $maxnum)) { @@ -55,7 +55,7 @@ if ($RCMAIL->action == 'group-addmembers') { } else if ($RCMAIL->action == 'group-delmembers') { - if (($gid = get_input_value('_gid', RCUBE_INPUT_POST)) && ($ids = get_input_value('_cid', RCUBE_INPUT_POST))) { + if (($gid = get_input_value('_gid', RCUBE_INPUT_POST)) && ($ids = rcmail_get_cids($source))) { $plugin = $RCMAIL->plugins->exec_hook('group_delmembers', array('group_id' => $gid, 'ids' => $ids, 'source' => $source)); if (!$plugin['abort']) @@ -63,10 +63,11 @@ else if ($RCMAIL->action == 'group-delmembers') { else $result = $plugin['result']; - if ($result){ + if ($result) { $OUTPUT->show_message('contactremovedfromgroup'); $OUTPUT->command('remove_group_contacts',array('source' => $source, 'gid' => $gid)); - }else{ + } + else { $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'errorsaving', 'error'); } } -- cgit v1.2.3 From 83f7077ec930952cdc9cfc8982b80cd4dad06b5f Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Mon, 7 Jan 2013 14:21:25 +0100 Subject: Fix searching by date in address book (#1488888) --- CHANGELOG | 1 + program/js/app.js | 11 ++++--- program/lib/Roundcube/rcube_addressbook.php | 50 ++++++++++++++++++++++++++--- program/lib/Roundcube/rcube_contacts.php | 25 +++------------ program/lib/Roundcube/rcube_ldap.php | 17 ++-------- program/steps/addressbook/search.inc | 6 +++- 6 files changed, 65 insertions(+), 45 deletions(-) (limited to 'program/steps/addressbook') diff --git a/CHANGELOG b/CHANGELOG index dd249885d..fe98dd00f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Fix searching by date in address book (#1488888) - Improve charset detection by prioritizing charset according to user language (#1485669) - Fix handling of escaped separator in vCard file (#1488896) - Fix #countcontrols issue in IE<=8 when text is very long (#1488890) diff --git a/program/js/app.js b/program/js/app.js index 5b8c2cd2f..c627983f4 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -361,7 +361,7 @@ function rcube_webmail() if (this.gui_objects.editform) { this.enable_command('save', true); - if (this.env.action == 'add' || this.env.action == 'edit') + if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search') this.init_contact_form(); } @@ -4396,10 +4396,11 @@ function rcube_webmail() { var ref = this, col; - this.set_photo_actions($('#ff_photo').val()); - - for (col in this.env.coltypes) - this.init_edit_field(col, null); + if (this.env.coltypes) { + this.set_photo_actions($('#ff_photo').val()); + for (col in this.env.coltypes) + this.init_edit_field(col, null); + } $('.contactfieldgroup .row a.deletebutton').click(function() { ref.delete_edit_field(this); diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index 98d8f98ee..ffe35097a 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -46,6 +46,7 @@ abstract class rcube_addressbook public $sort_order = 'ASC'; public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1)); + protected $date_types = array(); protected $error; /** @@ -222,7 +223,6 @@ abstract class rcube_addressbook return true; } - /** * Create a new contact record * @@ -407,7 +407,6 @@ abstract class rcube_addressbook return array(); } - /** * Utility function to return all values of a certain data column * either as flat list or grouped by subtype @@ -440,7 +439,6 @@ abstract class rcube_addressbook return $out; } - /** * Normalize the given string for fulltext search. * Currently only optimized for Latin-1 characters; to be extended @@ -488,7 +486,6 @@ abstract class rcube_addressbook return $fn; } - /** * Compose the name to display in the contacts list for the given contact record. * This respects the settings parameter how to list conacts. @@ -526,5 +523,50 @@ abstract class rcube_addressbook return $fn; } + /** + * Compare search value with contact data + * + * @param string $colname Data name + * @param string|array $value Data value + * @param string $search Search value + * @param int $mode Search mode + * + * @return bool Comparision result + */ + protected function compare_search_value($colname, $value, $search, $mode) + { + // The value is a date string, for date we'll + // use only strict comparison (mode = 1) + // @TODO: partial search, e.g. match only day and month + if (in_array($colname, $this->date_types)) { + return (($value = rcube_utils::strtotime($value)) + && ($search = rcube_utils::strtotime($search)) + && date('Ymd', $value) == date('Ymd', $search)); + } + + // composite field, e.g. address + foreach ((array)$value as $val) { + $val = mb_strtolower($val); + switch ($mode) { + case 1: + $got = ($val == $search); + break; + + case 2: + $got = ($search == substr($val, 0, strlen($search))); + break; + + default: + $got = (strpos($val, $search) !== false); + } + + if ($got) { + return true; + } + } + + return false; + } + } diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index a98b13865..062bd1e19 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -45,6 +45,7 @@ class rcube_contacts extends rcube_addressbook private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname', 'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone', 'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes'); + protected $date_types = array('birthday', 'anniversary'); // public properties public $primary_key = 'contact_id'; @@ -401,32 +402,16 @@ class rcube_contacts extends rcube_addressbook for ($i=0; $i<$pages; $i++) { $this->list_records(null, $i, true); while ($row = $this->result->next()) { - $id = $row[$this->primary_key]; + $id = $row[$this->primary_key]; $found = array(); foreach (preg_grep($regexp, array_keys($row)) as $col) { $pos = strpos($col, ':'); $colname = $pos ? substr($col, 0, $pos) : $col; $search = $post_search[$colname]; foreach ((array)$row[$col] as $value) { - // composite field, e.g. address - foreach ((array)$value as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { - $found[$colname] = true; - break 2; - } + if ($this->compare_search_value($colname, $value, $search, $mode)) { + $found[$colname] = true; + break 2; } } } diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index d4bc669fd..700c6f60c 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -794,27 +794,14 @@ class rcube_ldap extends rcube_addressbook $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)"); // get all entries of this page and post-filter those that really match the query - $search = mb_strtolower($value); + $search = mb_strtolower($value); $entries = ldap_get_entries($this->conn, $this->ldap_result); for ($i = 0; $i < $entries['count']; $i++) { $rec = $this->_ldap2result($entries[$i]); foreach ($fields as $f) { foreach ((array)$rec[$f] as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { + if ($this->compare_search_value($f, $val, $search, $mode)) { $this->result->add($rec); $this->result->count++; break 2; diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc index 851325070..bbd9b9a76 100644 --- a/program/steps/addressbook/search.inc +++ b/program/steps/addressbook/search.inc @@ -300,9 +300,13 @@ function rcmail_contact_search_form($attrib) $label = isset($colprop['label']) ? $colprop['label'] : rcube_label($col); $category = $colprop['category'] ? $colprop['category'] : 'other'; - if ($ftype == 'text') + // load jquery UI datepicker for date fields + if ($colprop['type'] == 'date') + $colprop['class'] .= ($colprop['class'] ? ' ' : '') . 'datepicker'; + else if ($ftype == 'text') $colprop['size'] = $i_size; + $content = html::div('row', html::div('contactfieldlabel label', Q($label)) . html::div('contactfieldcontent', rcmail_get_edit_field('search_'.$col, '', $colprop, $ftype))); -- cgit v1.2.3 From 9a6c38e14895bd093627e12f2fcf2c6ff1e3af3c Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 11 Jan 2013 14:39:23 +0100 Subject: New feature to export only selected contacts from addressbook (by Phil Weir) --- CHANGELOG | 1 + program/js/app.js | 8 ++++++++ program/localization/en_US/labels.inc | 2 ++ program/steps/addressbook/export.inc | 23 +++++++++++++++++++++++ skins/classic/addressbook.css | 8 ++++++++ skins/classic/templates/addressbook.html | 10 ++++++++++ skins/larry/styles.css | 2 +- skins/larry/templates/addressbook.html | 12 +++++++++++- 8 files changed, 64 insertions(+), 2 deletions(-) (limited to 'program/steps/addressbook') diff --git a/CHANGELOG b/CHANGELOG index 25d79d1e2..ecc45c220 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Feature to export only selected contacts from addressbook (by Phil Weir) - Force autocommit mode in mysql database driver (#1488902) RELEASE 0.9-beta diff --git a/program/js/app.js b/program/js/app.js index c627983f4..2804e88df 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1090,6 +1090,12 @@ function rcube_webmail() } break; + case 'export-selected': + if (this.contact_list.rowcount > 0) { + this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }); + } + break; + case 'upload-photo': this.upload_contact_photo(props || this.gui_objects.uploadform); break; @@ -4115,6 +4121,7 @@ function rcube_webmail() // thend we can enable the group-remove-selected command this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0); this.enable_command('compose', this.env.group || list.selection.length > 0); + this.enable_command('export-selected', list.selection.length > 0); this.enable_command('edit', id && writable); this.enable_command('delete', list.selection.length && writable); @@ -6238,6 +6245,7 @@ function rcube_webmail() this.enable_command('compose', (uid && this.contact_list.rows[uid])); this.enable_command('delete', 'edit', writable); this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0)); + this.enable_command('export-selected', false); } case 'moveto': diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 9deaa6677..a0b6e6a31 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -335,6 +335,8 @@ $labels['composeto'] = 'Compose mail to'; $labels['contactsfromto'] = 'Contacts $from to $to of $count'; $labels['print'] = 'Print'; $labels['export'] = 'Export'; +$labels['exportall'] = 'Export all'; +$labels['exportsel'] = 'Export selected'; $labels['exportvcards'] = 'Export contacts in vCard format'; $labels['newcontactgroup'] = 'Create new contact group'; $labels['grouprename'] = 'Rename group'; diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 850795c85..bf0657b74 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -56,6 +56,29 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search $result = new rcube_result_set($count); $result->records = array_values($records); } +// selected contacts +else if (!empty($_REQUEST['_cid'])) { + $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name'); + $records = array(); + + $cids = explode(',', get_input_value('_cid', RCUBE_INPUT_GET)); + $CONTACTS = rcmail_contact_source(null, true); + + // Get records from all sources + foreach ($cids as $cid) { + $record = $CONTACTS->get_record($cid, true); + $key = rcmail_contact_key($record, $sort_col); + $records[$key] = $record; + unset($record); + } + + ksort($records, SORT_LOCALE_STRING); + + // create resultset object + $count = count($records); + $result = new rcube_result_set($count); + $result->records = array_values($records); +} // selected directory/group else { $CONTACTS = rcmail_contact_source(null, true); diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css index a398325b4..1bb1e2c61 100644 --- a/skins/classic/addressbook.css +++ b/skins/classic/addressbook.css @@ -67,6 +67,14 @@ background-position: -128px -32px; } +#abooktoolbar a.exportAll { + background-position: -128px 0; +} + +#abooktoolbar a.exportAllSel { + background-position: -128px -32px; +} + #abooktoolbar span.separator { width: 5px; background-position: -162px 0; diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index 74673007a..404fb2c11 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -27,7 +27,10 @@   + + + @@ -38,6 +41,13 @@ +
+
    +
  • +
  • +
+
+
  • diff --git a/skins/larry/styles.css b/skins/larry/styles.css index 9386c79d7..773ef23d4 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -1680,6 +1680,7 @@ ul.proplist li { } .toolbar a.button.export { + min-width: 74px; background-position: center -1054px; } @@ -1695,7 +1696,6 @@ ul.proplist li { background-position: 0 -1745px; } - a.menuselector { display: inline-block; border: 1px solid #ababab; diff --git a/skins/larry/templates/addressbook.html b/skins/larry/templates/addressbook.html index 9a9a2d747..7904f6f3a 100644 --- a/skins/larry/templates/addressbook.html +++ b/skins/larry/templates/addressbook.html @@ -13,7 +13,11 @@
    - + + + + + @@ -75,6 +79,12 @@
    +
    +
      +
    • +
    • +
    +
      -- cgit v1.2.3 From 8e8f3b96b51fde1df953de7398b15e0f01e10777 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 17 Jan 2013 18:47:01 +0100 Subject: Fix export of selected contacts from search result (#1488905) --- program/steps/addressbook/export.inc | 70 ++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) (limited to 'program/steps/addressbook') diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index bf0657b74..fc9f23fa1 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -40,11 +40,31 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search // get records $result = $source->list_records(); - while ($row = $result->next()) { - $row['sourceid'] = $s; - $key = rcmail_contact_key($row, $sort_col); - $records[$key] = $row; + while ($record = $result->next()) { + // because vcard_map is per-source we need to create vcard here + if (empty($record['vcard']) || empty($record['name'])) { + $vcard = new rcube_vcard(); + $vcard->extend_fieldmap($source->vcard_map); + $vcard->load($record['vcard']); + $vcard->reset(); + + foreach ($record as $key => $values) { + list($field, $section) = explode(':', $key); + foreach ((array)$values as $value) { + if (is_array($value) || @strlen($value)) { + $vcard->set($field, $value, strtoupper($section)); + } + } + } + + $record['vcard'] = $vcard->export(true); + } + + $record['sourceid'] = $s; + $key = rcmail_contact_key($record, $sort_col); + $records[$key] = $record; } + unset($result); } @@ -59,17 +79,39 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search // selected contacts else if (!empty($_REQUEST['_cid'])) { $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name'); - $records = array(); + $records = array(); - $cids = explode(',', get_input_value('_cid', RCUBE_INPUT_GET)); - $CONTACTS = rcmail_contact_source(null, true); + // Selected contact IDs (with multi-source support) + $cids = rcmail_get_cids(); - // Get records from all sources - foreach ($cids as $cid) { - $record = $CONTACTS->get_record($cid, true); - $key = rcmail_contact_key($record, $sort_col); - $records[$key] = $record; - unset($record); + foreach ($cids as $s => $ids) { + $source = $RCMAIL->get_address_book($s); + $result = $source->search('ID', $ids, 1, true, true); + + while ($record = $result->next()) { + // because vcard_map is per-source we need to create vcard here + if (empty($record['vcard']) || empty($record['name'])) { + $vcard = new rcube_vcard(); + $vcard->extend_fieldmap($source->vcard_map); + $vcard->load($record['vcard']); + $vcard->reset(); + + foreach ($record as $key => $values) { + list($field, $section) = explode(':', $key); + foreach ((array)$values as $value) { + if (is_array($value) || @strlen($value)) { + $vcard->set($field, $value, strtoupper($section)); + } + } + } + + $record['vcard'] = $vcard->export(true); + } + + $record['sourceid'] = $s; + $key = rcmail_contact_key($record, $sort_col); + $records[$key] = $record; + } } ksort($records, SORT_LOCALE_STRING); @@ -91,7 +133,7 @@ else { // send downlaod headers header('Content-Type: text/x-vcard; charset='.RCMAIL_CHARSET); -header('Content-Disposition: attachment; filename="rcube_contacts.vcf"'); +header('Content-Disposition: attachment; filename="contacts.vcf"'); while ($result && ($row = $result->next())) { // we already have a vcard record -- cgit v1.2.3 From 13dc9f2c862668554d87dcbf95f2f7bbaf221bf3 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 25 Jan 2013 14:15:12 +0100 Subject: Move rcmail_contact_key() to rcube_addressbook::compose_contact_key() --- program/lib/Roundcube/rcube_addressbook.php | 16 ++++++++++++++++ program/steps/addressbook/delete.inc | 2 +- program/steps/addressbook/export.inc | 4 ++-- program/steps/addressbook/func.inc | 18 ------------------ program/steps/addressbook/list.inc | 2 +- program/steps/addressbook/search.inc | 2 +- 6 files changed, 21 insertions(+), 23 deletions(-) (limited to 'program/steps/addressbook') diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index 421062772..cbc3c6773 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -523,6 +523,22 @@ abstract class rcube_addressbook return $fn; } + /** + * Create a unique key for sorting contacts + */ + public static function compose_contact_key($contact, $sort_col) + { + $key = $contact[$sort_col] . ':' . $row['sourceid']; + + // add email to a key to not skip contacts with the same name (#1488375) + if (!empty($contact['email'])) { + $key .= ':' . implode(':', (array)$contact['email']); + } + + return $key; + } + + /** * Compare search value with contact data * diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc index 81b8a0970..56118583c 100644 --- a/program/steps/addressbook/delete.inc +++ b/program/steps/addressbook/delete.inc @@ -93,7 +93,7 @@ if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$searc while ($row = $result->next()) { $row['sourceid'] = $s; - $key = rcmail_contact_key($row, $sort_col); + $key = rcube_addressbook::compose_contact_key($row, $sort_col); $records[$key] = $row; } unset($result); diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index fc9f23fa1..15bf8b0d4 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -61,7 +61,7 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search } $record['sourceid'] = $s; - $key = rcmail_contact_key($record, $sort_col); + $key = rcube_addressbook::compose_contact_key($record, $sort_col); $records[$key] = $record; } @@ -109,7 +109,7 @@ else if (!empty($_REQUEST['_cid'])) { } $record['sourceid'] = $s; - $key = rcmail_contact_key($record, $sort_col); + $key = rcube_addressbook::compose_contact_key($record, $sort_col); $records[$key] = $record; } } diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 2f47483de..7fb862d5e 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -733,24 +733,6 @@ function rcmail_format_date_col($val) } -function rcmail_contact_key($row, $sort_col) -{ - $key = $row[$sort_col] . ':' . $row['sourceid']; - - // add email to a key to not skip contacts with the same name (#1488375) - if (!empty($row['email'])) { - if (is_array($row['email'])) { - $key .= ':' . implode(':', $row['email']); - } - else { - $key .= ':' . $row['email']; - } - } - - return $key; -} - - /** * Returns contact ID(s) and source(s) from GET/POST data * diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc index 06a1e10a3..1bb28658b 100644 --- a/program/steps/addressbook/list.inc +++ b/program/steps/addressbook/list.inc @@ -49,7 +49,7 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search while ($row = $result->next()) { $row['sourceid'] = $s; - $key = rcmail_contact_key($row, $sort_col); + $key = rcube_addressbook::compose_contact_key($row, $sort_col); $records[$key] = $row; } unset($result); diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc index bbd9b9a76..d153c255a 100644 --- a/program/steps/addressbook/search.inc +++ b/program/steps/addressbook/search.inc @@ -184,7 +184,7 @@ function rcmail_contact_search() while ($row = $result->next()) { $row['sourceid'] = $s['id']; - $key = rcmail_contact_key($row, $sort_col); + $key = rcube_addressbook::compose_contact_key($row, $sort_col); $records[$key] = $row; } -- cgit v1.2.3 From 3c309af0663f1673f921682c6ae3f673426e1397 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 31 Jan 2013 13:49:35 +0100 Subject: - Refactored the hierarchical mailboxlist control into a separate widget class - Build address book directories list as hierarchical list - Make address book groups collapsible using the new new treelist widget - Use encoded identifiers for address book directory list items --- program/include/rcmail.php | 16 +- program/js/app.js | 206 +++++---------- program/js/treelist.js | 425 +++++++++++++++++++++++++++++++ program/steps/addressbook/func.inc | 28 +- skins/classic/addressbook.css | 35 ++- skins/classic/templates/addressbook.html | 3 +- skins/larry/addressbook.css | 21 +- skins/larry/templates/addressbook.html | 2 +- 8 files changed, 566 insertions(+), 170 deletions(-) create mode 100644 program/js/treelist.js (limited to 'program/steps/addressbook') diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 5f2a2177b..70dba4192 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -1406,6 +1406,7 @@ class rcmail extends rcube $js_mailboxlist = array(); $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib); + $rcmail->output->include_script('treelist.js'); $rcmail->output->add_gui_object('mailboxlist', $attrib['id']); $rcmail->output->set_env('mailboxes', $js_mailboxlist); $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']); @@ -1584,14 +1585,13 @@ class rcmail extends rcube 'id' => "rcmli".$folder_id, 'class' => join(' ', $classes), 'noclose' => true), - html::a($link_attrib, $html_name) . - (!empty($folder['folders']) ? html::div(array( - 'class' => ($is_collapsed ? 'collapsed' : 'expanded'), - 'style' => "position:absolute", - 'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail_output::JS_OBJECT_NAME, $js_name) - ), ' ') : '')); - - $jslist[$folder_id] = array( + html::a($link_attrib, $html_name)); + + if (!empty($folder['folders'])) { + $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); + } + + $jslist[$folder['id']] = array( 'id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual'] diff --git a/program/js/app.js b/program/js/app.js index 6d5bdfe74..dbb65eea4 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -4,7 +4,7 @@ | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2013, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2011-2013, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -459,9 +459,22 @@ function rcube_webmail() this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); // map implicit containers - if (this.gui_objects.folderlist) + if (this.gui_objects.folderlist) { this.gui_containers.foldertray = $(this.gui_objects.folderlist); + // init treelist widget + if (window.rcube_treelist_widget) { + this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { + id_prefix: 'rcmli', + id_encode: this.html_identifier_encode, + id_decode: this.html_identifier_decode, + check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) } + }); + this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) }); + this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) }); + } + } + // activate html5 file drop feature (if browser supports it and if configured) if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); }); @@ -1263,11 +1276,12 @@ function rcube_webmail() this.html_identifier = function(str, encode) { - str = String(str); - if (encode) - return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); - else - return str.replace(this.identifier_expr, '_'); + return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_'); + }; + + this.html_identifier_encode = function(str) + { + return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); }; this.html_identifier_decode = function(str) @@ -1320,29 +1334,9 @@ function rcube_webmail() if (this.preview_read_timer) clearTimeout(this.preview_read_timer); - // save folderlist and folders location/sizes for droptarget calculation in drag_move() - if (this.gui_objects.folderlist && model) { - this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset; - this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop; - - var k, li, height, - list = $(this.gui_objects.folderlist); - pos = list.offset(); - - this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() }; - - this.env.folder_coords = []; - for (k in model) { - if (li = this.get_folder_li(k)) { - // only visible folders - if (height = li.firstChild.offsetHeight) { - pos = $(li.firstChild).offset(); - this.env.folder_coords[k] = { x1:pos.left, y1:pos.top, - x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 }; - } - } - } - } + // prepare treelist widget for dragging interactions + if (this.treelist) + this.treelist.drag_start(); }; this.drag_end = function(e) @@ -1350,87 +1344,28 @@ function rcube_webmail() this.drag_active = false; this.env.last_folder_target = null; - if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - // over the folders - if (this.gui_objects.folderlist && this.env.folder_coords) { - for (var k in this.env.folder_coords) { - if (this.env.folder_coords[k].on) - $(this.get_folder_li(k)).removeClass('droptarget'); - } - } + if (this.treelist) + this.treelist.drag_end(); }; this.drag_move = function(e) { - if (this.gui_objects.folderlist && this.env.folder_coords) { - var k, li, div, check, oldclass, + if (this.gui_objects.folderlist) { + var drag_target, oldclass, layerclass = 'draglayernormal', - mouse = rcube_event.get_mouse_pos(e), - pos = this.env.folderlist_coords, - // offsets to compensate for scrolling while dragging a message - boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop, - moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop; + mouse = rcube_event.get_mouse_pos(e); if (this.contact_list && this.contact_list.draglayer) oldclass = this.contact_list.draglayer.attr('class'); - mouse.y += -moffset-boffset; - - // if mouse pointer is outside of folderlist - if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) { - if (this.env.last_folder_target) { - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); - this.env.folder_coords[this.env.last_folder_target].on = 0; - this.env.last_folder_target = null; - } - if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) - this.contact_list.draglayer.attr('class', layerclass); - return; + // mouse intersects a valid drop target on the treelist + if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) { + this.env.last_folder_target = drag_target; + layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal'); } - - // over the folders - for (k in this.env.folder_coords) { - pos = this.env.folder_coords[k]; - if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) { - if (check = this.check_droptarget(k)) { - li = this.get_folder_li(k); - div = $(li.getElementsByTagName('div')[0]); - - // if the folder is collapsed, expand it after 1sec and restart the drag & drop process. - if (div.hasClass('collapsed')) { - if (this.folder_auto_timer) - clearTimeout(this.folder_auto_timer); - - this.folder_auto_expand = this.env.mailboxes[k].id; - this.folder_auto_timer = setTimeout(function() { - rcmail.command('collapse-folder', rcmail.folder_auto_expand); - rcmail.drag_start(null); - }, 1000); - } - else if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - $(li).addClass('droptarget'); - this.env.folder_coords[k].on = 1; - this.env.last_folder_target = k; - layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal'); - } - // Clear target, otherwise drag end will trigger move into last valid droptarget - else - this.env.last_folder_target = null; - } - else if (pos.on) { - $(this.get_folder_li(k)).removeClass('droptarget'); - this.env.folder_coords[k].on = 0; - } + else { + // Clear target, otherwise drag end will trigger move into last valid droptarget + this.env.last_folder_target = null; } if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) @@ -1440,40 +1375,33 @@ function rcube_webmail() this.collapse_folder = function(name) { - var li = this.get_folder_li(name, '', true), - div = $('div:first', li), - ul = $('ul:first', li); + if (this.treelist) + this.treelist.toggle(name); + }; + + this.folder_collapsed = function(node) + { + var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders'; - if (div.hasClass('collapsed')) { - ul.show(); - div.removeClass('collapsed').addClass('expanded'); - var reg = new RegExp('&'+urlencode(name)+'&'); - this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, ''); - } - else if (div.hasClass('expanded')) { - ul.hide(); - div.removeClass('expanded').addClass('collapsed'); - this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&'; + if (node.collapsed) { + this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&'; // select the folder if one of its childs is currently selected // don't select if it's virtual (#1488346) - if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual')) + if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual) this.command('list', name); } - else - return; - - // Work around a bug in IE6 and IE7, see #1485309 - if (bw.ie6 || bw.ie7) { - var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null; - if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') { - li.style.display = 'none'; - li.style.display = ''; - } + else { + var reg = new RegExp('&'+urlencode(node.id)+'&'); + this.env[prefname] = this.env[prefname].replace(reg, ''); } - this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders }); - this.set_unread_count_display(name, false); + if (!this.drag_active) { + this.command('save-pref', { name: prefname, value: this.env[prefname] }); + + if (this.env.unread_counts) + this.set_unread_count_display(node.id, false); + } }; this.doc_mouse_up = function(e) @@ -1498,9 +1426,9 @@ function rcube_webmail() if (this.drag_active && model && this.env.last_folder_target) { var target = model[this.env.last_folder_target]; - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); this.env.last_folder_target = null; list.draglayer.hide(); + this.drag_end(e); if (!this.drag_menu(e, target)) this.command('moveto', target); @@ -4164,7 +4092,7 @@ function rcube_webmail() else if (!this.env.search_request) folder = group ? 'G'+src+group : src; - this.select_folder(folder); + this.select_folder(folder, '', true); this.env.source = src; this.env.group = group; @@ -4468,7 +4396,7 @@ function rcube_webmail() this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.env.group_renaming = true; - var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG'); + var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true); if (li && (link = li.firstChild)) { $(link).hide().before(this.name_input); } @@ -4489,7 +4417,7 @@ function rcube_webmail() this.remove_group_item = function(prop) { var li, key = 'G'+prop.source+prop.id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li }); li.parentNode.removeChild(li); @@ -4511,7 +4439,7 @@ function rcube_webmail() this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.name_input_li = $('
    • ').addClass(type).append(this.name_input); - var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source); + var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : $('ul.groups li:last', this.get_folder_li(this.env.source,'',true)); this.name_input_li.insertAfter(li); } @@ -4612,7 +4540,7 @@ function rcube_webmail() this.reset_add_input(); var key = 'G'+prop.source+prop.id, - li = this.get_folder_li(key), + li = this.get_folder_li(key,'',true), link; // group ID has changed, replace link node and identifiers @@ -4651,8 +4579,8 @@ function rcube_webmail() this.add_contact_group_row = function(prop, li, reloc) { var row, name = prop.name.toUpperCase(), - sibling = this.get_folder_li(prop.source), - prefix = 'rcmliG' + this.html_identifier(prop.source); + sibling = this.get_folder_li(prop.source,'',true), + prefix = 'rcmli' + this.html_identifier('G'+prop.source, true); // When renaming groups, we need to remove it from DOM and insert it in the proper place if (reloc) { @@ -4901,12 +4829,12 @@ function rcube_webmail() .attr('rel', id) .click(function() { return rcmail.command('listsearch', id, this); }) .html(name), - li = $('
    • ').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'}) + li = $('
    • ').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' }) .append(link), prop = {name:name, id:id, li:li[0]}; this.add_saved_search_row(prop, li); - this.select_folder('S'+id); + this.select_folder(key,'',true); this.enable_command('search-delete', true); this.env.search_id = id; @@ -4960,7 +4888,7 @@ function rcube_webmail() this.remove_search_item = function(id) { var li, key = 'S'+id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('search_delete', { id:id, li:li }); li.parentNode.removeChild(li); @@ -4982,7 +4910,7 @@ function rcube_webmail() } this.reset_qsearch(); - this.select_folder('S'+id); + this.select_folder('S'+id, '', true); // reset vars this.env.current_page = 1; diff --git a/program/js/treelist.js b/program/js/treelist.js new file mode 100644 index 000000000..47ac0c1a0 --- /dev/null +++ b/program/js/treelist.js @@ -0,0 +1,425 @@ +/* + +-----------------------------------------------------------------------+ + | Roundcube Treelist widget | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2013, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + +-----------------------------------------------------------------------+ + | Authors: Thomas Bruederli | + +-----------------------------------------------------------------------+ + | Requires: common.js | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Roundcube Treelist widget class + * @contructor + */ +function rcube_treelist_widget(node, p) +{ + // apply some defaults to p + p = $.extend({ + id_prefix: '', + autoexpand: 1000, + selectable: false, + check_droptarget: function(node){ return !node.virtual } + }, p || {}); + + var container = $(node); + var data = p.data || []; + var indexbyid = {}; + var selection = null; + var drag_active = false; + var box_coords = {}; + var item_coords = []; + var autoexpand_timer; + var autoexpand_item; + var body_scroll_top = 0; + var list_scroll_top = 0; + var me = this; + + + /////// export public members and methods + + this.container = container; + this.expand = expand; + this.collapse = collapse; + this.select = select; + this.render = render; + this.drag_start = drag_start; + this.drag_end = drag_end; + this.intersects = intersects; + + + /////// startup code (constructor) + + // abort if node not found + if (!container.length) + return; + + if (p.data) { + index_data({ children:data }); + } + // load data from DOM + else { + data = walk_list(container); + // console.log(data); + } + + // register click handlers on list + container.on('click', 'div.treetoggle', function(e){ + toggle(dom2id($(this).parent())); + }); + + container.on('click', 'li', function(e){ + var node = p.selectable ? indexbyid[dom2id($(this))] : null; + if (node && !node.virtual) { + select(node.id); + e.stopPropagation(); + } + }); + + + /////// private methods + + /** + * Collaps a the node with the given ID + */ + function collapse(id, recursive, set) + { + var node; + if (node = indexbyid[id]) { + node.collapsed = typeof set == 'undefined' || set; + update_dom(node); + + // Work around a bug in IE6 and IE7, see #1485309 + if (window.bw && (bw.ie6 || bw.ie7) && node.collapsed) { + id2dom(node.id).next().children('ul:visible').hide().show(); + } + + if (recursive && node.children) { + for (var i=0; i < node.children.length; i++) { + collapse(node.children[i].id, recursive, set); + } + } + + me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node); + } + } + + /** + * Expand a the node with the given ID + */ + function expand(id, recursive) + { + collapse(id, recursive, false); + } + + /** + * Toggle collapsed state of a list node + */ + function toggle(id, recursive) + { + var node; + if (node = indexbyid[id]) { + collapse(id, recursive, !node.collapsed); + } + } + + /** + * Select a tree node by it's ID + */ + function select(id) + { + if (selection) { + id2dom(selection).removeClass('selected'); + selection = null; + } + + var li = id2dom(id); + if (li.length) { + li.addClass('selected'); + selection = id; + // TODO: expand all parent nodes if collapsed + scroll_to_node(li); + } + + me.triggerEvent('select', indexbyid[id]); + } + + /** + * Getter for the currently selected node ID + */ + function get_selection() + { + return selection; + } + + /** + * Return the DOM element of the list item with the given ID + */ + function get_item(id) + { + return id2dom(id).get(0); + } + + /** + * Apply the 'collapsed' status of the data node to the corresponding DOM element(s) + */ + function update_dom(node) + { + var li = id2dom(node.id); + li.children('ul').first()[(node.collapsed ? 'hide' : 'show')](); + li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded'); + me.triggerEvent('toggle', node); + } + + /** + * Render the tree list from the internal data structure + */ + function render() + { + if (me.triggerEvent('renderBefore', data) === false) + return; + + // remove all child nodes + container.html(''); + + // render child nodes + for (var i=0; i < data.length; i++) { + render_node(data[i], container); + } + + me.triggerEvent('renderAfter', container); + } + + /** + * Render a specific node into the DOM list + */ + function render_node(node, parent) + { + var li = $('
    • ' + node.html + '
    • ') + .attr('id', p.id_prefix + node.id) + .addClass((node.classes || []).join(' ')) + .appendTo(parent); + + if (node.virtual) + li.addClass('virtual'); + if (node.id == selection) + li.addClass('selected'); + + // add child list and toggle icon + if (node.children && node.children.length) { + $('
       
      ').appendTo(li); + var ul = $('
        ').appendTo(li); + if (node.collapsed) + ul.hide(); + + for (var i=0; i < node.children.length; i++) { + render_node(node.children[i], ul); + } + } + } + + /** + * Recursively walk the DOM tree and build an internal data structure + * representing the skeleton of this tree list. + */ + function walk_list(ul) + { + var result = []; + ul.children('li').each(function(i,e){ + var li = $(e); + var node = { + id: dom2id(li), + classes: li.attr('class').split(' '), + virtual: li.hasClass('virtual'), + html: li.children().first().get(0).outerHTML, + children: walk_list(li.children('ul')) + } + + if (node.children.length) { + node.collapsed = li.children('ul').css('display') == 'none'; + } + if (li.hasClass('selected')) { + selection = node.id; + } + + result.push(node); + indexbyid[node.id] = node; + }) + + return result; + } + + /** + * Recursively walk the data tree and index nodes by their ID + */ + function index_data(node) + { + if (node.id) { + indexbyid[node.id] = node; + } + for (var c=0; node.children && c < node.children.length; c++) { + index_data(node.children[c]); + } + } + + /** + * Get the (stripped) node ID from the given DOM element + */ + function dom2id(li) + { + var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), ''); + return p.id_decode ? p.id_decode(domid) : domid; + } + + /** + * Get the
      • element for the given node ID + */ + function id2dom(id) + { + var domid = p.id_encode ? p.id_encode(id) : id; + return $('#' + p.id_prefix + domid); + } + + /** + * Scroll the parent container to make the given list item visible + */ + function scroll_to_node(li) + { + var scroller = container.parent(); + scroller.scrollTop(li.offset().top - scroller.offset().top + scroller.scrollTop()); + } + + ///// drag & drop support + + /** + * When dragging starts, compute absolute bounding boxes of the list and it's items + * for faster comparisons while mouse is moving + */ + function drag_start() + { + var li, item, height, + pos = container.offset(); + + body_scroll_top = bw.ie ? 0 : window.pageYOffset; + list_scroll_top = container.parent().scrollTop(); + + drag_active = true; + box_coords = { + x1: pos.left, + y1: pos.top, + x2: pos.left + container.width(), + y2: pos.top + container.height() + }; + + item_coords = []; + for (var id in indexbyid) { + li = id2dom(id); + item = li.children().first().get(0); + if (height = item.offsetHeight) { + pos = $(item).offset(); + item_coords[id] = { + x1: pos.left, + y1: pos.top, + x2: pos.left + item.offsetWidth, + y2: pos.top + height, + on: id == autoexpand_item + }; + } + } + } + + /** + * Signal that dragging has stopped + */ + function drag_end() + { + drag_active = false; + + if (autoexpand_timer) { + clearTimeout(autoexpand_timer); + autoexpand_timer = null; + autoexpand_item = null; + } + + $('li.droptarget', container).removeClass('droptarget'); + } + + /** + * Determine if the given mouse coords intersect the list and one if its items + */ + function intersects(mouse, highlight) + { + // offsets to compensate for scrolling while dragging a message + var boffset = bw.ie ? -document.documentElement.scrollTop : body_scroll_top, + moffset = list_scroll_top - container.parent().scrollTop(), + result = null; + + mouse.top = mouse.y + -moffset - boffset; + + // no intersection with list bounding box + if (mouse.x < box_coords.x1 || mouse.x >= box_coords.x2 || mouse.top < box_coords.y1 || mouse.top >= box_coords.y2) { + // TODO: optimize performance for this operation + $('li.droptarget', container).removeClass('droptarget'); + return result; + } + + // check intersection with visible list items + var pos, node; + for (var id in item_coords) { + pos = item_coords[id]; + if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.top >= pos.y1 && mouse.top < pos.y2) { + node = indexbyid[id]; + + // if the folder is collapsed, expand it after the configured time + if (node.children && node.children.length && node.collapsed && p.autoexpand && autoexpand_item != id) { + if (autoexpand_timer) + clearTimeout(autoexpand_timer); + + autoexpand_item = id; + autoexpand_timer = setTimeout(function() { + expand(autoexpand_item); + drag_start(); // re-calculate item coords + autoexpand_item = null; + }, p.autoexpand); + } + else if (autoexpand_timer && autoexpand_item != id) { + clearTimeout(autoexpand_timer); + autoexpand_item = null; + autoexpand_timer = null; + } + + // check if this item is accepted as drop target + if (p.check_droptarget(node)) { + if (highlight) { + id2dom(id).addClass('droptarget'); + pos.on = true; + } + result = id; + } + else { + result = null; + } + } + else if (pos.on) { + id2dom(id).removeClass('droptarget'); + pos.on = false; + } + } + + return result; + } +} + +// use event processing functions from Roundcube's rcube_event_engine +rcube_treelist_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener; +rcube_treelist_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener; +rcube_treelist_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent; diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 7fb862d5e..80631cd61 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -187,7 +187,7 @@ function rcmail_directory_list($attrib) $jsdata = array(); $line_templ = html::tag('li', array( - 'id' => 'rcmli%s', 'class' => '%s'), + 'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true), html::a(array('href' => '%s', 'rel' => '%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s')); @@ -213,7 +213,7 @@ function rcmail_directory_list($attrib) $name = !empty($source['name']) ? $source['name'] : $id; $out .= sprintf($line_templ, - html_identifier($id), + rcube_utils::html_identifier($id, true), $class_name, Q(rcmail_url(null, array('_source' => $id))), $source['id'], @@ -224,10 +224,11 @@ function rcmail_directory_list($attrib) $groupdata = rcmail_contact_groups($groupdata); $jsdata = $groupdata['jsdata']; $out = $groupdata['out']; + $out .= '
      • '; } $line_templ = html::tag('li', array( - 'id' => 'rcmliS%s', 'class' => '%s'), + 'id' => 'rcmli%s', 'class' => '%s'), html::a(array('href' => '#', 'rel' => 'S%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s')); @@ -245,14 +246,17 @@ function rcmail_directory_list($attrib) $class_name .= ' ' . $source['class_name']; $out .= sprintf($line_templ, - html_identifier($id), + rcube_utils::html_identifier('S'.$id, true), $class_name, $id, $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id))); } $OUTPUT->set_env('contactgroups', $jsdata); + $OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks','')); $OUTPUT->add_gui_object('folderlist', $attrib['id']); + $OUTPUT->include_script('treelist.js'); + // add some labels to client $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember'); @@ -265,18 +269,24 @@ function rcmail_contact_groups($args) global $RCMAIL; $groups = $RCMAIL->get_address_book($args['source'])->list_groups(); + $js_id = $RCMAIL->JQ($args['source']); if (!empty($groups)) { $line_templ = html::tag('li', array( - 'id' => 'rcmliG%s', 'class' => 'contactgroup'), + 'id' => 'rcmli%s', 'class' => 'contactgroup'), html::a(array('href' => '#', 'rel' => '%s:%s', 'onclick' => "return ".JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s')); + // append collapse/expand toggle and open a new
          + $is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false; + $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); + $jsdata = array(); + $groups_html = ''; foreach ($groups as $group) { - $args['out'] .= sprintf($line_templ, - html_identifier($args['source'] . $group['ID']), + $groups_html .= sprintf($line_templ, + rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true), $args['source'], $group['ID'], $args['source'], $group['ID'], Q($group['name']) ); @@ -286,6 +296,10 @@ function rcmail_contact_groups($args) } } + $args['out'] .= html::tag('ul', + array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), + $groups_html); + return $args; } diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css index 78314538a..5afa4592f 100644 --- a/skins/classic/addressbook.css +++ b/skins/classic/addressbook.css @@ -118,7 +118,7 @@ #directorylistbox input { - margin: 0px; + margin: 0 0 0 20px; font-size: 11px; width: 90%; } @@ -144,7 +144,8 @@ width: 280px; } -#directorylist +#directorylist, +#directorylist li ul { list-style: none; margin: 0; @@ -152,11 +153,15 @@ background-color: #FFFFFF; } +#directorylist li ul +{ + border-top: 1px solid #EBEBEB; +} + #directorylist li { display: block; font-size: 11px; - background: url(images/icons/folders.png) 5px -108px no-repeat; border-bottom: 1px solid #EBEBEB; white-space: nowrap; } @@ -168,31 +173,37 @@ padding-left: 25px; padding-top: 2px; padding-bottom: 2px; + height: 16px; text-decoration: none; white-space: nowrap; + background: url(images/icons/folders.png) 5px -108px no-repeat; } -#directorylist li.contactgroup +#directorylist li ul li a { - padding-left: 15px; - background-position: 20px -143px; + padding-left: 45px; } -#directorylist li.contactsearch +#directorylist li ul li:last-child { - background-position: 6px -162px; + border-bottom: 0; } -#directorylist li.selected +#directorylist li.contactgroup a { - background-color: #929292; - border-bottom: 1px solid #898989; + background-position: 22px -143px; +} + +#directorylist li.contactsearch a +{ + background-position: 6px -162px; } -#directorylist li.selected a +#directorylist li.selected > a { color: #FFF; font-weight: bold; + background-color: #929292; } #directorylist li.droptarget diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index 404fb2c11..9d959d5ef 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -63,8 +63,7 @@
          - - +
        tag --- program/steps/addressbook/func.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'program/steps/addressbook') diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 80631cd61..f6d2ae519 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -268,6 +268,7 @@ function rcmail_contact_groups($args) { global $RCMAIL; + $groups_html = ''; $groups = $RCMAIL->get_address_book($args['source'])->list_groups(); $js_id = $RCMAIL->JQ($args['source']); @@ -283,7 +284,6 @@ function rcmail_contact_groups($args) $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); $jsdata = array(); - $groups_html = ''; foreach ($groups as $group) { $groups_html .= sprintf($line_templ, rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true), -- cgit v1.2.3 From 63cff249a8937b65b168f3171b395d83cfae9bd2 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 10 Feb 2013 14:37:37 +0100 Subject: Properly quote form validation error messages --- program/steps/addressbook/save.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'program/steps/addressbook') diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc index 901ea0190..8cab6e817 100644 --- a/program/steps/addressbook/save.inc +++ b/program/steps/addressbook/save.inc @@ -82,7 +82,7 @@ if (empty($a_record['name'])) { // do input checks (delegated to $CONTACTS instance) if (!$CONTACTS->validate($a_record)) { $err = (array)$CONTACTS->get_error(); - $OUTPUT->show_message($err['message'] ? $err['message'] : 'formincomplete', 'warning'); + $OUTPUT->show_message($err['message'] ? Q($err['message']) : 'formincomplete', 'warning'); $GLOBALS['EDIT_RECORD'] = $a_record; // store submitted data to be used in edit form rcmail_overwrite_action($return_action); return; -- cgit v1.2.3 From eafb68b32160c73646a6d067b9352d05ceaf5023 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 17 Feb 2013 16:23:30 +0100 Subject: - Fix regression in handling LDAP contact identifiers (#1488959) --- CHANGELOG | 1 + program/js/app.js | 4 ++-- program/steps/addressbook/func.inc | 23 +++++++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) (limited to 'program/steps/addressbook') diff --git a/CHANGELOG b/CHANGELOG index de14e445d..d7c211dae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Fix regression in handling LDAP contact identifiers (#1488959) - Updated translations from Transifex - Fix buggy error template in a frame (#1488938) - Add addressbook widget on compose page in classic skin diff --git a/program/js/app.js b/program/js/app.js index 4ecbda706..65ad8a3c3 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -4225,7 +4225,7 @@ function rcube_webmail() this.group_member_change('add', cid, dest, to.id); else { var lock = this.display_message(this.get_label('copyingcontact'), 'loading'), - post_data = {_cid: cid, _source: source, _to: dest, _togid: to.id, _gid: group}; + post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group}; this.http_post('copy', post_data, lock); } @@ -4233,7 +4233,7 @@ function rcube_webmail() // target is an addressbook else if (to.id != source) { var lock = this.display_message(this.get_label('copyingcontact'), 'loading'), - post_data = {_cid: cid, _source: source, _to: to.id, _gid: group}; + post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group}; this.http_post('copy', post_data, lock); } diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index f6d2ae519..9b01ebaa3 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -769,20 +769,27 @@ function rcmail_get_cids($filter = null) return array(); } - $cid = explode(',', $cid); - $result = array(); + $cid = explode(',', $cid); + $got_source = strlen($source); + $result = array(); // create per-source contact IDs array foreach ($cid as $id) { - // get source from decoded ID - if ($sep = strrpos($id, '-')) { - $contact_id = substr($id, 0, $sep); - $source_id = substr($id, $sep+1); - if (strlen($source_id)) { - $result[(string)$source_id][] = $contact_id; + // extract source ID from contact ID (it's there in search mode) + // see #1488959 and #1488862 for reference + if (!$got_source) { + if ($sep = strrpos($id, '-')) { + $contact_id = substr($id, 0, $sep); + $source_id = (string) substr($id, $sep+1); + if (strlen($source_id)) { + $result[$source_id][] = $contact_id; + } } } else { + if (substr($id, -($got_source+1)) == "-$source") { + $id = substr($id, 0, -($got_source+1)); + } $result[$source][] = $id; } } -- cgit v1.2.3 From d7e129e07df65b0603418b4ece926100397a8f66 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 6 Mar 2013 13:17:52 +0100 Subject: Increase maxlength to 254 chars for email input fields in addressbook (#1488987) --- CHANGELOG | 1 + program/steps/addressbook/func.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'program/steps/addressbook') diff --git a/CHANGELOG b/CHANGELOG index fa5fb8e24..33be5fbbd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Increase maxlength to 254 chars for email input fields in addressbook (#1488987) - Add attachment menu with Open and Download options (#1488975) - Fix thumbnail size when GD extension is used for image resize (#1488985) - Display user-friendly message on IMAP "over quota" errors (#1484164) diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 9b01ebaa3..ffc0b3b92 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -26,7 +26,7 @@ $CONTACT_COLTYPES = array( 'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('name'), 'category' => 'main'), 'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('firstname'), 'category' => 'main'), 'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('surname'), 'category' => 'main'), - 'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => rcube_label('email'), 'subtypes' => array('home','work','other'), 'category' => 'main'), + 'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 254, 'label' => rcube_label('email'), 'subtypes' => array('home','work','other'), 'category' => 'main'), 'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => rcube_label('middlename'), 'category' => 'main'), 'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => rcube_label('nameprefix'), 'category' => 'main'), 'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => rcube_label('namesuffix'), 'category' => 'main'), -- cgit v1.2.3 From a26c031621410f030751eae404490a10498583d7 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 24 Mar 2013 17:29:46 +0100 Subject: Fix vcard folding when outputing already generated vcard --- program/steps/addressbook/export.inc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'program/steps/addressbook') diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 15bf8b0d4..761f26b75 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -138,7 +138,9 @@ header('Content-Disposition: attachment; filename="contacts.vcf"'); while ($result && ($row = $result->next())) { // we already have a vcard record if ($row['vcard'] && $row['name']) { - $row['vcard'] = preg_replace('/\r?\n/', rcube_vcard::$eol, $row['vcard']); + // fix folding and end-of-line chars + $row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']); + $row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']); echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol; } // copy values into vcard object -- cgit v1.2.3 From 38c1951266b1fda074340be4a8eb840a60f9ac11 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 24 Mar 2013 18:00:17 +0100 Subject: Be less restrictive on vCard import, do not require FN when N exists --- program/lib/Roundcube/rcube_vcard.php | 4 +++- program/steps/addressbook/import.inc | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'program/steps/addressbook') diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index de28767f8..54bb9521d 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -491,7 +491,9 @@ class rcube_vcard if (preg_match('/^END:VCARD$/i', $line)) { // parse vcard $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap); - if (!empty($obj->displayname) || !empty($obj->email)) { + // FN and N is required by vCard format (RFC 2426) + // on import we can be less restrictive, let's addressbook decide + if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) { $out[] = $obj; } diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc index df07d64bc..72da15078 100644 --- a/program/steps/addressbook/import.inc +++ b/program/steps/addressbook/import.inc @@ -209,6 +209,15 @@ if (is_array($_FILES['_file'])) { foreach ($vcards as $vcard) { $a_record = $vcard->get_assoc(); + // Generate contact's display name (must be before validation), the same we do in save.inc + if (empty($a_record['name'])) { + $a_record['name'] = rcube_addressbook::compose_display_name($a_record, true); + // Reset it if equals to email address (from compose_display_name()) + if ($a_record['name'] == $a_record['email'][0]) { + $a_record['name'] = ''; + } + } + // skip invalid (incomplete) entries if (!$CONTACTS->validate($a_record, true)) { $IMPORT_STATS->invalid++; @@ -250,7 +259,7 @@ if (is_array($_FILES['_file'])) { if ($success) { $IMPORT_STATS->inserted++; - $IMPORT_STATS->names[] = $vcard->displayname ? $vcard->displayname : $email; + $IMPORT_STATS->names[] = $a_record['name'] ? $a_record['name'] : $email; } else { $IMPORT_STATS->errors++; -- cgit v1.2.3