Merge branch 'dev/kolab-notes'

This commit is contained in:
Thomas Bruederli 2014-04-10 12:14:21 +02:00
commit 5d126a296c
30 changed files with 3301 additions and 216 deletions

View file

@ -29,7 +29,7 @@
class kolab_addressbook extends rcube_plugin
{
public $task = 'mail|settings|addressbook|calendar|tasks';
public $task = '?(?!login|logout).*';
private $sources;
private $rc;

View file

@ -0,0 +1 @@
../tasklist/jquery.tagedit.js

View file

@ -0,0 +1,797 @@
<?php
/**
* Kolab notes module
*
* Adds simple notes management features to the web client
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@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
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_notes extends rcube_plugin
{
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('kolab_notes_sort_col');
public $rc;
private $ui;
private $lists;
private $folders;
private $cache = array();
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$this->require_plugin('libkolab');
$this->rc = rcube::get_instance();
$this->register_task('notes');
// load plugin configuration
$this->load_config();
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the notes module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('notes_disabled', false) || !$this->rc->config->get('notes_enabled', true)) {
return;
}
// load localizations
$this->add_texts('localization/', $args['task'] == 'notes' && !$args['action']);
$this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'notes') {
// register task actions
$this->register_action('index', array($this, 'notes_view'));
$this->register_action('fetch', array($this, 'notes_fetch'));
$this->register_action('get', array($this, 'note_record'));
$this->register_action('action', array($this, 'note_action'));
$this->register_action('list', array($this, 'list_action'));
}
else if ($args['task'] == 'mail') {
$this->add_hook('message_compose', array($this, 'mail_message_compose'));
}
if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'folder-acl')) {
require_once($this->home . '/kolab_notes_ui.php');
$this->ui = new kolab_notes_ui($this);
$this->ui->init();
}
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists($force = false)
{
// already read sources
if (isset($this->lists) && !$force)
return $this->lists;
// get all folders that have type "task"
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('note'));
$this->lists = $this->folders = array();
// find default folder
$default_index = 0;
foreach ($folders as $i => $folder) {
if ($folder->default)
$default_index = $i;
}
// put default folder on top of the list
if ($default_index > 0) {
$default_folder = $folders[$default_index];
unset($folders[$default_index]);
array_unshift($folders, $default_folder);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$listnames = array();
// 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);
foreach ($folders as $folder) {
$utf7name = $folder->name;
$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);
$fullname = $folder->get_name();
$listname = kolab_storage::folder_displayname($fullname, $listnames);
// 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,
'listname' => $listname,
'virtual' => true,
'editable' => false,
);
continue;
}
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 = 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;
}
}
/**
* Get a list of available folders from this source
*/
public function get_lists()
{
$this->_read_lists();
// attempt to create a default folder for this user
if (empty($this->lists)) {
#if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
# $this->_read_lists(true);
}
return $this->lists;
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function notes_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('kolab_notes.notes');
}
/**
* Handler to retrieve note records for the given list and/or search query
*/
public function notes_fetch()
{
$search = rcube_utils::get_input_value('_q', RCUBE_INPUT_GPC, true);
$list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC);
$data = $this->notes_data($this->list_notes($list, $search), $tags);
$this->rc->output->command('plugin.data_ready', array('list' => $list, 'search' => $search, 'data' => $data, 'tags' => array_values($tags)));
}
/**
* Convert the given note records for delivery to the client
*/
protected function notes_data($records, &$tags)
{
$tags = array();
foreach ($records as $i => $rec) {
unset($records[$i]['description']);
$this->_client_encode($records[$i]);
foreach ((array)$rec['categories'] as $tag) {
$tags[] = $tag;
}
}
$tags = array_unique($tags);
return $records;
}
/**
* Read note records for the given list from the storage backend
*/
protected function list_notes($list_id, $search = null)
{
$results = array();
// query Kolab storage
$query = array();
// full text search (only works with cache enabled)
if (strlen($search)) {
$words = array_filter(rcube_utils::normalize_string(mb_strtolower($search), true));
foreach ($words as $word) {
if (strlen($word) > 2) { // only words > 3 chars are stored in DB
$query[] = array('words', '~', $word);
}
}
}
$this->_read_lists();
if ($folder = $this->folders[$list_id]) {
foreach ($folder->select($query) as $record) {
// post-filter search results
if (strlen($search)) {
$matches = 0;
$contents = mb_strtolower(
$record['title'] .
($this->is_html($record) ? strip_tags($record['description']) : $record['description']) .
join(' ', (array)$record['categories'])
);
foreach ($words as $word) {
if (mb_strpos($contents, $word) !== false) {
$matches++;
}
}
// skip records not matching all search words
if ($matches < count($words)) {
continue;
}
}
$record['list'] = $list_id;
$results[] = $record;
}
}
return $results;
}
/**
* Handler for delivering a full note record to the client
*/
public function note_record()
{
$data = $this->get_note(array(
'uid' => rcube_utils::get_input_value('_id', RCUBE_INPUT_GPC),
'list' => rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC),
));
// encode for client use
if (is_array($data)) {
$this->_client_encode($data);
}
$this->rc->output->command('plugin.render_note', $data);
}
/**
* Get the full note record identified by the given UID + Lolder identifier
*/
public function get_note($note)
{
if (is_array($note)) {
$uid = $note['uid'] ?: $note['id'];
$list_id = $note['list'];
}
else {
$uid = $note;
}
// deliver from in-memory cache
$key = $list_id . ':' . $uid;
if ($this->cache[$key]) {
return $this->cache[$key];
}
$this->_read_lists();
if ($list_id) {
if ($folder = $this->folders[$list_id]) {
return $folder->get_object($uid);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->folders as $list_id => $folder) {
if ($result = $folder->get_object($uid)) {
$result['list'] = $list_id;
return $result;
}
}
}
return false;
}
/**
* Helper method to encode the given note record for use in the client
*/
private function _client_encode(&$note)
{
foreach ($note as $key => $prop) {
if ($key[0] == '_' || $key == 'x-custom') {
unset($note[$key]);
}
}
foreach (array('created','changed') as $key) {
if (is_object($note[$key]) && $note[$key] instanceof DateTime) {
$note[$key.'_'] = $note[$key]->format('U');
$note[$key] = $this->rc->format_date($note[$key]);
}
}
// clean HTML contents
if (!empty($note['description']) && $this->is_html($note)) {
$note['html'] = $this->_wash_html($note['description']);
}
return $note;
}
/**
* Handler for client-initiated actions on a single note record
*/
public function note_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_POST);
$note = rcube_utils::get_input_value('_data', RCUBE_INPUT_POST, true);
$success = false;
switch ($action) {
case 'new':
$temp_id = $rec['tempid'];
case 'edit':
if ($success = $this->save_note($note)) {
$refresh = $this->get_note($note);
$refresh['tempid'] = $temp_id;
}
break;
case 'move':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->move_note($note, $note['to']))) {
$refresh = $this->get_note($note);
break;
}
}
break;
case 'delete':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->delete_note($note))) {
$refresh = $this->get_note($note);
break;
}
}
break;
}
// show confirmation/error message
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$this->rc->output->show_message('errorsaving', 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
$this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
}
}
/**
* Update an note record with the given data
*
* @param array Hash array with note properties (id, list)
* @return boolean True on success, False on error
*/
private function save_note(&$note)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
// moved from another folder
if ($note['_fromlist'] && ($fromfolder = $this->folders[$note['_fromlist']])) {
if (!$fromfolder->move($note['uid'], $folder->name))
return false;
unset($note['_fromlist']);
}
// load previous version of this record to merge
if ($note['uid']) {
$old = $folder->get_object($note['uid']);
if (!$old || PEAR::isError($old))
return false;
// merge existing properties if the update isn't complete
if (!isset($note['title']) || !isset($note['description']))
$note += $old;
}
// generate new note object from input
$object = $this->_write_preprocess($note, $old);
$saved = $folder->save($object, 'note', $note['uid']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving note object to Kolab server"),
true, false);
$saved = false;
}
else {
$note = $object;
$note['list'] = $list_id;
// cache this in memory for later read
$key = $list_id . ':' . $note['uid'];
$this->cache[$key] = $note;
}
return $saved;
}
/**
* Move the given note to another folder
*/
function move_note($note, $list_id)
{
$this->_read_lists();
$tofolder = $this->folders[$list_id];
$fromfolder = $this->folders[$note['list']];
if ($fromfolder && $tofolder) {
return $fromfolder->move($note['uid'], $tofolder->name);
}
return false;
}
/**
* Remove a single note record from the backend
*
* @param array Hash array with note properties (id, list)
* @param boolean Remove record irreversible (mark as deleted otherwise)
* @return boolean True on success, False on error
*/
public function delete_note($note, $force = true)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
return $folder->delete($note['uid'], $force);
}
/**
* Handler for client requests to list (aka folder) actions
*/
public function list_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_GPC);
$list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC, true);
$success = $update_cmd = false;
switch ($action) {
case 'form-new':
case 'form-edit':
$this->_read_lists();
echo $this->ui->list_editform($action, $this->lists[$list['id']], $this->folders[$list['id']]);
exit;
case 'new':
$list['type'] = 'note';
$list['subscribed'] = true;
$folder = kolab_storage::folder_update($list);
if ($folder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['id'] = kolab_storage::folder_id($folder);
$list['_reload'] = true;
}
break;
case 'edit':
$this->_read_lists();
$oldparent = $this->lists[$list['id']]['parentfolder'];
$newfolder = kolab_storage::folder_update($list);
if ($newfolder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['newid'] = kolab_storage::folder_id($newfolder);
$list['_reload'] = $list['parent'] != $oldparent;
// compose the new display name
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$path_imap = explode($delim, $newfolder);
$list['name'] = kolab_storage::object_name($newfolder);
$list['editname'] = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
$list['listname'] = str_repeat('&nbsp;&nbsp;&nbsp;', count($path_imap)) . '&raquo; ' . $list['editname'];
}
break;
case 'delete':
$this->_read_lists();
$folder = $this->folders[$list['id']];
if ($folder && kolab_storage::folder_delete($folder->name)) {
$success = true;
$update_cmd = 'plugin.destroy_list';
}
else {
$save_error = $this->gettext(kolab_storage::$last_error);
}
break;
}
$this->rc->output->command('plugin.unlock_saving');
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
if ($update_cmd) {
$this->rc->output->command($update_cmd, $list);
}
}
else {
$error_msg = $this->gettext('errorsaving') . ($save_error ? ': ' . $save_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
}
/**
* Hook to add note attachments to message compose if the according parameter is present.
* This completes the 'send note by mail' feature.
*/
public function mail_message_compose($args)
{
if (!empty($args['param']['with_notes'])) {
$uids = explode(',', $args['param']['with_notes']);
$list = $args['param']['notes_list'];
$attachments = array();
foreach ($uids as $uid) {
if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
$args['attachments'][] = array(
'name' => abbreviate_string($note['title'], 50, ''),
'mimetype' => 'message/rfc822',
'data' => $this->note2message($note),
);
if (empty($args['param']['subject'])) {
$args['param']['subject'] = $note['title'];
}
}
}
unset($args['param']['with_notes'], $args['param']['notes_list']);
}
return $args;
}
/**
* Determine whether the given note is HTML formatted
*/
private function is_html($note)
{
// check for opening and closing <html> or <body> tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m) && strpos($note['description'], '</'.$m[1].'>') > 0);
}
/**
* Build an RFC 822 message from the given note
*/
private function note2message($note)
{
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', '8bit');
$message->setParam('html_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCUBE_CHARSET);
$message->setParam('html_charset', RCUBE_CHARSET);
$message->setParam('text_charset', RCUBE_CHARSET);
$message->headers(array(
'Subject' => $note['title'],
'Date' => $note['changed']->format('r'),
));
console($note);
if ($this->is_html($note)) {
$message->setHTMLBody($note['description']);
// add a plain text version of the note content as an alternative part.
$h2t = new rcube_html2text($note['description'], false, true, 0, RCUBE_CHARSET);
$plain_part = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 72), "\r\n", false, RCUBE_CHARSET);
$plain_part = trim(wordwrap($plain_part, 998, "\r\n", true));
// make sure all line endings are CRLF
$plain_part = preg_replace('/\r?\n/', "\r\n", $plain_part);
$message->setTXTBody($plain_part);
}
else {
$message->setTXTBody($note['description']);
}
return $message->getMessage();
}
/**
* Process the given note data (submitted by the client) before saving it
*/
private function _write_preprocess($note, $old = array())
{
$object = $note;
// TODO: handle attachments
// clean up HTML content
$object['description'] = $this->_wash_html($note['description']);
$is_html = true;
// try to be smart and convert to plain-text if no real formatting is detected
if (preg_match('!<body><(?:p|pre)>(.*)</(?:p|pre)></body>!Uims', $object['description'], $m)) {
if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li)(\s+[a-z]|>)!im', $m[1], $n) || !strpos($m[1], '</'.$n[1].'>')) {
// $converter = new rcube_html2text($m[1], false, true, 0);
// $object['description'] = rtrim($converter->get_text());
$object['description'] = html_entity_decode(preg_replace('!<br(\s+/)>!', "\n", $m[1]));
$is_html = false;
}
}
// Add proper HTML header, otherwise Kontact renders it as plain text
if ($is_html) {
$object['description'] = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'."\n" .
str_replace('<head>', '<head><meta name="qrichtext" content="1" />', $object['description']);
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// make list of categories unique
if (is_array($object['categories'])) {
$object['categories'] = array_unique(array_filter($object['categories']));
}
unset($object['list'], $object['tempid'], $object['created'], $object['changed'], $object['created_'], $object['changed_']);
return $object;
}
/**
* Sanity checks/cleanups HTML content
*/
private function _wash_html($html)
{
// Add header with charset spec., washtml cannot work without that
$html = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></html>';
// clean HTML with washtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => array('html', 'head', 'meta', 'body', 'link'),
'html_attribs' => array('rel', 'type', 'name', 'http-equiv'),
);
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
$washer->add_callback('form', array($this, '_washtml_callback'));
$washer->add_callback('a', array($this, '_washtml_callback'));
// Remove non-UTF8 characters
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments (produced by washtml)
$html = preg_replace('/<!--[^>]+-->/', '', $html);
return $html;
}
/**
* Callback function for washtml cleaning class
*/
public function _washtml_callback($tagname, $attrib, $content, $washtml)
{
switch ($tagname) {
case 'form':
$out = html::div('form', $content);
break;
case 'a':
// strip temporary link tags from plain-text markup
$attrib = html::parse_attrib_string($attrib);
if (!empty($attrib['class']) && strpos($attrib['class'], 'x-templink') !== false) {
// remove link entirely
if (strpos($attrib['href'], html_entity_decode($content)) !== false) {
$out = $content;
break;
}
$attrib['class'] = trim(str_replace('x-templink', '', $attrib['class']));
}
$out = html::a($attrib, $content);
break;
default:
$out = '';
}
return $out;
}
}

