5 commits - plugins/kolab_addressbook plugins/kolab_notes

Thomas Brüderli bruederli at kolabsys.com
Tue Jun 24 15:49:27 CEST 2014


 plugins/kolab_addressbook/kolab_addressbook.js         |  122 +------
 plugins/kolab_addressbook/kolab_addressbook.php        |  196 +++++++++--
 plugins/kolab_addressbook/lib/rcube_kolab_contacts.php |   18 +
 plugins/kolab_notes/kolab_notes.php                    |  289 +++++++++++++----
 plugins/kolab_notes/kolab_notes_ui.php                 |  134 ++++++-
 plugins/kolab_notes/localization/en_US.inc             |    6 
 plugins/kolab_notes/notes.js                           |   35 +-
 plugins/kolab_notes/skins/larry/folder_icons.png       |    1 
 plugins/kolab_notes/skins/larry/notes.css              |  136 ++++++--
 plugins/kolab_notes/skins/larry/sprites.png            |binary
 plugins/kolab_notes/skins/larry/templates/notes.html   |   40 ++
 11 files changed, 706 insertions(+), 271 deletions(-)

New commits:
commit b120d3958fc934d3377fb15c15af8f666b2b00c5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jun 24 15:07:48 2014 +0200

    New hierarchical folder navigation for address book (#3046)

diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js
index 79628e8..82b6d9d 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.js
+++ b/plugins/kolab_addressbook/kolab_addressbook.js
@@ -2,11 +2,12 @@
  * Client script for the Kolab address book plugin
  *
  * @author Aleksander Machniak <machniak at kolabsys.com>
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
  * @licstart  The following is the entire license notice for the
  * JavaScript code in this file.
  *
- * Copyright (C) 2011, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2011-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -220,124 +221,39 @@ rcube_webmail.prototype.book_delete_done = function(id, recur)
 };
 
 // action executed after book create/update
-rcube_webmail.prototype.book_update = function(data, old, recur)
+rcube_webmail.prototype.book_update = function(data, old)
 {
-    var n, i, id, len, link, row, prop, olddata, oldid, name, sources, level,
-        folders = [], classes = ['addressbook'],
-        groups = this.env.contactgroups;
+    var link, classes = [(data.group || ''), 'addressbook'];
 
-    this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
     this.show_contentframe(false);
 
-    // update (remove old row)
-    if (old && old != data.id) {
-        olddata = this.env.address_sources[old];
-        delete this.env.address_sources[old];
-        delete this.env.contactfolders[old];
-        this.treelist.remove(old);
-    }
-
-    sources = this.env.address_sources;
-
     // set row attributes
     if (data.readonly)
         classes.push('readonly');
-    if (data.class_name)
-        classes.push(data.class_name);
-    // updated currently selected book
-    if (this.env.source != '' && this.env.source == old) {
-        classes.push('selected');
-        this.env.source = data.id;
-    }
+    if (data.group)
+        classes.push(data.group);
 
     link = $('<a>').html(data.name)
       .attr({
-        href: '#', rel: data.id,
+        href: this.url('', { _source: data.id }),
+        rel: data.id,
         onclick: "return rcmail.command('list', '" + data.id + "', this)"
       });
 
-    // add row at the end of the list
-    // treelist widget is not very smart, we need
-    // to do sorting and add groups list by ourselves
-    this.treelist.insert({id: data.id, html:link, classes: classes, childlistclass: 'groups'}, '', false);
-    row = $(this.treelist.get_item(data.id));
-    row.append($('<ul class="groups">').hide());
-
-    // we need to sort rows because treelist can't sort by property
-    $.each(sources, function(i, v) {
-        if (v.kolab && v.realname)
-            folders.push(v.realname);
-    });
-    folders.sort();
-
-    for (n=0, len=folders.length; n<len; n++)
-        if (folders[n] == data.realname)
-           break;
-
-    // find the row before and re-insert after it
-    if (n && n < len - 1) {
-        name = folders[n-1];
-        for (n in sources)
-            if (sources[n].realname && sources[n].realname == name) {
-                row.detach().insertAfter(this.treelist.get_item(n));
-                break;
-            }
+    // update (remove old row)
+    if (old) {
+      this.treelist.update(old, { id: data.id, html:link, classes: classes, parent:(old != data.id ? data.parent : null) }, data.group || true);
+    }
+    else {
+      this.treelist.insert({ id: data.id, html:link, classes: classes, childlistclass: 'groups' }, data.parent, data.group || true);
     }
 
-    if (olddata) {
-        // update groups
-        for (n in groups) {
-            if (groups[n].source == old) {
-                prop = groups[n];
-                prop.type = 'group';
-                prop.source = data.id;
-                id = 'G' + prop.source + prop.id;
-
-                link = $('<a>').text(prop.name)
-                  .attr({
-                    href: '#', rel: prop.source + ':' + prop.id,
-                    onclick: "return rcmail.command('listgroup', {source: '"+prop.source+"', id: '"+prop.id+"'}, this)"
-                  });
-
-                this.treelist.insert({id:id, html:link, classes:['contactgroup']}, prop.source, true);
-
-                this.env.contactfolders[id] = this.env.contactgroups[id] = prop;
-                delete this.env.contactgroups[n];
-                delete this.env.contactfolders[n];
-            }
-        }
-
-        if (recur)
-            return;
-
-        // update subfolders
-        old += '_';
-        level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length;
-        olddata.realname += this.env.delimiter;
-
-        for (n in sources) {
-            if (sources[n].realname && sources[n].realname.indexOf(olddata.realname) == 0) {
-                prop = sources[n];
-                oldid = sources[n].id;
-                // new ID
-                prop.id = data.id + '_' + n.substr(old.length);
-                prop.realname = data.realname + prop.realname.substr(olddata.realname.length - 1);
-                name = prop.name;
-
-                // update display name
-                if (level > 0) {
-                    for (i=level; i>0; i--)
-                        name = name.replace(/^  /, '');
-                }
-                else if (level < 0) {
-                    for (i=level; i<0; i++)
-                        name = '  ' + name;
-                }
+    this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
 
-                prop.name = name;
-                this.book_update(prop, oldid, true)
-            }
-        }
+    // updated currently selected book
+    if (this.env.source != '' && this.env.source == old) {
+        this.treelist.select(data.id);
+        this.env.source = data.id;
     }
 };
 
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index f28d496..530580b 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -32,6 +32,7 @@ class kolab_addressbook extends rcube_plugin
     public $task = '?(?!login|logout).*';
 
     private $sources;
+    private $folders;
     private $rc;
     private $ui;
 
@@ -60,6 +61,7 @@ class kolab_addressbook extends rcube_plugin
         if ($this->rc->task == 'addressbook') {
             $this->add_texts('localization');
             $this->add_hook('contact_form', array($this, 'contact_form'));
+            $this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));
 
             // Plugin actions
             $this->register_action('plugin.book', array($this, 'book_actions'));
@@ -103,24 +105,9 @@ class kolab_addressbook extends rcube_plugin
         }
 
         $sources = array();
