diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index 4f812c23..c8dd59e7 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -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; diff --git a/plugins/kolab_notes/jquery.tagedit.js b/plugins/kolab_notes/jquery.tagedit.js new file mode 120000 index 00000000..6bb782c4 --- /dev/null +++ b/plugins/kolab_notes/jquery.tagedit.js @@ -0,0 +1 @@ +../tasklist/jquery.tagedit.js \ No newline at end of file diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php new file mode 100644 index 00000000..cdf61bd9 --- /dev/null +++ b/plugins/kolab_notes/kolab_notes.php @@ -0,0 +1,797 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * 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 . + */ + +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('   ', count($path_imap)) . '» ' . $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 or tags + return (preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m) && strpos($note['description'], '') > 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('!<(?:p|pre)>(.*)!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], '')) { + // $converter = new rcube_html2text($m[1], false, true, 0); + // $object['description'] = rtrim($converter->get_text()); + $object['description'] = html_entity_decode(preg_replace('!!', "\n", $m[1])); + $is_html = false; + } + } + + // Add proper HTML header, otherwise Kontact renders it as plain text + if ($is_html) { + $object['description'] = ''."\n" . + str_replace('', '', $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 . ''; + + // 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; + } + +} + diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php new file mode 100644 index 00000000..ce8bb975 --- /dev/null +++ b/plugins/kolab_notes/kolab_notes_ui.php @@ -0,0 +1,321 @@ +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'), ' '); + $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; + } + +} + diff --git a/plugins/kolab_notes/localization/en_US.inc b/plugins/kolab_notes/localization/en_US.inc new file mode 100644 index 00000000..66545c0d --- /dev/null +++ b/plugins/kolab_notes/localization/en_US.inc @@ -0,0 +1,38 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * 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 . + */ + +function rcube_kolab_notes_ui(settings) +{ + /* private vars */ + var ui_loading = false; + var saving_lock; + var search_query; + var folder_drop_target; + var notebookslist; + var noteslist; + var notesdata = {}; + var tagsfilter = []; + var tags = []; + var search_request; + var search_query; + var tag_draghelper; + var me = this; + + /* public members */ + this.selected_list; + this.selected_note; + this.notebooks = rcmail.env.kolab_notebooks || {}; + + /** + * initialize the notes UI + */ + function init() + { + // register button commands + rcmail.register_command('createnote', function(){ + warn_unsaved_changes(function(){ edit_note(null, 'new'); }) + }, false); + rcmail.register_command('list-create', function(){ list_edit_dialog(null); }, true); + rcmail.register_command('list-edit', function(){ list_edit_dialog(me.selected_list); }, false); + rcmail.register_command('list-remove', function(){ list_remove(me.selected_list); }, false); + rcmail.register_command('list-sort', list_set_sort, true); + rcmail.register_command('save', save_note, true); + rcmail.register_command('delete', delete_notes, false); + rcmail.register_command('search', quicksearch, true); + rcmail.register_command('reset-search', reset_search, true); + rcmail.register_command('sendnote', send_note, false); + rcmail.register_command('print', print_note, false); + + // register server callbacks + rcmail.addEventListener('plugin.data_ready', data_ready); + rcmail.addEventListener('plugin.render_note', render_note); + rcmail.addEventListener('plugin.update_note', update_note); + rcmail.addEventListener('plugin.update_list', list_update); + rcmail.addEventListener('plugin.destroy_list', list_destroy); + rcmail.addEventListener('plugin.unlock_saving', function(){ + if (saving_lock) { + rcmail.set_busy(false, null, saving_lock); + } + if (rcmail.gui_objects.noteseditform) { + rcmail.lock_form(rcmail.gui_objects.noteseditform, false); + } + }); + + // 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))) { + settings.selected_list = id; + } + } + + notebookslist = new rcube_treelist_widget(rcmail.gui_objects.notebooks, { + id_prefix: 'rcmliknb', + selectable: true, + check_droptarget: function(node) { + var list = me.notebooks[node.id]; + return !node.virtual && list.editable && node.id != me.selected_list; + } + }); + notebookslist.addEventListener('select', function(node) { + var id = node.id; + if (me.notebooks[id] && id != me.selected_list) { + warn_unsaved_changes(function(){ + rcmail.enable_command('createnote', 'list-edit', 'list-remove', me.notebooks[id].editable); + fetch_notes(id); // sets me.selected_list + }, + function(){ + // restore previous selection + notebookslist.select(me.selected_list); + }); + } + }); + + // register dbl-click handler to open list edit dialog + $(rcmail.gui_objects.notebooks).on('dblclick', 'li:not(.virtual)', function(e){ + var id = String(this.id).replace(/^rcmliknb/, ''); + if (me.notebooks[id] && me.notebooks[id].editable) { + list_edit_dialog(id); + } + + // clear text selection (from dbl-clicking) + var sel = window.getSelection ? window.getSelection() : document.selection; + if (sel && sel.removeAllRanges) { + sel.removeAllRanges(); + } + else if (sel && sel.empty) { + sel.empty(); + } + + e.preventDefault(); + return false; + }); + + // initialize notes list widget + if (rcmail.gui_objects.noteslist) { + noteslist = new rcube_list_widget(rcmail.gui_objects.noteslist, + { multiselect:true, draggable:true, keyboard:false }); + noteslist.addEventListener('select', function(list) { + var selection_changed = list.selection.length != 1 || !me.selected_note || list.selection[0] != me.selected_note.id; + selection_changed && warn_unsaved_changes(function(){ + var note; + if (noteslist.selection.length == 1 && (note = notesdata[noteslist.selection[0]])) { + edit_note(note.uid, 'edit'); + } + else { + reset_view(); + } + }, + function(){ + // TODO: previous restore selection + list.select(me.selected_note.id); + }); + + rcmail.enable_command('delete', me.notebooks[me.selected_list] && me.notebooks[me.selected_list].editable && list.selection.length > 0); + rcmail.enable_command('sendnote', list.selection.length > 0); + rcmail.enable_command('print', list.selection.length == 1); + }) + .addEventListener('dragstart', function(e) { + folder_drop_target = null; + notebookslist.drag_start(); + }) + .addEventListener('dragmove', function(e) { + folder_drop_target = notebookslist.intersects(rcube_event.get_mouse_pos(e), true); + }) + .addEventListener('dragend', function(e) { + notebookslist.drag_end(); + + // move dragged notes to this folder + if (folder_drop_target) { + noteslist.draglayer.hide(); + move_notes(folder_drop_target); + noteslist.clear_selection(); + reset_view(); + } + folder_drop_target = null; + }) + .init(); + } + + if (settings.sort_col) { + $('#notessortmenu a.by-' + settings.sort_col).addClass('selected'); + } + + // click-handler on tags list + $(rcmail.gui_objects.notestagslist).on('click', function(e){ + var item = e.target.nodeName == 'LI' ? $(e.target) : $(e.target).closest('li'), + tag = item.data('value'); + + if (!tag) + return false; + + // reset selection on regular clicks + var index = $.inArray(tag, tagsfilter); + var shift = e.shiftKey || e.ctrlKey || e.metaKey; + + if (!shift) { + if (tagsfilter.length > 1) + index = -1; + + $('li', this).removeClass('selected'); + tagsfilter = []; + } + + // add tag to filter + if (index < 0) { + item.addClass('selected'); + tagsfilter.push(tag); + } + else if (shift) { + item.removeClass('selected'); + var a = tagsfilter.slice(0,index); + tagsfilter = a.concat(tagsfilter.slice(index+1)); + } + + filter_notes(); + + // clear text selection in IE after shift+click + if (shift && document.selection) + document.selection.empty(); + + e.preventDefault(); + return false; + }) + .mousedown(function(e){ + // disable content selection with the mouse + e.preventDefault(); + return false; + }); + + // initialize tinyMCE editor + var editor_conf = { + mode: 'textareas', + elements: 'notecontent', + apply_source_formatting: true, + theme: 'advanced', + language: settings.editor.lang, + content_css: settings.editor.editor_css, + theme_advanced_toolbar_location: 'top', + theme_advanced_toolbar_align: 'left', + theme_advanced_buttons3: '', + theme_advanced_statusbar_location: 'none', + relative_urls: false, + remove_script_host: false, + gecko_spellcheck: true, + convert_urls: false, + paste_data_images: true, + plugins: 'paste,tabfocus,searchreplace,table,inlinepopups', + theme_advanced_buttons1: 'bold,italic,underline,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,outdent,indent,blockquote,|,forecolor,backcolor,fontselect,fontsizeselect', + theme_advanced_buttons2: 'link,unlink,table,charmap,|,search,code,|,undo,redo', + setup: function(ed) { + // make links open on shift-click + ed.onClick.add(function(ed, e) { + var link = $(e.target).closest('a'); + if (link.length && e.shiftKey) { + if (!bw.mz) window.open(link.get(0).href, '_blank'); + return false; + } + }); + } + }; + + // support external configuration settings e.g. from skin + if (window.rcmail_editor_settings) + $.extend(editor_conf, window.rcmail_editor_settings); + + tinyMCE.init(editor_conf); + + if (settings.selected_list) { + notebookslist.select(settings.selected_list) + } + } + this.init = init; + + /** + * Quote HTML entities + */ + function Q(str) + { + return String(str).replace(//g, '>').replace(/"/g, '"'); + } + + /** + * Trim whitespace off the given string + */ + function trim(str) + { + return String(str).replace(/\s+$/, '').replace(/^\s+/, ''); + } + + /** + * + */ + function edit_note(uid, action) + { + if (!uid) { + noteslist.clear_selection(); + me.selected_note = { + list: me.selected_list, + uid: null, + title: rcmail.gettext('newnote','kolab_notes'), + description: '', + categories: [], + created: rcmail.gettext('now', 'kolab_notes'), + changed: rcmail.gettext('now', 'kolab_notes') + } + render_note(me.selected_note); + rcmail.enable_command('print', true); + } + else { + ui_loading = rcmail.set_busy(true, 'loading'); + rcmail.http_request('get', { _list:me.selected_list, _id:uid }, true); + } + } + + /** + * + */ + function list_edit_dialog(id) + { + if (!rcmail.gui_containers.notebookeditform) { + return false; + } + + // close show dialog first + var $dialog = rcmail.gui_containers.notebookeditform; + if ($dialog.is(':ui-dialog')) { + $dialog.dialog('close'); + } + + var list = me.notebooks[id] || { name:'', editable:true }; + var form, name; + + $dialog.html(rcmail.get_label('loading')); + $.ajax({ + type: 'GET', + dataType: 'html', + url: rcmail.url('list'), + data: { _do: (list.id ? 'form-edit' : 'form-new'), _list: { id: list.id } }, + success: function(data) { + $dialog.html(data); + rcmail.triggerEvent('kolab_notes_editform_load', list); + + // resize and reposition dialog window + form = $('#noteslistpropform'); + var win = $(window), w = win.width(), h = win.height(); + $dialog.dialog('option', { height: Math.min(h-20, form.height()+130), width: Math.min(w-20, form.width()+50) }) + .dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?) + + name = $('#noteslist-name').prop('disabled', !list.editable).val(list.editname || list.name); + name.select(); + } + }); + + // dialog buttons + var buttons = {}; + buttons[rcmail.gettext('save')] = function() { + // form is not loaded + if (!form || !form.length) + return; + + // do some input validation + if (!name.val() || name.val().length < 2) { + alert(rcmail.gettext('invalidlistproperties', 'kolab_notes')); + name.select(); + return; + } + + // post data to server + var data = form.serializeJSON(); + if (list.id) + data.id = list.id; + + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('list', { _do: (list.id ? 'edit' : 'new'), _list: data }); + $dialog.dialog('close'); + }; + + buttons[rcmail.gettext('cancel')] = function() { + $dialog.dialog('close'); + }; + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: true, + closeOnEscape: false, + title: rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'kolab_notes'), + open: function() { + $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction'); + }, + close: function() { + $dialog.html('').dialog('destroy').hide(); + }, + buttons: buttons, + minWidth: 480, + width: 640, + }).show(); + + } + + /** + * Callback from server after changing list properties + */ + function list_update(prop) + { + if (prop._reload) { + rcmail.redirect(rcmail.url('', { _list: (prop.newid || prop.id) })); + } + else if (prop.newid && prop.newid != prop.id) { + var book = $.extend({}, me.notebooks[prop.id]); + book.id = prop.newid; + book.name = prop.name; + book.listname = prop.listname; + book.editname = prop.editname || prop.name; + + me.notebooks[prop.newid] = book; + delete me.notebooks[prop.id]; + + // update treelist item + var li = $(notebookslist.get_item(prop.id)); + $('.listname', li).html(prop.listname); + notebookslist.update(prop.id, { id:book.id, html:li.html() }); + + // link all loaded note records to the new list id + if (me.selected_list == prop.id) { + me.selected_list = prop.newid; + for (var k in notesdata) { + if (notesdata[k].list == prop.id) { + notesdata[k].list = book.id; + } + } + notebookslist.select(prop.newid); + } + } + } + + + /** + * + */ + function list_remove(id) + { + var list = me.notebooks[id]; + if (list && confirm(rcmail.gettext('deletenotebookconfirm', 'kolab_notes'))) { + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('list', { _do: 'delete', _list: { id: list.id } }); + } + } + + /** + * Callback from server on list delete command + */ + function list_destroy(prop) + { + if (!me.notebooks[prop.id]) { + return; + } + + notebookslist.remove(prop.id); + delete me.notebooks[prop.id]; + + if (me.selected_list == prop.id) { + for (id in me.notebooks) { + if (me.notebooks[id]) { + notebookslist.select(id); + break; + } + } + } + } + + /** + * Change notes list sort order + */ + function list_set_sort(col) + { + if (settings.sort_col != col) { + settings.sort_col = col; + $('#notessortmenu a').removeClass('selected').filter('.by-' + col).addClass('selected'); + rcmail.save_pref({ name: 'kolab_notes_sort_col', value: col }); + + // re-sort table in DOM + $(noteslist.tbody).children().sortElements(function(la, lb){ + var a_id = String(la.id).replace(/^rcmrow/, ''), + b_id = String(lb.id).replace(/^rcmrow/, ''), + a = notesdata[a_id], + b = notesdata[b_id]; + + if (!a || !b) { + return 0; + } + else if (settings.sort_col == 'title') { + return String(a.title).toLowerCase() > String(b.title).toLowerCase() ? 1 : -1; + } + else { + return b.changed_ - a.changed_; + } + }); + } + } + + /** + * Execute search + */ + function quicksearch() + { + var q; + if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) { + var id = 'search-'+q; + + // ignore if query didn't change + if (search_request == id) + return; + + warn_unsaved_changes(function(){ + search_request = id; + search_query = q; + + fetch_notes(); + }, + function(){ + reset_search(); + }); + } + else { // empty search input equals reset + reset_search(); + } + } + + /** + * Reset search and get back to normal listing + */ + function reset_search() + { + $(rcmail.gui_objects.qsearchbox).val(''); + + if (search_request) { + search_request = search_query = null; + fetch_notes(); + } + } + + /** + * + */ + function fetch_notes(id) + { + if (rcmail.busy) + return; + + if (id && id != me.selected_list) { + me.selected_list = id; + } + + ui_loading = rcmail.set_busy(true, 'loading'); + rcmail.http_request('fetch', { _list:me.selected_list, _q:search_query }, true); + + reset_view(); + noteslist.clear(true); + notesdata = {}; + tagsfilter = []; + } + + function filter_notes() + { + // tagsfilter + var note, tr, match; + for (var id in noteslist.rows) { + tr = noteslist.rows[id].obj; + note = notesdata[id]; + match = note.categories && note.categories.length; + for (var i=0; match && note && i < tagsfilter.length; i++) { + if ($.inArray(tagsfilter[i], note.categories) < 0) + match = false; + } + + if (match || !tagsfilter.length) { + $(tr).show(); + } + else { + $(tr).hide(); + } + + if (me.selected_note && me.selected_note.uid == note.uid && !match) { + warn_unsaved_changes(function(){ + me.selected_note = null; + noteslist.clear_selection(); + }, function(){ + tagsfilter = []; + filter_notes(); + update_tagcloud(); + }); + } + } + } + + /** + * + */ + function data_ready(data) + { + data.data.sort(function(a,b){ + if (settings.sort_col == 'title') { + return String(a.title).toLowerCase() > String(b.title).toLowerCase() ? 1 : -1; + } + else { + return b.changed_ - a.changed_; + } + }); + + var i, id, rec; + for (i=0; data.data && i < data.data.length; i++) { + rec = data.data[i]; + rec.id = rcmail.html_identifier_encode(rec.uid); + noteslist.insert_row({ + id: 'rcmrow' + rec.id, + cols: [ + { className:'title', innerHTML:Q(rec.title) }, + { className:'date', innerHTML:Q(rec.changed || '') } + ] + }); + + notesdata[rec.id] = rec; + } + + render_tagslist(data.tags || [], !data.search) + rcmail.set_busy(false, 'loading', ui_loading); + + // select the single result + if (data.data.length == 1) { + noteslist.select(data.data[0].id); + } + else if (settings.selected_id) { + noteslist.select(settings.selected_id); + delete settings.selected_id; + } + else if (me.selected_note && notesdata[me.selected_note.id]) { + noteslist.select(me.selected_note.id); + } + } + + /** + * + */ + function render_note(data) + { + rcmail.set_busy(false, 'loading', ui_loading); + + if (!data) { + rcmail.display_message(rcmail.get_label('recordnotfound', 'kolab_notes'), 'error'); + return; + } + + var list = me.notebooks[data.list] || me.notebooks[me.selected_list]; + content = $('#notecontent').val(data.description), + readonly = data.readonly || !list.editable; + $('.notetitle', rcmail.gui_objects.noteviewtitle).val(data.title).prop('disabled', readonly); + $('.dates .notecreated', rcmail.gui_objects.noteviewtitle).html(Q(data.created || '')); + $('.dates .notechanged', rcmail.gui_objects.noteviewtitle).html(Q(data.changed || '')); + if (data.created || data.changed) { + $('.dates', rcmail.gui_objects.noteviewtitle).show(); + } + + // tag-edit line + var tagline = $('.tagline', rcmail.gui_objects.noteviewtitle).empty().show(); + $.each(typeof data.categories == 'object' && data.categories.length ? data.categories : [''], function(i,val){ + $('') + .attr('name', 'tags[]') + .attr('tabindex', '2') + .addClass('tag') + .val(val) + .appendTo(tagline); + }); + + if (!data.categories || !data.categories.length) { + $('').addClass('placeholder').html(rcmail.gettext('notags', 'kolab_notes')).appendTo(tagline); + } + + $('.tagline input.tag', rcmail.gui_objects.noteviewtitle).tagedit({ + animSpeed: 100, + allowEdit: false, + allowAdd: !readonly, + allowDelete: !readonly, + checkNewEntriesCaseSensitive: false, + autocompleteOptions: { source: tags, minLength: 0, noCheck: true }, + texts: { removeLinkTitle: rcmail.gettext('removetag', 'kolab_notes') } + }) + + if (!readonly) { + $('.tagedit-list', rcmail.gui_objects.noteviewtitle) + .on('click', function(){ $('.tagline .placeholder').hide(); }); + } + + me.selected_note = data; + me.selected_note.id = rcmail.html_identifier_encode(data.uid); + rcmail.enable_command('save', list.editable && !data.readonly); + + var html = data.html || data.description; + + // convert plain text to HTML and make URLs clickable + if (!data.html || !html.match(/<(html|body)/)) { + html = text2html(html); + } + + var node, editor = tinyMCE.get('notecontent'); + if (!readonly && editor) { + $(rcmail.gui_objects.notesdetailview).hide(); + $(rcmail.gui_objects.noteseditform).show(); + editor.setContent(html); + node = editor.getContentAreaContainer().childNodes[0]; + if (node) node.tabIndex = content.get(0).tabIndex; + + if (me.selected_note.uid) + editor.getBody().focus(); + else + $('.notetitle', rcmail.gui_objects.noteviewtitle).focus().select(); + + // read possibly re-formatted content back from editor for later comparison + me.selected_note.description = editor.getContent({ format:'html' }) + } + else { + $(rcmail.gui_objects.noteseditform).hide(); + $(rcmail.gui_objects.notesdetailview).html(html).show(); + } + + // Trigger resize (needed for proper editor resizing) + $(window).resize(); + } + + /** + * Convert the given plain text to HTML contents to be displayed in editor + */ + function text2html(str) + { + // simple link parser (similar to rcube_string_replacer class in PHP) + var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})', + url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-', + link_pattern = new RegExp('([hf]t+ps?://|www.)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig'), + link_replace = function(matches, p1, p2) { + var url = (p1 == 'www.' ? 'http://' : '') + p1 + p2; + return '' + p1 + p2 + ''; + }; + + return '
' + Q(str).replace(link_pattern, link_replace) + '
'; + } + + /** + * Open a new window to print the currently selected note + */ + function print_note() + { + var printwin, data; + if (me.selected_note && (printwin = rcmail.open_window(settings.print_template))) { + data = get_save_data(); + $(printwin).load(function(){ + printwin.document.title = data.title; + $('#notetitle', printwin.document).html(Q(data.title)); + $('#notebody', printwin.document).html(data.description); + $('#notetags', printwin.document).html('' + data.categories.join('') + ''); + $('#notecreated', printwin.document).html(Q(me.selected_note.created)); + $('#notechanged', printwin.document).html(Q(me.selected_note.changed)); + printwin.print(); + }); + } + } + + /** + * Redirect to message compose screen with UIDs of notes to be appended + */ + function send_note() + { + var uids = []; + for (var rec, i=0; i < noteslist.selection.length; i++) { + if (rec = notesdata[noteslist.selection[i]]) { + uids.push(rec.uid); + // TODO: check if rec.uid == me.selected_note.uid and unsaved changes + } + } + + if (uids.length) { + rcmail.goto_url('mail/compose', { _with_notes: uids.join(','), _notes_list: me.selected_list }, true); + } + } + + /** + * + */ + function render_tagslist(newtags, replace) + { + if (replace) { + tags = newtags; + } + else { + var append = []; + for (var i=0; i < newtags.length; i++) { + if ($.inArray(newtags[i], tags) < 0) + append.push(newtags[i]); + } + if (!append.length) { + update_tagcloud(); + return; // nothing to be added + } + tags = tags.concat(append); + } + + // sort tags first + tags.sort(function(a,b){ + return a.toLowerCase() > b.toLowerCase() ? 1 : -1; + }) + + var widget = $(rcmail.gui_objects.notestagslist).html(''); + + // append tags to tag cloud + $.each(tags, function(i, tag){ + li = $('
  • ').attr('rel', tag).data('value', tag) + .html(Q(tag) + '') + .appendTo(widget) + .draggable({ + addClasses: false, + revert: 'invalid', + revertDuration: 300, + helper: tag_draggable_helper, + start: tag_draggable_start, + appendTo: 'body', + cursor: 'pointer' + }); + }); + + update_tagcloud(); + } + + /** + * Display the given counts to each tag and set those inactive which don't + * have any matching records in the current view. + */ + function update_tagcloud(counts) + { + // compute counts first by iterating over all visible task items + if (typeof counts == 'undefined') { + counts = {}; + $.each(notesdata, function(id, rec){ + for (var t, j=0; rec && rec.categories && j < rec.categories.length; j++) { + t = rec.categories[j]; + if (typeof counts[t] == 'undefined') + counts[t] = 0; + counts[t]++; + } + }); + } + + $(rcmail.gui_objects.notestagslist).children('li').each(function(i,li){ + var elem = $(li), tag = elem.attr('rel'), + count = counts[tag] || 0; + + elem.children('.count').html(count+''); + if (count == 0) elem.addClass('inactive'); + else elem.removeClass('inactive'); + + if (tagsfilter && tagsfilter.length && $.inArray(tag, tagsfilter)) { + elem.addClass('selected'); + } + else { + elem.removeClass('selected'); + } + }); + } + + /** + * Callback from server after saving a note record + */ + function update_note(data) + { + data.id = rcmail.html_identifier_encode(data.uid); + + var row, is_new = notesdata[data.id] == undefined + notesdata[data.id] = data; + + if (is_new || me.selected_note && data.id == me.selected_note.id) { + render_note(data); + render_tagslist(data.categories || []); + } + else if (data.categories) { + render_tagslist(data.categories); + } + + // add list item on top + if (is_new) { + noteslist.insert_row({ + id: 'rcmrow' + data.id, + cols: [ + { className:'title', innerHTML:Q(data.title) }, + { className:'date', innerHTML:Q(data.changed || '') } + ] + }, true); + + noteslist.select(data.id); + } + // update list item + else if (row = noteslist.rows[data.id]) { + $('.title', row.obj).html(Q(data.title)); + $('.date', row.obj).html(Q(data.changed || '')); + // TODO: move to top + } + } + + /** + * + */ + function reset_view() + { + me.selected_note = null; + $('.notetitle', rcmail.gui_objects.noteviewtitle).val(''); + $('.tagline, .dates', rcmail.gui_objects.noteviewtitle).hide(); + $(rcmail.gui_objects.noteseditform).hide(); + $(rcmail.gui_objects.notesdetailview).hide(); + rcmail.enable_command('save', false); + } + + /** + * Collect data from the edit form and submit it to the server + */ + function save_note() + { + if (!me.selected_note) { + return false; + } + + var savedata = get_save_data(); + + // do some input validation + if (savedata.title == '') { + alert(rcmail.gettext('entertitle', 'kolab_notes')); + $('.notetitle', rcmail.gui_objects.noteviewtitle).focus(); + return false; + } + + if (check_change_state(savedata)) { + rcmail.lock_form(rcmail.gui_objects.noteseditform, true); + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('action', { _data: savedata, _do: savedata.uid?'edit':'new' }, true); + } + else { + rcmail.display_message(rcmail.get_label('nochanges', 'kolab_notes'), 'info'); + } + } + + /** + * Collect updated note properties from edit form for saving + */ + function get_save_data() + { + var editor = tinyMCE.get('notecontent'); + var savedata = { + title: trim($('.notetitle', rcmail.gui_objects.noteviewtitle).val()), + description: editor ? editor.getContent({ format:'html' }) : $('#notecontent').val(), + list: me.selected_note.list || me.selected_list, + uid: me.selected_note.uid, + categories: [] + }; + + // collect tags + $('.tagedit-list input[type="hidden"]', rcmail.gui_objects.noteviewtitle).each(function(i, elem){ + if (elem.value) + savedata.categories.push(elem.value); + }); + // including the "pending" one in the text box + var newtag = $('#tagedit-input').val(); + if (newtag != '') { + savedata.categories.push(newtag); + } + + return savedata; + } + + /** + * Check if the currently edited note record was changed + */ + function check_change_state(data) + { + if (!me.selected_note || me.selected_note.readonly || !me.notebooks[me.selected_note.list || me.selected_list].editable) { + return false; + } + + var savedata = data || get_save_data(); + + return savedata.title != me.selected_note.title + || savedata.description != me.selected_note.description + || savedata.categories.join(',') != (me.selected_note.categories || []).join(','); + } + + /** + * Check for unsaved changes and warn the user + */ + function warn_unsaved_changes(ok, nok) + { + if (typeof ok != 'function') + ok = function(){ }; + if (typeof nok != 'function') + nok = function(){ }; + + if (check_change_state()) { + var dialog, buttons = []; + + buttons.push({ + text: rcmail.gettext('discard', 'kolab_notes'), + click: function() { + dialog.dialog('close'); + ok(); + } + }); + + buttons.push({ + text: rcmail.gettext('save'), + click: function() { + save_note(); + dialog.dialog('close'); + ok(); + } + }); + + buttons.push({ + text: rcmail.gettext('abort', 'kolab_notes'), + click: function() { + dialog.dialog('close'); + nok(); + } + }); + + var options = { + width: 460, + resizable: false, + closeOnEscape: false, + dialogClass: 'warning', + open: function(event, ui) { + $(this).parent().find('.ui-dialog-titlebar-close').hide(); + $(this).parent().find('.ui-button').first().addClass('mainaction').focus(); + } + }; + + // open jquery UI dialog + dialog = rcmail.show_popup_dialog( + rcmail.gettext('discardunsavedchanges', 'kolab_notes'), + rcmail.gettext('unsavedchanges', 'kolab_notes'), + buttons, + options + ); + + return false; + } + + if (typeof ok == 'function') { + ok(); + } + + return true; + } + + /** + * + */ + function delete_notes() + { + if (!noteslist.selection.length) { + return false; + } + + if (confirm(rcmail.gettext('deletenotesconfirm','kolab_notes'))) { + var rec, id, uids = []; + for (var i=0; i < noteslist.selection.length; i++) { + id = noteslist.selection[i]; + rec = notesdata[id]; + if (rec) { + noteslist.remove_row(id); + uids.push(rec.uid); + delete notesdata[id]; + } + } + + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('action', { _data: { uid: uids.join(','), list: me.selected_list }, _do: 'delete' }, true); + + reset_view(); + update_tagcloud(); + } + } + + /** + * + */ + function move_notes(list_id) + { + var rec, id, uids = []; + for (var i=0; i < noteslist.selection.length; i++) { + id = noteslist.selection[i]; + rec = notesdata[id]; + if (rec) { + noteslist.remove_row(id); + uids.push(rec.uid); + delete notesdata[id]; + } + } + + if (uids.length) { + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('action', { _data: { uid: uids.join(','), list: me.selected_list, to: list_id }, _do: 'move' }, true); + } + } + + /* Helper functions for drag & drop functionality of tags */ + + function tag_draggable_helper() + { + if (!tag_draghelper) + tag_draghelper = $('
    '); + else + tag_draghelper.html(''); + + $(this).clone().addClass('tag').appendTo(tag_draghelper); + return tag_draghelper; + } + + function tag_draggable_start(event, ui) + { + // register notes list to receive drop events + $('li', rcmail.gui_objects.noteslist).droppable({ + hoverClass: 'droptarget', + accept: tag_droppable_accept, + drop: tag_draggable_dropped, + addClasses: false + }); + + // allow to drop tags onto edit form title + $(rcmail.gui_objects.noteviewtitle).droppable({ + drop: function(event, ui){ + $('#tagedit-input').val(ui.draggable.data('value')).trigger('transformToTag'); + }, + addClasses: false + }) + } + + function tag_droppable_accept(draggable) + { + if (rcmail.busy) + return false; + + var tag = draggable.data('value'), + drop_id = $(this).attr('id').replace(/^rcmrow/, ''), + drop_rec = notesdata[drop_id]; + + // target already has this tag assigned + if (!drop_rec || (drop_rec.categories && $.inArray(tag, drop_rec.categories) >= 0)) { + return false; + } + + return true; + } + + function tag_draggable_dropped(event, ui) + { + var drop_id = $(this).attr('id').replace(/^rcmrow/, ''), + tag = ui.draggable.data('value'), + rec = notesdata[drop_id], + savedata; + + if (rec && rec.id) { + savedata = me.selected_note && rec.uid == me.selected_note.uid ? get_save_data() : $.extend({}, rec); + + if (savedata.id) delete savedata.id; + if (savedata.html) delete savedata.html; + + if (!savedata.categories) + savedata.categories = []; + savedata.categories.push(tag); + + rcmail.lock_form(rcmail.gui_objects.noteseditform, true); + saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata'); + rcmail.http_post('action', { _data: savedata, _do: 'edit' }, true); + } + } + +} + + +// extend jQuery +// from http://james.padolsey.com/javascript/sorting-elements-with-jquery/ +jQuery.fn.sortElements = (function(){ + var sort = [].sort; + + return function(comparator, getSortable) { + getSortable = getSortable || function(){ return this }; + + var last = null; + return sort.call(this, comparator).each(function(i){ + // at this point the array is sorted, so we can just detach each one from wherever it is, and add it after the last + var node = $(getSortable.call(this)); + var parent = node.parent(); + if (last) last.after(node); + else parent.prepend(node); + last = node; + }); + }; +})(); + + +/* notes plugin UI initialization */ +var kolabnotes; +window.rcmail && rcmail.addEventListener('init', function(evt) { + kolabnotes = new rcube_kolab_notes_ui(rcmail.env.kolab_notes_settings); + kolabnotes.init(); +}); + diff --git a/plugins/kolab_notes/skins/larry/editor.css b/plugins/kolab_notes/skins/larry/editor.css new file mode 100644 index 00000000..943b1184 --- /dev/null +++ b/plugins/kolab_notes/skins/larry/editor.css @@ -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%; +} diff --git a/plugins/kolab_notes/skins/larry/folder_icons.png b/plugins/kolab_notes/skins/larry/folder_icons.png new file mode 120000 index 00000000..2a6ab2be --- /dev/null +++ b/plugins/kolab_notes/skins/larry/folder_icons.png @@ -0,0 +1 @@ +../../../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 new file mode 100644 index 00000000..97a7421c --- /dev/null +++ b/plugins/kolab_notes/skins/larry/notes.css @@ -0,0 +1,339 @@ +/** + * Kolab Notes plugin styles for skin "Larry" + * + * Copyright (C) 2014, Kolab Systems AG + * 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; +} \ No newline at end of file diff --git a/plugins/kolab_notes/skins/larry/print.css b/plugins/kolab_notes/skins/larry/print.css new file mode 100644 index 00000000..2744aaac --- /dev/null +++ b/plugins/kolab_notes/skins/larry/print.css @@ -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; +} \ No newline at end of file diff --git a/plugins/kolab_notes/skins/larry/sprites.png b/plugins/kolab_notes/skins/larry/sprites.png new file mode 100644 index 00000000..184cef41 Binary files /dev/null and b/plugins/kolab_notes/skins/larry/sprites.png differ diff --git a/plugins/kolab_notes/skins/larry/tagedit.css b/plugins/kolab_notes/skins/larry/tagedit.css new file mode 120000 index 00000000..f25378ff --- /dev/null +++ b/plugins/kolab_notes/skins/larry/tagedit.css @@ -0,0 +1 @@ +../../../tasklist/skins/larry/tagedit.css \ No newline at end of file diff --git a/plugins/kolab_notes/skins/larry/templates/kolabacl.html b/plugins/kolab_notes/skins/larry/templates/kolabacl.html new file mode 100644 index 00000000..ed9b0c7e --- /dev/null +++ b/plugins/kolab_notes/skins/larry/templates/kolabacl.html @@ -0,0 +1,26 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + + + + + + diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html new file mode 100644 index 00000000..75fa1889 --- /dev/null +++ b/plugins/kolab_notes/skins/larry/templates/notes.html @@ -0,0 +1,138 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + +
    +
    + + + + + +
    + + + +
    +
    + + + +
    +
    +

    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    + + + +
    + +
    +
    +
    + +
    + + + +
    +
      +
    • +
    • +
    • +
    +
    + +
    +
      +
    • +
    • +
    +
    + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/plugins/kolab_notes/skins/larry/templates/print.html b/plugins/kolab_notes/skins/larry/templates/print.html new file mode 100644 index 00000000..0387e93c --- /dev/null +++ b/plugins/kolab_notes/skins/larry/templates/print.html @@ -0,0 +1,25 @@ + + + +Print + + + + + +
    +

    #Title

    +
    #Tags
    +
    + + #Created + + #Changed +
    +
    + +
    +#Body +
    + + \ No newline at end of file diff --git a/plugins/libkolab/SQL/mysql.initial.sql b/plugins/libkolab/SQL/mysql.initial.sql index 89b7244d..2aa046d7 100644 --- a/plugins/libkolab/SQL/mysql.initial.sql +++ b/plugins/libkolab/SQL/mysql.initial.sql @@ -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 */; diff --git a/plugins/libkolab/SQL/mysql/2014032700.sql b/plugins/libkolab/SQL/mysql/2014032700.sql new file mode 100644 index 00000000..a45fae30 --- /dev/null +++ b/plugins/libkolab/SQL/mysql/2014032700.sql @@ -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`); diff --git a/plugins/libkolab/SQL/mysql/2014040900.sql b/plugins/libkolab/SQL/mysql/2014040900.sql new file mode 100644 index 00000000..61649c14 --- /dev/null +++ b/plugins/libkolab/SQL/mysql/2014040900.sql @@ -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; diff --git a/plugins/libkolab/lib/kolab_format_file.php b/plugins/libkolab/lib/kolab_format_file.php index 5f73bf15..34c0ca67 100644 --- a/plugins/libkolab/lib/kolab_format_file.php +++ b/plugins/libkolab/lib/kolab_format_file.php @@ -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'; diff --git a/plugins/libkolab/lib/kolab_format_note.php b/plugins/libkolab/lib/kolab_format_note.php index 1f49deeb..08b77350 100644 --- a/plugins/libkolab/lib/kolab_format_note.php +++ b/plugins/libkolab/lib/kolab_format_note.php @@ -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], '')) { + $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))); + } + } diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php index 718d95eb..606c5d54 100644 --- a/plugins/libkolab/lib/kolab_storage_cache.php +++ b/plugins/libkolab/lib/kolab_storage_cache.php @@ -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 ') . diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index 1580314b..d625cc5e 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -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) { diff --git a/plugins/tasklist/jquery.tagedit.js b/plugins/tasklist/jquery.tagedit.js old mode 100755 new mode 100644 index d70cb327..0b3ab766 --- a/plugins/tasklist/jquery.tagedit.js +++ b/plugins/tasklist/jquery.tagedit.js @@ -5,13 +5,14 @@ * Examples and documentation at: tagedit.webwork-albrecht.de * * Copyright (c) 2010 Oliver Albrecht +* Copyright (c) 2012 Thomas Brüderli * * 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 += '
  • '; html += '' + $(this).val() + ''; html += ''; - html += 'x'; + if (options.allowDelete) + html += 'x'; html += '
  • '; } } @@ -155,7 +159,8 @@ // put an input field at the End // Put an empty element at the end html = '
  • '; - html += ''; + if (options.allowAdd) + html += ''; html += '
  • '; html += ''; @@ -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 = $('').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, '&').replace(/\s/g,' ').replace(//g, '>'); - 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 = $('').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, '&').replace(/\s/g,' ').replace(//g, '>'); + 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); \ No newline at end of file diff --git a/plugins/tasklist/skins/larry/iehacks.css b/plugins/tasklist/skins/larry/iehacks.css index 9b51999f..24ebaadd 100644 --- a/plugins/tasklist/skins/larry/iehacks.css +++ b/plugins/tasklist/skins/larry/iehacks.css @@ -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; } diff --git a/plugins/tasklist/skins/larry/tagedit.css b/plugins/tasklist/skins/larry/tagedit.css new file mode 100644 index 00000000..a26b5ab0 --- /dev/null +++ b/plugins/tasklist/skins/larry/tagedit.css @@ -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; +} + diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index 3d0da623..30707dcd 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -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; -} - diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index 9b5a3a68..b9a47855 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -19,7 +19,7 @@

    - +
    diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index c592e3d9..cba90780 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -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(); diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index c60ae142..d53b0a8c 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -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; diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index d3537ae0..f2a90bef 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -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'); } /**