View file

@ -0,0 +1,321 @@
<?php
class kolab_notes_ui
{
private $rc;
private $plugin;
private $ready = false;
function __construct($plugin)
{
$this->plugin = $plugin;
$this->rc = $plugin->rc;
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->plugin->add_button(array(
'command' => 'notes',
'class' => 'button-notes',
'classsel' => 'button-notes button-selected',
'innerclass' => 'button-inner',
'label' => 'kolab_notes.navtitle',
), 'taskbar');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/notes.css');
$this->plugin->register_action('print', array($this, 'print_template'));
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist'));
$this->plugin->register_handler('plugin.notebooks', array($this, 'folders'));
#$this->plugin->register_handler('plugin.folders_select', array($this, 'folders_select'));
$this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
$this->plugin->register_handler('plugin.listing', array($this, 'listing'));
$this->plugin->register_handler('plugin.editform', array($this, 'editform'));
$this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle'));
$this->plugin->register_handler('plugin.detailview', array($this, 'detailview'));
$this->rc->output->include_script('list.js');
$this->rc->output->include_script('treelist.js');
$this->plugin->include_script('notes.js');
$this->plugin->include_script('jquery.tagedit.js');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
// load config options and user prefs relevant for the UI
$settings = array(
'sort_col' => $this->rc->config->get('kolab_notes_sort_col', 'changed'),
'print_template' => $this->rc->url('print'),
);
if (!empty($_REQUEST['_list'])) {
$settings['selected_list'] = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC);
}
// TinyMCE uses two-letter lang codes, with exception of Chinese
$lang = strtolower($_SESSION['language']);
$lang = strpos($lang, 'zh_') === 0 ? str_replace('_', '-', $lang) : substr($lang, 0, 2);
if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
$lang = 'en';
}
$settings['editor'] = array(
'lang' => $lang,
'editor_css' => $this->plugin->url($this->plugin->local_skin_path() . '/editor.css'),
'spellcheck' => intval($this->rc->config->get('enable_spellcheck')),
'spelldict' => intval($this->rc->config->get('spellcheck_dictionary'))
);
$this->rc->output->set_env('kolab_notes_settings', $settings);
$this->rc->output->add_label('save','cancel');
}
public function folders($attrib)
{
$attrib += array('id' => 'rcmkolabnotebooks');
$jsenv = array();
$items = '';
foreach ($this->plugin->get_lists() as $prop) {
unset($prop['user_id']);
$id = $prop['id'];
$class = '';
if (!$prop['virtual'])
$jsenv[$id] = $prop;
$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'];
$items .= html::tag('li', array('id' => 'rcmliknb' . $html_id, 'class' => trim($class)),
html::span(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 html::tag('ul', $attrib, $items, html::$common_attrib);
}
public function listing($attrib)
{
$attrib += array('id' => 'rcmkolabnoteslist');
$this->rc->output->add_gui_object('noteslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
public function tagslist($attrib)
{
$attrib += array('id' => 'rcmkolabnotestagslist');
$this->rc->output->add_gui_object('notestagslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
public function editform($attrib)
{
$attrib += array('action' => '#', 'id' => 'rcmkolabnoteseditform');
$this->rc->output->add_gui_object('noteseditform', $attrib['id']);
$this->rc->output->include_script('tiny_mce/tiny_mce.js');
$textarea = new html_textarea(array('name' => 'content', 'id' => 'notecontent', 'cols' => 60, 'rows' => 20, 'tabindex' => 3));
return html::tag('form', $attrib, $textarea->show(), array_merge(html::$common_attrib, array('action')));
}
public function detailview($attrib)
{
$attrib += array('id' => 'rcmkolabnotesdetailview');
$this->rc->output->add_gui_object('notesdetailview', $attrib['id']);
return html::div($attrib, '');
}
public function notetitle($attrib)
{
$attrib += array('id' => 'rcmkolabnotestitle');
$this->rc->output->add_gui_object('noteviewtitle', $attrib['id']);
$summary = new html_inputfield(array('name' => 'summary', 'class' => 'notetitle inline-edit', 'size' => 60, 'tabindex' => 1));
$html = $summary->show();
$html .= html::div(array('class' => 'tagline tagedit', 'style' => 'display:none'), '&nbsp;');
$html .= html::div(array('class' => 'dates', 'style' => 'display:none'),
html::label(array(), $this->plugin->gettext('created')) .
html::span('notecreated', '') .
html::label(array(), $this->plugin->gettext('changed')) .
html::span('notechanged', '')
);
return html::div($attrib, $html);
}
/**
* Render edit for notes lists (folders)
*/
public function list_editform($action, $list, $folder)
{
if (is_object($folder)) {
$folder_name = $folder->name; // UTF7
}
else {
$folder_name = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder_name)) {
$options = $storage->folder_info($folder_name);
$path_imap = explode($delim, $folder_name);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
}
else {
$path_imap = '';
$options = array();
}
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
// folder name (default field)
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'noteslist-name', 'size' => 20));
$form['properties']['fields']['name'] = array(
'label' => $this->plugin->gettext('listname'),
'value' => $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected']))),
'id' => 'folder-name',
);
// prevent user from moving folder
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = kolab_storage::folder_selector('note', array('name' => 'parent'), $folder_name);
$form['properties']['fields']['path'] = array(
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show(strlen($folder_name) ? $path_imap : ''),
);
}
// add folder ACL tab
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->plugin->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->rc->url(array('_action' => 'folder-acl', '_folder' => $folder_name, 'framed' => 1)),
'width' => '100%',
'height' => 280,
'border' => 0,
'style' => 'border:0'),
'')
);
}
$form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$form_html .= $hiddenfield->show() . "\n";
}
}
// create form output
foreach ($form as $tab) {
if (is_array($tab['fields']) && empty($tab['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($tab['fields'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : $this->plugin->gettext($col);
$table->add('title', html::label($colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $tab['content'];
}
if (!empty($content)) {
$form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) . "\n";
}
}
return html::tag('form', array('action' => "#", 'method' => "post", 'id' => "noteslistpropform"), $form_html);
}
/**
* Handler to render ACL form for a notes folder
*/
public function folder_acl()
{
$this->plugin->require_plugin('acl');
$this->rc->output->add_handler('folderacl', array($this, 'folder_acl_form'));
$this->rc->output->send('kolab_notes.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function folder_acl_form()
{
$folder = rcube_utils::get_input_value('_folder', RCUBE_INPUT_GPC);
if (strlen($folder)) {
$storage = $this->rc->get_storage();
$options = $storage->folder_info($folder);
// get sharing UI from acl plugin
$acl = $this->rc->plugins->exec_hook('folder_form',
array('form' => array(), 'options' => $options, 'name' => $folder));
}
return $acl['form']['sharing']['content'] ?: html::div('hint', $this->plugin->gettext('aclnorights'));
}
/**
* Render the template for printing with placeholders
*/
public function print_template()
{
$this->rc->output->reset(true);
echo $this->rc->output->parse('kolab_notes.print', false, false);
exit;
}
}

View file

@ -0,0 +1,38 @@
<?php
$labels = array();
$labels['navtitle'] = 'Notes';
$labels['tags'] = 'Tags';
$labels['lists'] = 'Notebooks';
$labels['notes'] = 'Notes';
$labels['create'] = 'New Note';
$labels['createnote'] = 'Create a new note';
$labels['send'] = 'Send';
$labels['sendnote'] = 'Send note by email';
$labels['newnote'] = 'New Note';
$labels['notags'] = 'No tags';
$labels['removetag'] = 'Remove tag';
$labels['created'] = 'Created';
$labels['changed'] = 'Last Modified';
$labels['title'] = 'Title';
$labels['now'] = 'Now';
$labels['sortby'] = 'Sort by';
$labels['createlist'] = 'New Notebook';
$labels['editlist'] = 'Edit Notebook';
$labels['listname'] = 'Name';
$labels['tabsharing'] = 'Sharing';
$labels['discard'] = 'Discard';
$labels['abort'] = 'Abort';
$labels['unsavedchanges'] = 'Unsaved Changes!';
$labels['savingdata'] = 'Saving data...';
$labels['recordnotfound'] = 'Record not found';
$labels['nochanges'] = 'No changes to be saved';
$labels['entertitle'] = 'Please enter a title for this note!';
$labels['deletenotesconfirm'] = 'Do you really want to delete the selected notes?';
$labels['deletenotebookconfirm'] = 'Do you really want to delete this notebook with all its notes? This action cannot be undone.';
$labels['discardunsavedchanges'] = 'The current note has not yet been saved. Discard the changes?';
$labels['invalidlistproperties'] = 'Invalid notebook properties! Please set a valid name.';
$labels['entertitle'] = 'Please enter a title for this note.';
$labels['aclnorights'] = 'You do not have administrator rights for this notebook.';

1206
plugins/kolab_notes/notes.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
/* This file contains the CSS data for the editable area(iframe) of TinyMCE */
body, td, pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
}
body {
background-color: #FFF;
margin: 6px;
}
pre
{
margin: 0;
padding: 0;
white-space: -moz-pre-wrap !important;
white-space: pre-wrap !important;
white-space: pre;
word-wrap: break-word; /* IE (and Safari) */
}
blockquote
{
padding-left: 5px;
border-left: #1010ff 2px solid;
margin-left: 5px;
width: 100%;
}

View file

@ -0,0 +1 @@
../../../kolab_addressbook/skins/larry/folder_icons.png

View file

@ -0,0 +1,339 @@
/**
* Kolab Notes plugin styles for skin "Larry"
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#taskbar a.button-notes span.button-inner {
background-image: url('sprites.png');
background-position: 0 0;
}
#taskbar a.button-notes:hover span.button-inner,
#taskbar a.button-notes.button-selected span.button-inner {
background-image: url('sprites.png');
background-position: 0 -26px;
}
.notesview #sidebar {
position: absolute;
top: 42px;
left: 0;
bottom: 0;
width: 240px;
}
.notesview #notestoolbar {
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 40px;
white-space: nowrap;
}
.notesview #notestoolbar a.button.createnote {
background-image: url('sprites.png');
background-position: center -54px;
}
.notesview #notestoolbar a.button.sendnote {
background-position: left -650px;
}
.notesview #quicksearchbar {
top: 8px;
}
.notesview #searchmenulink {
width: 15px;
}
.notesview #mainview-right {
top: 42px;
}
.notesview #tagsbox {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 242px;
}
.notesview #notebooksbox {
position: absolute;
top: 300px;
left: 0;
width: 100%;
bottom: 0px;
}
.notesview #noteslistbox {
position: absolute;
top: 0;
left: 0;
width: 240px;
bottom: 0px;
}
.notesview #kolabnoteslist .title {
display: block;
padding: 4px 8px;
overflow: hidden;
text-overflow: ellipsis;
}
.notesview #kolabnoteslist .date {
display: block;
padding: 0px 8px 4px 8px;
color: #777;
font-weight: normal;
}
.notesview .boxpagenav a.icon.sortoptions {
background: url(sprites.png) center -93px no-repeat;
}
.notesview .toolbarmenu.iconized .selected span.icon {
background: url(sprites.png) -5px -109px no-repeat;
}
.notesview #notedetailsbox {
position: absolute;
top: 0;
left: 256px;
right: 0;
bottom: 0px;
}
.notesview #notedetailsbox .formbuttons {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 12px;
background: #f9f9f9;
}
.notesview #noteform,
.notesview #notedetails {
display: none;
position: absolute;
top: 82px;
left: 0;
bottom: 41px;
width: 100%;
}
.notesview #notedetails {
padding: 8px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.notesview #notedetails pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
margin: 0;
}
.notesview #notecontent {
position: relative;
width: 100%;
height: 100%;
border: 0;
border-radius: 0;
padding: 8px 0 8px 8px;
resize: none;
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
outline: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
}
.notesview #notecontent:active,
.notesview #notecontent:focus {
-webkit-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-o-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
}
.notesview .defaultSkin table.mceLayout {
border: 0;
}
.notesview #notedetailstitle {
height: auto;
}
.notesview #notedetailstitle .tagedit-list,
.notesview #notedetailstitle input.inline-edit,
.notesview #notedetailstitle input.inline-edit:focus {
outline: none;
padding: 0;
margin: 0;
border: 0;
background: rgba(255,255,255,0.01);
-webkit-box-shadow: none;
-moz-box-shadow: none;
-o-box-shadow: none;
box-shadow: none;
}
.notesview #notedetailstitle input.notetitle,
.notesview #notedetailstitle input.notetitle:focus {
width: 100%;
font-size: 14px;
font-weight: bold;
color: #777;
}
.notesview #notedetailstitle .dates,
.notesview #notedetailstitle .tagline {
color: #999;
font-weight: normal;
font-size: 0.9em;
margin-top: 6px;
}
.notesview #notedetailstitle .dates {
margin-top: 4px;
margin-bottom: 4px;
}
.notesview #notedetailstitle .tagline {
position: relative;
cursor: text;
}
.notesview #notedetailstitle .tagline .placeholder {
position: absolute;
top: 4px;
left: 0;
z-index: 1;
}
.notesview #notedetailstitle .tagedit-list {
position: relative;
z-index: 2;
}
.notesview #notedetailstitle #tagedit-input {
background: none;
}
.notesview .tag-draghelper {
z-index: 1000;
}
.notesview #notedetailstitle .notecreated,
.notesview #notedetailstitle .notechanged {
display: inline-block;
padding-left: 0.4em;
padding-right: 2em;
color: #777;
}
.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 span.listname {
display: block;
position: absolute;
top: 7px;
left: 9px;
right: 6px;
cursor: default;
padding-bottom: 2px;
padding-right: 26px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notesview #notebooks li.virtual span.listname {
color: #aaa;
top: 3px;
}
.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 li.readonly {
background-position: 98% -21px;
}
.notesview #notebooks li.other {
background-position: 98% -52px;
}
.notesview #notebooks li.other.readonly {
background-position: 98% -77px;
}
.notesview #notebooks li.shared {
background-position: 98% -103px;
}
.notesview #notebooks li.shared.readonly {
background-position: 98% -130px;
}
.notesview #notebooks li.other.readonly span.listname,
.notesview #notebooks li.shared.readonly span.listname {
padding-right: 36px;
}
.notesview #notebooks li.selected > a {
background-color: transparent;
}
.notesview .uidialog .tabbed {
margin-top: -12px;
}
.notesview .uidialog .propform fieldset.tab {
display: block;
background: #efefef;
margin-top: 0.5em;
padding: 0.5em 1em;
min-height: 290px;
}
.notesview .uidialog .propform #noteslist-name {
width: 20em;
}

View file

@ -0,0 +1,43 @@
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 10pt;
}
body {
background-color: #FFF;
margin: 1em;
}
#notebody {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#notedetailstitle {
padding-bottom: 0.5em;
margin-bottom: 1em;
border-bottom: 1px solid #999;
}
#notedetailstitle #notetags {
margin-bottom: 0.4em;
font-style: italic;
color: #666;
}
#notedetailstitle #notetags .tag {
margin-right: 0.8em;
}
#notedetailstitle .dates {
margin-bottom: 0.2em;
color: #999;
}
#notedetailstitle .dates span {
margin-right: 1em;
}
#notedetailstitle h1 {
font-size: 14pt;
margin: 0 0 0.5em 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
../../../tasklist/skins/larry/tagedit.css

View file

@ -0,0 +1,26 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<style type="text/css" media="screen">
body.aclform {
background: #efefef;
margin: 0;
}
body.aclform .hint {
margin: 1em;
}
</style>
</head>
<body class="iframe aclform">
<roundcube:object name="folderacl" />
<roundcube:include file="/includes/footer.html" />
</body>
</html>

View file

@ -0,0 +1,138 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="notesview noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<div id="notestoolbar" class="toolbar">
<roundcube:button command="createnote" type="link" class="button createnote disabled" classAct="button createnote" classSel="button createnote pressed" label="kolab_notes.create" title="kolab_notes.createnote" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="print" />
<roundcube:button command="sendnote" type="link" class="button sendnote disabled" classAct="button sendnote" classSel="button sendnote pressed" label="kolab_notes.send" title="kolab_notes.sendnote" />
<roundcube:container name="toolbar" id="notestoolbar" />
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
</div>
<div id="sidebar">
<div id="tagsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.tags" id="taglist" /></h2>
<div class="scroller">
<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" />
</div>
</div>
<div id="notebooksbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.lists" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.notebooks" id="notebooks" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="kolab_notes.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="notesoptionslink" id="notesoptionsmenulink" type="link" title="kolab_notes.listactions" class="listbutton groupactions" onclick="UI.show_popup('notesoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
</div>
</div>
</div>
<div id="mainview-right">
<div id="noteslistbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.notes" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.listing" id="kolabnoteslist" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="delete" type="link" title="delete" class="listbutton delete disabled" classAct="listbutton delete" innerClass="inner" content="-" />
<roundcube:object name="plugin.recordsCountDisplay" class="countdisplay" label="fromtoshort" />
</div>
<div class="boxpagenav">
<roundcube:button name="notessortmenulink" id="notessortmenulink" type="link" title="kolab_notes.sortby" class="icon sortoptions" onclick="UI.show_popup('notessortmenu');return false" innerClass="inner" content="v" />
</div>
</div>
<div id="notedetailsbox" class="uibox contentbox">
<roundcube:object name="plugin.notetitle" id="notedetailstitle" class="boxtitle" />
<roundcube:object name="plugin.editform" id="noteform" />
<roundcube:object name="plugin.detailview" id="notedetails" class="scroller" />
<div class="footerleft formbuttons">
<roundcube:button command="save" type="input" class="button mainaction" label="save" />
</div>
</div>
</div>
</div>
<roundcube:object name="message" id="messagestack" />
<div id="notesoptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
<li><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
</ul>
</div>
<div id="notessortmenu" class="popupmenu">
<ul class="toolbarmenu iconized">
<li><roundcube:button command="list-sort" prop="changed" type="link" label="kolab_notes.changed" class="icon active by-changed" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="title" type="link" label="kolab_notes.title" class="icon active by-title" innerclass="icon" /></li>
</ul>
</div>
<div id="notebookeditform" class="uidialog">
<roundcube:container name="notebookeditform" id="notebookeditform" />
<roundcube:label name="loading" />
</div>
<script type="text/javascript">
// UI startup
var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
rcmail.addEventListener('kolab_notes_editform_load', function(e){
UI.init_tabs($('#notebookeditform > form').addClass('propform tabbed'));
})
new rcube_splitter({ id:'notesviewsplitter', p1:'#sidebar', p2:'#mainview-right',
orientation:'v', relative:true, start:240, min:180, size:16, offset:2, render:layout_view }).init();
new rcube_splitter({ id:'noteslistsplitter2', p1:'#noteslistbox', p2:'#notedetailsbox',
orientation:'v', relative:true, start:242, min:180, size:16, offset:2, render:layout_view }).init();
new rcube_splitter({ id:'notesviewsplitterv', p1:'#tagsbox', p2:'#notebooksbox',
orientation:'h', relative:true, start:242, min:120, size:16, offset:6 }).init();
function layout_view()
{
var form = $('#noteform, #notedetails'),
content = $('#notecontent'),
header = $('#notedetailstitle'),
w, h;
form.css('top', header.outerHeight()+'px');
w = form.outerWidth();
h = form.outerHeight();
content.width(w).height(h);
$('#notecontent_tbl').width(w+'px').height('').css('margin-top', '-1px');
$('#notecontent_ifr').width(w+'px').height((h-54)+'px');
}
$(window).resize(function(e){
layout_view();
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,25 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title>Print</title>
<link rel="stylesheet" type="text/css" href="/this/editor.css" />
<link rel="stylesheet" type="text/css" href="/this/print.css" />
</head>
<body class="notesprint">
<div id="notedetailstitle">
<h1 id="notetitle">#Title</h1>
<div id="notetags" class="tagline">#Tags</div>
<div class="dates">
<label><roundcube:label name="kolab_notes.created" /></label>
<span id="notecreated">#Created</span>
<label><roundcube:label name="kolab_notes.changed" /></label>
<span id="notechanged">#Changed</span>
</div>
</div>
<div id="notebody">
#Body
</div>
</body>
</html>

View file

@ -1,7 +1,7 @@
/**
* libkolab database schema
*
* @version 1.0
* @version 1.1
* @author Thomas Bruederli
* @licence GNU AGPL
**/
@ -29,7 +29,7 @@ CREATE TABLE `kolab_cache_contact` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -41,7 +41,8 @@ CREATE TABLE `kolab_cache_contact` (
CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `contact_type` (`folder_id`,`type`)
INDEX `contact_type` (`folder_id`,`type`),
INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_event`;
@ -52,7 +53,7 @@ CREATE TABLE `kolab_cache_event` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -60,7 +61,8 @@ CREATE TABLE `kolab_cache_event` (
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`)
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_task`;
@ -71,7 +73,7 @@ CREATE TABLE `kolab_cache_task` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -79,7 +81,8 @@ CREATE TABLE `kolab_cache_task` (
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`)
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_journal`;
@ -90,7 +93,7 @@ CREATE TABLE `kolab_cache_journal` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -98,7 +101,8 @@ CREATE TABLE `kolab_cache_journal` (
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`)
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_note`;
@ -109,13 +113,14 @@ CREATE TABLE `kolab_cache_note` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`)
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_file`;
@ -126,7 +131,7 @@ CREATE TABLE `kolab_cache_file` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -134,7 +139,8 @@ CREATE TABLE `kolab_cache_file` (
CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `folder_filename` (`folder_id`, `filename`)
INDEX `folder_filename` (`folder_id`, `filename`),
INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_configuration`;
@ -145,7 +151,7 @@ CREATE TABLE `kolab_cache_configuration` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -153,7 +159,8 @@ CREATE TABLE `kolab_cache_configuration` (
CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `configuration_type` (`folder_id`,`type`)
INDEX `configuration_type` (`folder_id`,`type`),
INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_freebusy`;
@ -164,7 +171,7 @@ CREATE TABLE `kolab_cache_freebusy` (
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`data` LONGTEXT NOT NULL,
`xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
@ -172,7 +179,8 @@ CREATE TABLE `kolab_cache_freebusy` (
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`)
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;

View file

@ -0,0 +1,8 @@
ALTER TABLE `kolab_cache_configuration` ADD INDEX `configuration_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_contact` ADD INDEX `contact_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_event` ADD INDEX `event_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_task` ADD INDEX `task_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_journal` ADD INDEX `journal_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_note` ADD INDEX `note_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_file` ADD INDEX `file_uid2msguid` (`folder_id`, `uid`, `msguid`);
ALTER TABLE `kolab_cache_freebusy` ADD INDEX `freebusy_uid2msguid` (`folder_id`, `uid`, `msguid`);

View file

@ -0,0 +1,8 @@
ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;

View file

@ -25,7 +25,7 @@
class kolab_format_file extends kolab_format
{
public $CTYPE = 'application/x-vnd.kolab.file';
public $CTYPE = 'application/vnd.kolab+xml';
protected $objclass = 'File';
protected $read_func = 'kolabformat::readKolabFile';

View file

@ -24,9 +24,11 @@
class kolab_format_note extends kolab_format
{
public $CTYPE = 'application/x-vnd.kolab.note';
public $CTYPE = 'application/vnd.kolab+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.note';
public static $fulltext_cols = array('title', 'description', 'categories');
protected $objclass = 'Note';
protected $read_func = 'readNote';
protected $write_func = 'writeNote';
@ -114,4 +116,29 @@ class kolab_format_note extends kolab_format
return $tags;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $col) {
// convert HTML content to plain text
if ($col == 'description' && preg_match('/<(html|body)(\s[a-z]|>)/', $this->data[$col], $m) && strpos($this->data[$col], '</'.$m[1].'>')) {
$converter = new rcube_html2text($this->data[$col], false, false, 0);
$val = $converter->get_text();
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_filter(array_unique(rcube_utils::normalize_string($data, true)));
}
}

View file

@ -911,6 +911,22 @@ class kolab_storage_cache
*/
public function uid2msguid($uid, $deleted = false)
{
// query local database if available
if (!isset($this->uid2msg[$uid]) && $this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT msguid FROM $this->cache_table ".
"WHERE folder_id=? AND uid=?",
$this->folder_id,
$uid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->uid2msg[$uid] = $sql_arr['msguid'];
}
}
if (!isset($this->uid2msg[$uid])) {
// use IMAP SEARCH to get the right message
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .

View file

@ -578,7 +578,7 @@ class kolab_storage_folder
// get XML part
foreach ((array)$message->attachments as $part) {
if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
$xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
}
else if ($part->filename || $part->content_id) {

194
plugins/tasklist/jquery.tagedit.js Executable file → Normal file
View file

@ -5,13 +5,14 @@
* Examples and documentation at: tagedit.webwork-albrecht.de
*
* Copyright (c) 2010 Oliver Albrecht <info@webwork-albrecht.de>
* Copyright (c) 2012 Thomas Brüderli <thomas@roundcube.net>
*
* License:
* This work is licensed under a MIT License
* http://www.opensource.org/licenses/mit-license.php
*
* @author Oliver Albrecht Mial: info@webwork-albrecht.de Twitter: @webworka
* @version 1.2.1 (11/2011)
* @version 1.5.1 (10/2013)
* Requires: jQuery v1.4+, jQueryUI v1.8+, jQuerry.autoGrowInput
*
* Example of usage:
@ -52,6 +53,7 @@
options = $.extend(true, {
// default options here
autocompleteURL: null,
checkToDeleteURL: null,
deletedPostfix: '-d',
addedPostfix: '-a',
additionalListClass: '',
@ -67,14 +69,15 @@
}
},
breakKeyCodes: [ 13, 44 ],
checkNewEntriesCaseSensitive: false,
checkNewEntriesCaseSensitive: false,
texts: {
removeLinkTitle: 'Remove from list.',
saveEditLinkTitle: 'Save changes.',
deleteLinkTitle: 'Delete this tag from database.',
deleteConfirmation: 'Are you sure to delete this entry?',
deletedElementTitle: 'This Element will be deleted.',
breakEditLinkTitle: 'Cancel'
breakEditLinkTitle: 'Cancel',
forceDeleteConfirmation: 'There are more records using this tag, are you sure do you want to remove it?'
},
tabindex: false
}, options || {});
@ -133,7 +136,8 @@
html += '<li class="tagedit-listelement tagedit-listelement-old">';
html += '<span dir="'+options.direction+'">' + $(this).val() + '</span>';
html += '<input type="hidden" name="'+baseName+'['+elementId+']" value="'+$(this).val()+'" />';
html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'">x</a>';
if (options.allowDelete)
html += '<a class="tagedit-close" title="'+options.texts.removeLinkTitle+'">x</a>';
html += '</li>';
}
}
@ -155,7 +159,8 @@
// put an input field at the End
// Put an empty element at the end
html = '<li class="tagedit-listelement tagedit-listelement-new">';
html += '<input type="text" name="'+baseName+'[]" value="" id="tagedit-input" disabled="disabled" class="tagedit-input-disabled" dir="'+options.direction+'"/>';
if (options.allowAdd)
html += '<input type="text" name="'+baseName+'[]" value="" id="tagedit-input" disabled="disabled" class="tagedit-input-disabled" dir="'+options.direction+'"/>';
html += '</li>';
html += '</ul>';
@ -169,16 +174,16 @@
.each(function() {
$(this).autoGrowInput({comfortZone: 15, minWidth: 15, maxWidth: 20000});
// Event ist triggert in case of choosing an item from the autocomplete, or finish the input
// Event is triggert in case of choosing an item from the autocomplete, or finish the input
$(this).bind('transformToTag', function(event, id) {
var oldValue = (typeof id != 'undefined' && id.length > 0);
var oldValue = (typeof id != 'undefined' && (id.length > 0 || id > 0));
var checkAutocomplete = oldValue == true? false : true;
var checkAutocomplete = oldValue == true || options.autocompleteOptions.noCheck ? false : true;
// check if the Value ist new
var isNewResult = isNew($(this).val(), checkAutocomplete);
if(isNewResult[0] === true || isNewResult[1] != null) {
if(isNewResult[0] === true || (isNewResult[0] === false && typeof isNewResult[1] == 'string')) {
if(oldValue == false && isNewResult[1] != null) {
if(oldValue == false && typeof isNewResult[1] == 'string') {
oldValue = true;
id = isNewResult[1];
}
@ -199,7 +204,8 @@
// close autocomplete
if(options.autocompleteOptions.source) {
$(this).autocomplete( "close" );
if($(this).is(':ui-autocomplete'))
$(this).autocomplete( "close" );
}
})
@ -288,7 +294,7 @@
}
return false;
})
// forward focus event
// forward focus event (on tabbing through the form)
.focus(function(e){ $(this).click(); })
}
@ -321,7 +327,7 @@
}
textfield.remove();
$(this).find('a.tagedit-save, a.tagedit-break, a.tagedit-delete, tester').remove(); // Workaround. This normaly has to be done by autogrow Plugin
$(this).find('a.tagedit-save, a.tagedit-break, a.tagedit-delete').remove(); // Workaround. This normaly has to be done by autogrow Plugin
$(this).removeClass('tagedit-listelement-edit').unbind('finishEdit');
return false;
});
@ -356,7 +362,16 @@
.click(function() {
window.clearTimeout(closeTimer);
if(confirm(options.texts.deleteConfirmation)) {
markAsDeleted($(this).parent());
var canDelete = checkToDelete($(this).parent());
if (!canDelete && confirm(options.texts.forceDeleteConfirmation)) {
markAsDeleted($(this).parent());
}
if(canDelete) {
markAsDeleted($(this).parent());
}
$(this).parent().find(':text').trigger('finishEdit', [true]);
}
else {
$(this).parent().find(':text').trigger('finishEdit', [true]);
@ -386,6 +401,42 @@
});
}
/**
* Verifies if the tag select to be deleted is used by other records using an Ajax request.
*
* @param element
* @returns {boolean}
*/
function checkToDelete(element) {
// if no URL is provide will not verify
if(options.checkToDeleteURL === null) {
return false;
}
var inputName = element.find('input:hidden').attr('name');
var idPattern = new RegExp('\\d');
var tagId = inputName.match(idPattern);
var checkResult = false;
$.ajax({
async : false,
url : options.checkToDeleteURL,
dataType: 'json',
type : 'POST',
data : { 'tagId' : tagId},
complete: function (XMLHttpRequest, textStatus) {
// Expected JSON Object: { "success": Boolean, "allowDelete": Boolean}
var result = $.parseJSON(XMLHttpRequest.responseText);
if(result.success === true){
checkResult = result.allowDelete;
}
}
});
return checkResult;
}
/**
* Marks a single Tag as deleted.
*
@ -451,12 +502,11 @@
}
});
}
// If there is an entry for that already in the autocomplete, don't use it (Check could be case sensitive or not)
for (var i = 0; i < result.length; i++) {
var label = typeof result[i] == 'string' ? result[i] : result[i].label;
if (options.checkNewEntriesCaseSensitive == false)
label = label.toLowerCase();
var resultValue = result[i].label? result[i].label : result[i];
var label = options.checkNewEntriesCaseSensitive == true? resultValue : resultValue.toLowerCase();
if (label == compareValue) {
isNew = false;
autoCompleteId = typeof result[i] == 'string' ? i : result[i].id;
@ -476,60 +526,60 @@
// See related thread: http://stackoverflow.com/questions/931207/is-there-a-jquery-autogrow-plugin-for-text-fields
$.fn.autoGrowInput = function(o) {
o = $.extend({
maxWidth: 1000,
minWidth: 0,
comfortZone: 70
}, o);
this.filter('input:text').each(function(){
var minWidth = o.minWidth || $(this).width(),
val = '',
input = $(this),
testSubject = $('<tester/>').css({
position: 'absolute',
top: -9999,
left: -9999,
width: 'auto',
fontSize: input.css('fontSize'),
fontFamily: input.css('fontFamily'),
fontWeight: input.css('fontWeight'),
letterSpacing: input.css('letterSpacing'),
whiteSpace: 'nowrap'
}),
check = function() {
if (val === (val = input.val())) {return;}
// Enter new content into testSubject
var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
testSubject.html(escaped);
// Calculate new width + whether to change
var testerWidth = testSubject.width(),
newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth,
currentWidth = input.width(),
isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
|| (newWidth > minWidth && newWidth < o.maxWidth);
// Animate width
if (isValidWidthChange) {
input.width(newWidth);
}
};
testSubject.insertAfter(input);
$(this).bind('keyup keydown blur update', check);
check();
});
return this;
o = $.extend({
maxWidth: 1000,
minWidth: 0,
comfortZone: 70
}, o);
this.filter('input:text').each(function(){
var minWidth = o.minWidth || $(this).width(),
val = '',
input = $(this),
testSubject = $('<tester/>').css({
position: 'absolute',
top: -9999,
left: -9999,
width: 'auto',
fontSize: input.css('fontSize'),
fontFamily: input.css('fontFamily'),
fontWeight: input.css('fontWeight'),
letterSpacing: input.css('letterSpacing'),
whiteSpace: 'nowrap'
}),
check = function() {
if (val === (val = input.val())) {return;}
// Enter new content into testSubject
var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
testSubject.html(escaped);
// Calculate new width + whether to change
var testerWidth = testSubject.width(),
newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth,
currentWidth = input.width(),
isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
|| (newWidth > minWidth && newWidth < o.maxWidth);
// Animate width
if (isValidWidthChange) {
input.width(newWidth);
}
};
testSubject.insertAfter(input);
$(this).bind('keyup keydown blur update', check);
check();
});
return this;
};
})(jQuery);
})(jQuery);

View file

@ -55,7 +55,7 @@ html.ie7 #taskselector li.selected.overdue .count {
color: #fff;
}
html.ie7 #tagslist li,
html.ie7 .tagcloud li,
html.ie7 #taskselector li {
float: left;
}

View file

@ -0,0 +1,113 @@
/**
* Styles of the tagedit inputsforms
*/
.tagedit-list {
width: 100%;
margin: 0;
padding: 4px 4px 0 5px;
overflow: auto;
min-height: 26px;
background: #fff;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
.tagedit-list li.tagedit-listelement {
list-style-type: none;
float: left;
margin: 0 4px 4px 0;
padding: 0;
}
/* New Item input */
.tagedit-list li.tagedit-listelement-new input {
border: 0;
height: 100%;
padding: 4px 1px;
width: 15px;
background: #fff;
border-radius: 0;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
}
.tagedit-list li.tagedit-listelement-new input:focus {
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
outline: none;
}
.tagedit-list li.tagedit-listelement-new input.tagedit-input-disabled {
display: none;
}
/* Item that is put to the List */
.tagedit span.tag-element,
.tagedit-list li.tagedit-listelement-old {
padding: 3px 6px 1px 6px;
background: #ddeef5;
background: -moz-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#edf6fa), color-stop(100%,#d6e9f3));
background: -o-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -ms-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
border: 1px solid #c2dae5;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #0d5165;
}
.tagedit span.tag-element {
margin-right: 0.6em;
padding: 2px 6px;
/* cursor: pointer; */
}
.tagedit span.tag-element.inherit {
color: #666;
background: #f2f2f2;
border-color: #ddd;
}
.tagedit-list li.tagedit-listelement-old a.tagedit-close,
.tagedit-list li.tagedit-listelement-old a.tagedit-break,
.tagedit-list li.tagedit-listelement-old a.tagedit-delete,
.tagedit-list li.tagedit-listelement-old a.tagedit-save {
text-indent: -2000px;
display: inline-block;
position: relative;
top: -1px;
width: 16px;
height: 16px;
margin: 0 -4px 0 6px;
background: url('data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAOCAYAAAD0f5bSAAAAgUlEQVQoz2NgQAKzdxwWAOIEIG5AwiC+AAM2AJQIAOL3QPwfCwaJB6BrSMChGB0nwDQYwATP3nn4f+Ge4ygKQXyQOJKYAUjTepjAm09fwBimEUTDxJA0rWdANxWmaMXB0xiGwDADurthGkEAmwbqaCLFeWQFBOlBTlbkkp2MSE2wAA8R50rWvqeRAAAAAElFTkSuQmCC') left 1px no-repeat;
cursor: pointer;
}
.tagedit-list li.tagedit-listelement-old span {
display: inline-block;
height: 15px;
}
/** Special hacks for IE7 **/
html.ie7 .tagedit span.tag-element,
html.ie7 .tagedit-list li.tagedit-listelement-old {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#edf6fa', endColorstr='#d6e9f3', GradientType=0);
}
html.ie7 .tagedit-list li.tagedit-listelement span {
position: relative;
top: -3px;
}
html.ie7 .tagedit-list li.tagedit-listelement-old a.tagedit-close {
left: 5px;
}

View file

@ -86,7 +86,7 @@ body.attachmentwin #topnav .topright {
padding-right: 0.3em;
}
#tagslist li,
.tagcloud li,
#taskselector li a {
display: inline-block;
color: #004458;
@ -117,7 +117,7 @@ body.attachmentwin #topnav .topright {
color: #97b3bf;
}
#tagslist li.selected,
.tagcloud li.selected,
#taskselector li.selected a {
color: #fff;
background: #005d76;
@ -180,13 +180,13 @@ body.attachmentwin #topnav .topright {
border-color: #ff3800 transparent;
}
#tagslist {
.tagcloud {
padding: 0;
margin: 6px;
list-style: none;
}
#tagslist li {
.tagcloud li {
display: inline-block;
color: #004458;
padding-right: 0.2em;
@ -196,14 +196,14 @@ body.attachmentwin #topnav .topright {
cursor: pointer;
}
#tagslist li.inactive {
.tagcloud li.inactive {
color: #89b3be;
padding-right: 0.6em;
font-size: 80%;
/* display: none; */
}
#tagslist li .count {
.tagcloud li .count {
position: relative;
top: -1px;
margin-left: 5px;
@ -221,7 +221,7 @@ body.attachmentwin #topnav .topright {
}
.tag-draghelper .tag .count,
#tagslist li.inactive .count {
.tagcloud li.inactive .count {
display: none;
}
@ -852,99 +852,6 @@ label.block {
}
/**
* Styles of the tagedit inputsforms
*/
.tagedit-list {
width: 100%;
margin: 0;
padding: 4px 4px 0 5px;
overflow: auto;
min-height: 26px;
background: #fff;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
.tagedit-list li.tagedit-listelement {
list-style-type: none;
float: left;
margin: 0 4px 4px 0;
padding: 0;
}
/* New Item input */
.tagedit-list li.tagedit-listelement-new input {
border: 0;
height: 100%;
padding: 4px 1px;
width: 15px;
background: #fff;
border-radius: 0;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
}
.tagedit-list li.tagedit-listelement-new input:focus {
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
outline: none;
}
.tagedit-list li.tagedit-listelement-new input.tagedit-input-disabled {
display: none;
}
/* Item that is put to the List */
.form-section span.tag-element,
.tagedit-list li.tagedit-listelement-old {
padding: 3px 0 1px 6px;
background: #ddeef5;
background: -moz-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#edf6fa), color-stop(100%,#d6e9f3));
background: -o-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -ms-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
border: 1px solid #c2dae5;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #0d5165;
}
.form-section span.tag-element {
margin-right: 0.6em;
padding: 2px 6px;
/* cursor: pointer; */
}
.form-section span.tag-element.inherit {
color: #666;
background: #f2f2f2;
border-color: #ddd;
}
.tagedit-list li.tagedit-listelement-old a.tagedit-close,
.tagedit-list li.tagedit-listelement-old a.tagedit-break,
.tagedit-list li.tagedit-listelement-old a.tagedit-delete,
.tagedit-list li.tagedit-listelement-old a.tagedit-save {
text-indent: -2000px;
display: inline-block;
position: relative;
top: -1px;
width: 16px;
height: 16px;
margin: 0 2px 0 6px;
background: url(sprites.png) -2px -122px no-repeat;
cursor: pointer;
}
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
@ -952,17 +859,3 @@ html.ie7 #taskedit-completeness-slider {
display: inline;
}
html.ie7 .form-section span.tag-element,
html.ie7 .tagedit-list li.tagedit-listelement-old {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#edf6fa', endColorstr='#d6e9f3', GradientType=0);
}
html.ie7 .tagedit-list li.tagedit-listelement span {
position: relative;
top: -3px;
}
html.ie7 .tagedit-list li.tagedit-listelement-old a.tagedit-close {
left: 5px;
}

View file

@ -19,7 +19,7 @@
<div id="tagsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.tags" id="taglist" /></h2>
<div class="scroller">
<roundcube:object name="plugin.tagslist" id="tagslist" />
<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" />
</div>
</div>

View file

@ -1205,7 +1205,7 @@ function rcube_tasklist_ui(settings)
animSpeed: 100,
allowEdit: false,
checkNewEntriesCaseSensitive: false,
autocompleteOptions: { source: tags, minLength: 0 },
autocompleteOptions: { source: tags, minLength: 0, noCheck: true },
texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') }
});
@ -1341,9 +1341,6 @@ function rcube_tasklist_ui(settings)
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
open: function() {
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
},
close: function() {
editform.hide().appendTo(document.body);
$dialog.dialog('destroy').remove();

View file

@ -782,11 +782,9 @@ class tasklist extends rcube_plugin
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
// "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
// "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
// "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;

View file

@ -80,6 +80,8 @@ class tasklist_ui
$this->plugin->include_script('jquery.tagedit.js');
$this->plugin->include_script('tasklist.js');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
}
/**