-        $names   = array();
-
         foreach ($this->_list_sources() as $abook_id => $abook) {
-            $name = kolab_storage::folder_displayname($abook->get_name(), $names);
-
             // register this address source
-            $sources[$abook_id] = array(
-                'id'       => $abook_id,
-                'name'     => $name,
-                'readonly' => $abook->readonly,
-                'editable' => $abook->editable,
-                'groups'   => $abook->groups,
-                'undelete' => $abook->undelete && $undelete,
-                'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
-                'class_name' => $abook->get_namespace(),
-                'carddavurl' => $abook->get_carddav_url(),
-                'kolab'    => true,
-            );
+            $sources[$abook_id] = $this->abook_prop($abook_id, $abook);
         }
 
         // Add personal address sources to the list
@@ -141,6 +128,139 @@ class kolab_addressbook extends rcube_plugin
         return $p;
     }
 
+    /**
+     * Helper method to build a hash array of address book properties
+     */
+    protected function abook_prop($id, $abook)
+    {
+        return array(
+            'id'       => $id,
+            'name'     => $abook->get_name(),
+            'listname' => $abook->get_foldername(),
+            'readonly' => $abook->readonly,
+            'editable' => $abook->editable,
+            'groups'   => $abook->groups,
+            'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'),
+            'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
+            'group'    => $abook->get_namespace(),
+            'carddavurl' => $abook->get_carddav_url(),
+            'kolab'    => true,
+        );
+    }
+
+    /**
+     *
+     */
+    public function directorylist_html($args)
+    {
+        $out = '';
+        $jsdata = array();
+        $sources = (array)$this->rc->get_address_sources();
+
+        // list all non-kolab sources first
+        foreach (array_filter($sources, function($source){ return empty($source['kolab']); }) as $j => $source) {
+            $id = strval(strlen($source['id']) ? $source['id'] : $j);
+            $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
+        }
+
+        // render a hierarchical list of kolab contact folders
+        kolab_storage::folder_hierarchy($this->folders, $tree);
+        $out .= $this->folder_tree_html($tree, $sources, $jsdata);
+
+        $this->rc->output->set_env('contactgroups', $jsdata);
+
+        $args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
+        return $args;
+    }
+
+    /**
+     * Return html for a structured list <ul> for the folder tree
+     */
+    public function folder_tree_html($node, $data, &$jsdata)
+    {
+        $out = '';
+        foreach ($node->children as $folder) {
+            $id = $folder->id;
+            $source = $data[$id];
+            $is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;
+
+            if ($folder->virtual) {
+                $source = array(
+                    'id'       => $folder->id,
+                    'name'     => $folder->get_name(),
+                    'listname' => $folder->get_foldername(),
+                    'group'    => $folder->get_namespace(),
+                    'readonly' => true,
+                    'editable' => false,
+                    'kolab'    => true,
+                    'virtual'  => true,
+                );
+            }
+
+            $content = $this->addressbook_list_item($id, $source, $jsdata);
+
+            if (!empty($folder->children)) {
+                $child_html = $this->folder_tree_html($folder, $data, $jsdata);
+
+                if (!empty($child_html) && preg_match('!</ul>\n*$!', $content)) {
+                    $content = preg_replace('!</ul>\n*$!', $child_html . '</ul>', $content);
+                }
+                else if (!empty($child_html)) {
+                    $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $child_html);
+                }
+            }
+
+            $out .= $content . '</li>';
+        }
+
+        return $out;
+    }
+
+    /**
+     *
+     */
+    protected function addressbook_list_item($id, $source, &$jsdata, $checkbox = false)
+    {
+        $folder = $this->folders[$id];
+        $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
+
+        // set class name(s)
+        $classes = array($source['group'] ?: '000', 'addressbook');
+        if ($current === $id)
+            $classes[] = 'selected';
+        if ($source['readonly'])
+            $classes[] = 'readonly';
+        if ($source['virtual'])
+            $classes[] = 'virtual';
+        if ($source['class_name'])
+            $classes[] = $source['class_name'];
+
+        $name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
+        $out .= html::tag('li', array(
+                'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
+                'class' => join(' ', $classes), 
+                'noclose' => true,
+            ),
+            ($source['virtual'] ?
+                html::a(array('tabindex' => '0'), $name) :
+                html::a(array(
+                        'href' => $this->rc->url(array('_source' => $id)),
+                        'rel' => $source['id'],
+                        'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
+                    ), $name)
+            )
+        );
+
+        $groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
+        if ($source['groups'] && function_exists('rcmail_contact_groups')) {
+            $groupdata = rcmail_contact_groups($groupdata);
+        }
+
+        $jsdata = $groupdata['jsdata'];
+        $out .= $groupdata['out'];
+
+        return $out;
+    }
 
     /**
      * Sets autocomplete_addressbooks option according to
@@ -203,6 +323,16 @@ class kolab_addressbook extends rcube_plugin
             if ($this->sources[$p['id']]) {
                 $p['instance'] = $this->sources[$p['id']];
             }
+            else {
+                $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id']));
+                if ($folder->type) {  // try with unencoded (old-style) identifier
+                    $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id'], false));
+                }
+                if ($folder->type) {
+                    $this->sources[$p['id']] = new rcube_kolab_contacts($folder->name);
+                    $p['instance'] = $this->sources[$p['id']];
+                }
+            }
         }
 
         return $p;
@@ -215,7 +345,9 @@ class kolab_addressbook extends rcube_plugin
         if (isset($this->sources))
             return $this->sources;
 
+        kolab_storage::$encode_ids = true;
         $this->sources = array();
+        $this->folders = array();
 
         $abook_prio = $this->addressbook_prio();
 
@@ -247,9 +379,10 @@ class kolab_addressbook extends rcube_plugin
             $names = array();
             foreach ($folders as $folder) {
                 // create instance of rcube_contacts
-                $abook_id = kolab_storage::folder_id($folder->name, false);
+                $abook_id = $folder->id;
                 $abook = new rcube_kolab_contacts($folder->name);
                 $this->sources[$abook_id] = $abook;
+                $this->folders[$abook_id] = $folder;
             }
         }
 
@@ -436,40 +569,19 @@ class kolab_addressbook extends rcube_plugin
         if ($result) {
             $storage = $this->rc->get_storage();
             $delimiter = $storage->get_hierarchy_delimiter();
-            $kolab_folder = new rcube_kolab_contacts($folder);
-
-            // create display name for the folder (see self::address_sources())
-            if (strpos($folder, $delimiter)) {
-                $names = array();
-                foreach ($this->_list_sources() as $abook) {
-                    $realname = $abook->get_realname();
-                    // The list can be not updated yet, handle old folder name
-                    if ($type == 'update' && $realname == $prop['oldname']) {
-                        $abook    = $kolab_folder;
-                        $realname = $folder;
-                    }
-
-                    $name = kolab_storage::folder_displayname($abook->get_name(), $names);
-
-                    if ($realname == $folder) {
-                        break;
-                    }
-                }
-            }
-            else {
-                $name = $kolab_folder->get_name();
-            }
+            $kolab_folder = kolab_storage::get_folder($folder);
 
             $this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
             $this->rc->output->command('set_env', 'delimiter', $delimiter);
             $this->rc->output->command('book_update', array(
                 'id'       => kolab_storage::folder_id($folder),
-                'name'     => $name,
+                'name'     => $kolab_folder->get_foldername(),
                 'readonly' => false,
                 'editable' => true,
                 'groups'   => true,
                 'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name
-                'class_name' => $kolab_folder->get_namespace(),
+                'group'    => $kolab_folder->get_namespace(),
+                'parent'   => kolab_storage::folder_id($kolab_folder->get_parent()),
                 'kolab'    => true,
             ), kolab_storage::folder_id($prop['oldname']));
 
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index ffce9b5..2e93d46 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -154,6 +154,14 @@ class rcube_kolab_contacts extends rcube_addressbook
         return $folder;
     }
 
+    /**
+     * Wrapper for kolab_storage_folder::get_foldername()
+     */
+    public function get_foldername()
+    {
+        return $this->storagefolder->get_foldername();
+    }
+
 
     /**
      * Getter for the IMAP folder name
@@ -181,6 +189,16 @@ class rcube_kolab_contacts extends rcube_addressbook
     }
 
     /**
+     * Getter for parent folder path
+     *
+     * @return string Full path to parent folder
+     */
+    public function get_parent()
+    {
+        return $this->storagefolder->get_parent();
+    }
+
+    /**
      * Compose an URL for CardDAV access to this address book (if configured)
      */
     public function get_carddav_url()


