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