commit 8503b61374ded069505ed5cb9f149eb62fd94765
Merge: cad1ef8 c996445
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jun 24 14:55:09 2014 +0200

    Merge branch 'master' of ssh://git.kolab.org/git/roundcubemail-plugins-kolab



commit cad1ef89dee5ff16e1e11dcbb31cac532ca2a93c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jun 23 12:33:48 2014 +0200

    Only render list checkbox in search mode

diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 86039ab..ed50af7 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -805,7 +805,7 @@ class kolab_notes extends rcube_plugin
                     unset($prop['editname']);  // force full name to be displayed
 
                     // let the UI generate HTML and CSS representation for this calendar
-                    $html = $this->ui->folder_list_item($id, $prop, $jsenv);
+                    $html = $this->ui->folder_list_item($id, $prop, $jsenv, true);
                     $prop += (array)$jsenv[$id];
                     $prop['editname'] = $editname;
                     $prop['html'] = $html;
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index 45ddcbe..527e89b 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -161,7 +161,7 @@ class kolab_notes_ui
             $prop = $data[$id];
             $is_collapsed = false; // TODO: determine this somehow?
 
-            $content = $this->folder_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+            $content = $this->folder_list_item($id, $prop, $jsenv);
 
             if (!empty($folder->children)) {
                 $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
@@ -183,7 +183,7 @@ class kolab_notes_ui
     /**
      * Helper method to build a tasklist item (HTML content and js data)
      */
-    public function folder_list_item($id, $prop, &$jsenv, $activeonly = false)
+    public function folder_list_item($id, $prop, &$jsenv, $checkbox = false)
     {
         if (!$prop['virtual']) {
             unset($prop['user_id']);
@@ -212,7 +212,10 @@ class kolab_notes_ui
         return html::div(join(' ', $classes),
             html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
             ($prop['virtual'] ? '' :
-                html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
+                ($checkbox ?
+                    html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) :
+                    ''
+                ) .
                 html::span('handle', '') .
                 (isset($prop['subscribed']) ?
                     html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') :


commit 6b6f0cef3bcd37b24afbaca83b2d405248771331
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jun 23 10:45:29 2014 +0200

    Set jsenv in flat folder listing mode (#3056)

diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index f03607d..45ddcbe 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -124,9 +124,13 @@ class kolab_notes_ui
         else {
             $html = '';
             foreach ($lists as $prop) {
-                unset($prop['user_id']);
                 $id = $prop['id'];
 
+                if (!$prop['virtual']) {
+                    unset($prop['user_id']);
+                    $jsenv[$id] = $prop;
+                }
+
                 if ($attrib['type'] == 'select') {
                     if ($prop['editable']) {
                         $select->add($prop['name'], $prop['id']);


commit 7a85b2590e4a4b6b04c8b95c36e436ef6ed17834
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jun 23 10:37:56 2014 +0200

    Implement new folder navigation for notes module (#3056)

diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 114e92c..86039ab 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -106,6 +106,9 @@ class kolab_notes extends rcube_plugin
         if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || in_array($args['action'], array('folder-acl','dialog-ui')))) {
             $this->load_ui();
         }
+
+        // notes use fully encoded identifiers
+        kolab_storage::$encode_ids = true;
     }
 
     /**
@@ -155,87 +158,216 @@ class kolab_notes extends rcube_plugin
         }
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
-        $listnames = array();
+
+        foreach ($folders as $folder) {
+            $item = $this->folder_props($folder, $delim);
+            $this->lists[$item['id']] = $item;
+            $this->folders[$item['id']] = $folder;
+            $this->folders[$folder->name] = $folder;
+        }
+    }
+
+    /**
+     * Get a list of available folders from this source
+     */
+    public function get_lists(&$tree = null)
+    {
+        $this->_read_lists();
+
+        // attempt to create a default folder for this user
+        if (empty($this->lists)) {
+            $folder = array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true);
+            if (kolab_storage::folder_update($folder)) {
+                $this->_read_lists(true);
+            }
+        }
+
+        $folders = array();
+        foreach ($this->lists as $id => $list) {
+            if (!empty($this->folders[$id])) {
+                $folders[] = $this->folders[$id];
+            }
+        }
 
         // include virtual folders for a full folder tree
-        if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
-            $folders = kolab_storage::folder_hierarchy($folders);
+        if (!is_null($tree)) {
+            $folders = kolab_storage::folder_hierarchy($folders, $tree);
+        }
+
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
 
+        $lists = array();
         foreach ($folders as $folder) {
-            $utf7name = $folder->name;
+            $list_id = $folder->id;
+            $imap_path = explode($delim, $folder->name);
+
+            // find parent
+            do {
+              array_pop($imap_path);
+              $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+            }
+            while (count($imap_path) > 1 && !$this->folders[$parent_id]);
 
-            $path_imap = explode($delim, $utf7name);
-            $editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');  // pop off raw name part
-            $path_imap = join($delim, $path_imap);
+            // restore "real" parent ID
+            if ($parent_id && !$this->folders[$parent_id]) {
+                $parent_id = kolab_storage::folder_id($folder->get_parent());
+            }
 
             $fullname = $folder->get_name();
-            $listname = kolab_storage::folder_displayname($fullname, $listnames);
+            $listname = $folder->get_foldername();
 
             // special handling for virtual folders
-            if ($folder->virtual) {
-                $list_id = kolab_storage::folder_id($utf7name);
-                $this->lists[$list_id] = array(
-                    'id' => $list_id,
-                    'name' => $fullname,
+            if ($folder instanceof kolab_storage_folder_user) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => $fullname,
                     'listname' => $listname,
-                    'virtual' => true,
+                    'title'    => $folder->get_owner(),
+                    'virtual'  => true,
                     'editable' => false,
+                    'group'    => 'other virtual',
+                    'class'    => 'user',
+                    'parent'   => $parent_id,
                 );
-                continue;
             }
-
-            if ($folder->get_namespace() == 'personal') {
-                $norename = false;
-                $readonly = false;
-                $alarms = true;
+            else if ($folder->virtual) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => kolab_storage::object_name($fullname),
+                    'listname' => $listname,
+                    'virtual'  => true,
+                    'editable' => false,
+                    'group'    => $folder->get_namespace(),
+                    'parent'   => $parent_id,
+                );
             }
             else {
-                $alarms = false;
-                $readonly = true;
-                if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
-                    if (strpos($rights, 'i') !== false)
-                      $readonly = false;
+                if (!$this->lists[$list_id]) {
+                    $this->lists[$list_id] = $this->folder_props($folder, $delim);
+                    $this->folders[$list_id] = $folder;
                 }
-                $info = $folder->get_folder_info();
-                $norename = $readonly || $info['norename'] || $info['protected'];
+                $this->lists[$list_id]['parent'] = $parent_id;
+                $lists[$list_id] = $this->lists[$list_id];
             }
-
-            $list_id = kolab_storage::folder_id($utf7name);
-            $item = array(
-                'id' => $list_id,
-                'name' => $fullname,
-                'listname' => $listname,
-                'editname' => $editname,
-                'editable' => !$readonly,
-                'norename' => $norename,
-                'parentfolder' => $path_imap,
-                'default' => $folder->default,
-                'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
-            );
-            $this->lists[$item['id']] = $item;
-            $this->folders[$item['id']] = $folder;
-            $this->folders[$folder->name] = $folder;
         }
+
+        return $lists;
     }
 
     /**
-     * Get a list of available folders from this source
+     * Search for shared or otherwise not listed folders the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of notes folders
      */
-    public function get_lists()
+    protected function search_lists($query, $source)
     {
-        $this->_read_lists();
+        if (!kolab_storage::setup()) {
+            return array();
+        }
 
-        // attempt to create a default folder for this user
-        if (empty($this->lists)) {
-            $folder = array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true);
-            if (kolab_storage::folder_update($folder)) {
-                $this->_read_lists(true);
+        $this->search_more_results = false;
+        $this->lists = $this->folders = array();
+
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+
+        // find unsubscribed IMAP folders that have "event" type
+        if ($source == 'folders') {
+            foreach ((array)kolab_storage::search_folders('note', $query, array('other')) as $folder) {
+                $this->folders[$folder->id] = $folder;
+                $this->lists[$folder->id] = $this->folder_props($folder, $delim);
+            }
+        }
+        // search other user's namespace via LDAP
+        else if ($source == 'users') {
+            $limit = $this->rc->config->get('autocomplete_max', 15) * 2;  // we have slightly more space, so display twice the number
+            foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
+                $folders = array();
+                // search for tasks folders shared by this user
+                foreach (kolab_storage::list_user_folders($user, 'note', false) as $foldername) {
+                    $folders[] = new kolab_storage_folder($foldername, 'note');
+                }
+
+                if (count($folders)) {
+                    $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
+                    $this->folders[$userfolder->id] = $userfolder;
+                    $this->lists[$userfolder->id] = $this->folder_props($userfolder, $delim, array());
+
+                    foreach ($folders as $folder) {
+                        $this->folders[$folder->id] = $folder;
+                        $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+                        $count++;
+                    }
+                }
+
+                if ($count >= $limit) {
+                    $this->search_more_results = true;
+                    break;
+                }
             }
+
         }
 
-        return $this->lists;
+        return $this->get_lists();
     }
 
+    /**
+     * Derive list properties from the given kolab_storage_folder object
+     */
+    protected function folder_props($folder, $delim)
+    {
+        if ($folder->get_namespace() == 'personal') {
+            $norename = false;
+            $readonly = false;
+            $alarms = true;
+        }
+        else {
+            $alarms = false;
+            $readonly = true;
+            if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
+                if (strpos($rights, 'i') !== false)
+                  $readonly = false;
+            }
+            $info = $folder->get_folder_info();
+            $norename = $readonly || $info['norename'] || $info['protected'];
+        }
+
+        $list_id = $folder->id;
+        return array(
+            'id' => $list_id,
+            'name' => $folder->get_name(),
+            'listname' => $folder->get_foldername(),
+            'editname' => $folder->get_foldername(),
+            'editable' => !$readonly,
+            'norename' => $norename,
+            'parentfolder' => $folder->get_parent(),
+            'subscribed' => (bool)$folder->is_subscribed(),
+            'default'  => $folder->default,
+            'group'    => $folder->default ? 'default' : $folder->get_namespace(),
+            'class'    => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
+        );
+    }
+
+    /**
+     * Get the kolab_calendar instance for the given calendar ID
+     *
+     * @param string List identifier (encoded imap folder name)
+     * @return object kolab_storage_folder Object nor null if list doesn't exist
+     */
+    public function get_folder($id)
+    {
+        // create list and folder instance if necesary
+        if (!$this->lists[$id]) {
+            $folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
+            if ($folder->type) {
+                $this->folders[$id] = $folder;
+                $this->lists[$id] = $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter());
+            }
+        }
+
+        return $this->folders[$id];
+    }
 
     /*******  UI functions  ********/
 
@@ -325,7 +457,7 @@ class kolab_notes extends rcube_plugin
         }
 
         $this->_read_lists();
-        if ($folder = $this->folders[$list_id]) {
+        if ($folder = $this->get_folder($list_id)) {
             foreach ($folder->select($query) as $record) {
                 // post-filter search results
                 if (strlen($search)) {
@@ -393,7 +525,7 @@ class kolab_notes extends rcube_plugin
 
         $this->_read_lists();
         if ($list_id) {
-            if ($folder = $this->folders[$list_id]) {
+            if ($folder = $this->get_folder($list_id)) {
                 return $folder->get_object($uid);
             }
         }
@@ -514,11 +646,11 @@ class kolab_notes extends rcube_plugin
         $this->_read_lists();
 
         $list_id = $note['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // moved from another folder
-        if ($note['_fromlist'] && ($fromfolder = $this->folders[$note['_fromlist']])) {
+        if ($note['_fromlist'] && ($fromfolder = $this->get_folder($note['_fromlist']))) {
             if (!$fromfolder->move($note['uid'], $folder->name))
                 return false;
 
@@ -566,8 +698,8 @@ class kolab_notes extends rcube_plugin
     function move_note($note, $list_id)
     {
         $this->_read_lists();
-        $tofolder = $this->folders[$list_id];
-        $fromfolder = $this->folders[$note['list']];
+        $tofolder = $this->get_folder($list_id);
+        $fromfolder = $this->get_folder($note['list']);
 
         if ($fromfolder && $tofolder) {
             return $fromfolder->move($note['uid'], $tofolder->name);
@@ -588,7 +720,7 @@ class kolab_notes extends rcube_plugin
         $this->_read_lists();
 
         $list_id = $note['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         return $folder->delete($note['uid'], $force);
@@ -603,6 +735,10 @@ class kolab_notes extends rcube_plugin
         $list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC, true);
         $success = $update_cmd = false;
 
+        if (empty($action)) {
+            $action = rcube_utils::get_input_value('action', RCUBE_INPUT_GPC);
+        }
+
         switch ($action) {
             case 'form-new':
             case 'form-edit':
@@ -651,7 +787,7 @@ class kolab_notes extends rcube_plugin
 
             case 'delete':
                 $this->_read_lists();
-                $folder = $this->folders[$list['id']];
+                $folder = $this->get_folder($list['id']);
                 if ($folder && kolab_storage::folder_delete($folder->name)) {
                     $success = true;
                     $update_cmd = 'plugin.destroy_list';
@@ -660,6 +796,39 @@ class kolab_notes extends rcube_plugin
                     $save_error = $this->gettext(kolab_storage::$last_error);
                 }
                 break;
+
+            case 'search':
+                $this->load_ui();
+                $results = array();
+                foreach ((array)$this->search_lists(rcube_utils::get_input_value('q', RCUBE_INPUT_GPC), rcube_utils::get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+                    $editname = $prop['editname'];
+                    unset($prop['editname']);  // force full name to be displayed
+
+                    // let the UI generate HTML and CSS representation for this calendar
+                    $html = $this->ui->folder_list_item($id, $prop, $jsenv);
+                    $prop += (array)$jsenv[$id];
+                    $prop['editname'] = $editname;
+                    $prop['html'] = $html;
+
+                    $results[] = $prop;
+                }
+                // report more results available
+                if ($this->driver->search_more_results) {
+                    $this->rc->output->show_message('autocompletemore', 'info');
+                }
+
+                $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', RCUBE_INPUT_GPC));
+                return;
+
+            case 'subscribe':
+                $success = false;
+                if ($list['id'] && ($folder = $this->get_folder($list['id']))) {
+                    if (isset($list['permanent']))
+                        $success |= $folder->subscribe(intval($list['permanent']));
+                    if (isset($list['active']))
+                        $success |= $folder->activate(intval($list['active']));
+                }
+                break;
         }
 
         $this->rc->output->command('plugin.unlock_saving');
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index 143bc54..f03607d 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -57,6 +57,11 @@ class kolab_notes_ui
         $this->plugin->include_script('notes.js');
         $this->plugin->include_script('jquery.tagedit.js');
 
+        // include kolab folderlist widget if available
+        if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
+            $this->plugin->api->include_script('libkolab/js/folderlist.js');
+        }
+
         $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
 
         // load config options and user prefs relevant for the UI
@@ -109,44 +114,110 @@ class kolab_notes_ui
             $select = new html_select($attrib);
         }
 
+        $tree = $attrib['type'] != 'select' ? true : null;
+        $lists = $this->plugin->get_lists($tree);
         $jsenv = array();
-        $items = '';
-        foreach ($this->plugin->get_lists() as $prop) {
-            unset($prop['user_id']);
-            $id = $prop['id'];
-            $class = '';
 
-            if (!$prop['virtual'])
-                $jsenv[$id] = $prop;
-
-            if ($attrib['type'] == 'select') {
-                if ($prop['editable']) {
-                    $select->add($prop['name'], $prop['id']);
+        if (is_object($tree)) {
+            $html = $this->folder_tree_html($tree, $lists, $jsenv, $attrib);
+        }
+        else {
+            $html = '';
+            foreach ($lists as $prop) {
+                unset($prop['user_id']);
+                $id = $prop['id'];
+
+                if ($attrib['type'] == 'select') {
+                    if ($prop['editable']) {
+                        $select->add($prop['name'], $prop['id']);
+                    }
+                }
+                else {
+                    $html .= html::tag('li', array('id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group']),
+                        $this->folder_list_item($id, $prop, $jsenv)
+                    );
                 }
-            }
-            else {
-                $html_id = rcube_utils::html_identifier($id);
-                $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
-                if ($prop['virtual'])
-                    $class .= ' virtual';
-                else if (!$prop['editable'])
-                    $class .= ' readonly';
-                if ($prop['class_name'])
-                    $class .= ' '.$prop['class_name'];
-
-                $attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
-                $items .= html::tag('li', array('id' => 'rcmliknb' . $html_id, 'class' => trim($class)),
-                    html::a($attr + array('class' => 'listname', 'title' => $title), $prop['listname']) .
-                    html::span(array('class' => 'count'), '')
-                );
             }
         }
 
         $this->rc->output->set_env('kolab_notebooks', $jsenv);
         $this->rc->output->add_gui_object('notebooks', $attrib['id']);
 
-        return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $items, html::$common_attrib);
+        return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib);
+    }
+
+    /**
+     * Return html for a structured list <ul> for the folder tree
+     */
+    public function folder_tree_html($node, $data, &$jsenv, $attrib)
+    {
+        $out = '';
+        foreach ($node->children as $folder) {
+            $id = $folder->id;
+            $prop = $data[$id];
+            $is_collapsed = false; // TODO: determine this somehow?
+
+            $content = $this->folder_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+            if (!empty($folder->children)) {
+                $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+                    $this->folder_tree_html($folder, $data, $jsenv, $attrib));
+            }
+
+            if (strlen($content)) {
+                $out .= html::tag('li', array(
+                      'id' => 'rcmliknb' . rcube_utils::html_identifier($id),
+                      'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+                    ),
+                    $content);
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Helper method to build a tasklist item (HTML content and js data)
+     */
+    public function folder_list_item($id, $prop, &$jsenv, $activeonly = false)
+    {
+        if (!$prop['virtual']) {
+            unset($prop['user_id']);
+            $jsenv[$id] = $prop;
+        }
+
+        $classes = array('folder');
+        if ($prop['virtual']) {
+            $classes[] = 'virtual';
+        }
+        else if (!$prop['editable']) {
+            $classes[] = 'readonly';
+        }
+        if ($prop['subscribed']) {
+            $classes[] = 'subscribed';
+        }
+        if ($prop['class']) {
+            $classes[] = $prop['class'];
+        }
+
+        $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+          html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+        $label_id = 'nl:' . $id;
+        $attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
+        return html::div(join(' ', $classes),
+            html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
+            ($prop['virtual'] ? '' :
+                html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
+                html::span('handle', '') .
+                (isset($prop['subscribed']) ?
+                    html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') :
+                    ''
+                )
+            )
+        );
+
+        return '';
     }
 
     public function listing($attrib)
diff --git a/plugins/kolab_notes/localization/en_US.inc b/plugins/kolab_notes/localization/en_US.inc
index 74e6537..3816c0f 100644
--- a/plugins/kolab_notes/localization/en_US.inc
+++ b/plugins/kolab_notes/localization/en_US.inc
@@ -28,6 +28,11 @@ $labels['unsavedchanges'] = 'Unsaved Changes!';
 $labels['appendnote'] = 'Add a Note';
 $labels['editnote'] = 'Edit Note';
 $labels['savein'] = 'Save in';
+$labels['foldersubscribe'] = 'List permanently';
+$labels['findnotebooks'] = 'Find notebooks...';
+$labels['listsearchresults'] = 'Additional notebooks';
+$labels['nrnotebooksfound'] = '$nr notebooks found';
+$labels['nonotebooksfound'] = 'No notebooks found';
 
 $labels['savingdata'] = 'Saving data...';
 $labels['recordnotfound'] = 'Record not found';
@@ -48,3 +53,4 @@ $labels['arialabelnotessortmenu'] = 'Notes list sorting options';
 $labels['arialabelnotesoptionsmenu'] = 'Notebook actions menu';
 $labels['arialabelnotebookform'] = 'Notebook properties';
 $labels['arialabelmessagereferences'] = 'Linked email messages';
+$labels['arialabelfolderearchform'] = 'Notebooks search form';
diff --git a/plugins/kolab_notes/notes.js b/plugins/kolab_notes/notes.js
index 5f29ba4..5d903d1 100644
--- a/plugins/kolab_notes/notes.js
+++ b/plugins/kolab_notes/notes.js
@@ -86,14 +86,21 @@ function rcube_kolab_notes_ui(settings)
         // initialize folder selectors
         var li, id;
         for (id in me.notebooks) {
-            if (me.notebooks[id].editable && (!settings.selected_list || (me.notebooks[id].active && !me.notebooks[me.selected_list].active))) {
+            if (me.notebooks[id].editable && !settings.selected_list) {
                 settings.selected_list = id;
             }
         }
 
-        notebookslist = new rcube_treelist_widget(rcmail.gui_objects.notebooks, {
+        var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+        notebookslist = new widget_class(rcmail.gui_objects.notebooks, {
           id_prefix: 'rcmliknb',
+          save_state: true,
           selectable: true,
+          keyboard: false,
+          searchbox: '#notebooksearch',
+          search_action: 'notes/list',
+          search_sources: [ 'folders', 'users' ],
+          search_title: rcmail.gettext('listsearchresults','kolab_notes'),
           check_droptarget: function(node) {
               var list = me.notebooks[node.id];
               return !node.virtual && list.editable && node.id != me.selected_list;
@@ -112,8 +119,30 @@ function rcube_kolab_notes_ui(settings)
                 });
             }
         });
+        notebookslist.addEventListener('subscribe', function(p) {
+            var list;
+            if ((list = me.notebooks[p.id])) {
+                list.subscribed = p.subscribed || false;
+                rcmail.http_post('list', { _do:'subscribe', _list:{ id:p.id, permanent:list.subscribed?1:0 } });
+            }
+        });
+        notebookslist.addEventListener('insert-item', function(p) {
+            var list = p.data;
+            if (list && list.id && !list.virtual) {
+                me.notebooks[list.id] = list;
+                var prop = { id:p.id, active:list.active?1:0 };
+                if (list.subscribed) prop.permanent = 1;
+                rcmail.http_post('list', { _do:'subscribe', _list:prop });
+            }
+        });
+        notebookslist.addEventListener('search-complete', function(data) {
+            if (data.length)
+                rcmail.display_message(rcmail.gettext('nrnotebooksfound','kolab_notes').replace('$nr', data.length), 'voice');
+            else
+                rcmail.display_message(rcmail.gettext('nonotebooksfound','kolab_notes'), 'info');
+        });
 
-        $(rcmail.gui_objects.notebooks).on('click', 'li a', function(e) {
+        $(rcmail.gui_objects.notebooks).on('click', 'div.folder > a.listname', function(e) {
             var id = String($(this).closest('li').attr('id')).replace(/^rcmliknb/, '');
             notebookslist.select(id);
             e.preventDefault();
diff --git a/plugins/kolab_notes/skins/larry/folder_icons.png b/plugins/kolab_notes/skins/larry/folder_icons.png
deleted file mode 120000
index 2a6ab2b..0000000
--- a/plugins/kolab_notes/skins/larry/folder_icons.png
+++ /dev/null
@@ -1 +0,0 @@
-../../../kolab_addressbook/skins/larry/folder_icons.png
\ No newline at end of file
diff --git a/plugins/kolab_notes/skins/larry/notes.css b/plugins/kolab_notes/skins/larry/notes.css
index d57c2f8..40971fa 100644
--- a/plugins/kolab_notes/skins/larry/notes.css
+++ b/plugins/kolab_notes/skins/larry/notes.css
@@ -293,71 +293,141 @@
 	bottom: 0;
 }
 
+.notesview #notebooksbox .scroller {
+	top: 34px;
+}
+
 .notesview #notebooks li {
 	margin: 0;
-	height: 20px;
-	padding: 6px 8px 2px 6px;
 	display: block;
 	position: relative;
-	white-space: nowrap;
 }
 
-.notesview #notebooks li.virtual {
-	height: 12px;
+.notesview #notebooks li > div.folder {
+	position: relative;
+	padding: 0;
+	height: 28px;
+}
+
+.notesview #notebooks li.virtual > div.folder {
+	height: 22px;
 }
 
 .notesview #notebooks li .listname {
 	display: block;
-	position: absolute;
-	top: 1px;
-	left: 2px;
-	right: 6px;
-	height: 19px;
 	cursor: default;
-	padding: 4px 26px 2px 6px;
+	margin: 0;
+	height: 24px;
+	padding-bottom: 0;
+	padding-right: 26px;
 	color: #004458;
 	overflow: hidden;
 	text-overflow: ellipsis;
 	white-space: nowrap;
 }
 
-.notesview #notebooks li.virtual .listname {
+.notesview #notebooks li.virtual > div > .listname {
 	color: #aaa;
-	top: 0;
-	padding: 1px 8px;
+	height: 18px;
+	padding-top: 3px;
+}
+
+.notesview #notebooksbox .treelist li a.subscribed {
+	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 5px;
+	height: 16px;
+	width: 16px;
+	padding: 0;
+	background: url(sprites.png) -100px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
+	cursor: pointer;
+}
+
+.notesview #notebooksbox .treelist div > a.subscribed:focus,
+.notesview #notebooksbox .treelist div:hover > a.subscribed {
+	background-position: 2px -266px;
+}
+
+.notesview #notebooksbox .treelist div.subscribed a.subscribed {
+	background-position: -16px -266px;
+}
+
+.notesview #notebooksbox .treelist li a.subscribed:focus {
+	border-radius: 3px;
+	outline: 2px solid rgba(30,150,192, 0.5);
+}
+
+.notesview #notebooksbox .treelist input {
+	position: absolute;
+	top: 4px;
+	left: 14px;
 }
 
-.notesview #notebooks li.readonly,
-.notesview #notebooks li.shared,
-.notesview #notebooks li.other {
-	background-image: url('folder_icons.png');
-	background-position: right -1000px;
-	background-repeat: no-repeat;
+.notesview #notebooks input {
+	display: none;
 }
 
-.notesview #notebooks li.readonly {
-	background-position: 98% -21px;
+.notesview #notebooksbox .searchresults a.listname {
+	padding-left: 36px;
+}
+
+.notesview #notebooks div.folder span.handle {
+	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 26px;
+	height: 16px;
+	width: 30px;
+	padding: 0;
+	background: url('sprites.png') right -1000px no-repeat;
 }
 
-.notesview #notebooks li.other {
-	background-position: 98% -52px;
+.notesview #notebooks div.readonly span.handle {
+	background-position: right -192px;
 }
 
-.notesview #notebooks li.other.readonly {
-	background-position: 98% -77px;
+.notesview #notebooks div.other span.handle {
+	background-position: right -210px;
 }
 
-.notesview #notebooks li.shared {
-	background-position: 98% -103px;
+.notesview #notebooks div.other.readonly span.handle {
+	background-position: right -228px;
 }
 
-.notesview #notebooks li.shared.readonly {
-	background-position: 98% -130px;
+.notesview #notebooks div.shared span.handle {
+	background-position: right -246px;
 }
 
-.notesview #notebooks li.other.readonly .listname,
-.notesview #notebooks li.shared.readonly .listname {
-	padding-right: 36px;
+.notesview #notebooks div.other .listname,
+.notesview #notebooks div.shared .listname,
+.notesview #notebooks div.readonly .listname {
+	padding-right: 46px;
+}
+
+.notesview #notebooks div.other.readonly .listname {
+	padding-right: 56px;
+}
+
+.notesview #notebooksbox .searchresults {
+	background: #b0ccd7;
+	margin-top: 8px;
+}
+
+.notesview #notebooksbox .searchresults  .boxtitle {
+	background: none;
+	padding: 2px 8px 2px 8px;
+}
+
+.notesview #notebooksbox .boxtitle a.iconbutton.search {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+	width: 16px;
+	cursor: pointer;
+	background-position: -2px -317px;
 }
 
 .notesview .uidialog .tabbed {
diff --git a/plugins/kolab_notes/skins/larry/sprites.png b/plugins/kolab_notes/skins/larry/sprites.png
index 62aff65..ceead25 100644
Binary files a/plugins/kolab_notes/skins/larry/sprites.png and b/plugins/kolab_notes/skins/larry/sprites.png differ
diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html
index 1647da3..6f6fec6 100644
--- a/plugins/kolab_notes/skins/larry/templates/notes.html
+++ b/plugins/kolab_notes/skins/larry/templates/notes.html
@@ -38,7 +38,18 @@
         </div>
 
         <div id="notebooksbox" class="uibox listbox" role="navigation" aria-labelledby="aria-label-notebooks">
-            <h2 class="boxtitle" id="aria-label-notebooks"><roundcube:label name="kolab_notes.lists" /></h2>
+            <h2 class="boxtitle" id="aria-label-notebooks"><roundcube:label name="kolab_notes.lists" />
+                <a href="#notebooks" class="iconbutton search" title="<roundcube:label name='kolab_notes.findnotebooks' />" tabindex="0"><roundcube:label name="kolab_notes.findnotebooks" /></a>
+            </h2>
+            <div class="listsearchbox" style="display:none">
+                <div class="searchbox" role="search" aria-labelledby="aria-label-notebooksearchform" aria-controls="kolabnoteslist">
+                    <h3 id="aria-label-notebooksearchform" class="voice"><roundcube:label name="kolab_notes.arialabelfolderearchform" /></h3>
+                    <label for="notebooksearch" class="voice"><roundcube:label name="kolab_notes.searchterms" /></label>
+                    <input type="text" name="q" id="notebooksearch" placeholder="<roundcube:label name='kolab_notes.findnotebooks' />" />
+                    <a class="iconbutton searchicon"></a>
+                    <roundcube:button command="reset-listsearch" id="notebooksearch-reset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
+                </div>
+            </div>
             <div class="scroller withfooter">
                 <roundcube:object name="plugin.notebooks" id="notebooks" class="listing treelist" />
             </div>
@@ -152,6 +163,33 @@ $(document).ready(function(e){
     $(window).resize(function(e){
         layout_view();
     });
+
+    // animation to unfold list search box
+    $('#notebooksbox .boxtitle a.search').click(function(e){
+        var box = $('#notebooksbox .listsearchbox'),
+            dir = box.is(':visible') ? -1 : 1;
+
+        box.slideToggle({
+            duration: 160,
+            progress: function(animation, progress) {
+                if (dir < 0) progress = 1 - progress;
+                $('#notebooksbox .scroller').css('top', (34 + 34 * progress) + 'px');
+            },
+            complete: function() {
+                box.toggleClass('expanded');
+                if (box.is(':visible')) {
+                    box.find('input[type=text]').focus();
+                }
+                else {
+                    $('#notebooksearch-reset').click();
+                }
+                // TODO: save state in localStorage
+            }
+        });
+
+        return false;
+    });
+    
 });
 
 </script>




More information about the commits mailing list