Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 92e85e30..cdf61bd9 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -1,724 +1,797 @@
<?php
/**
* Kolab notes module
*
* Adds simple notes management features to the web client
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_notes extends rcube_plugin
{
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('kolab_notes_sort_col');
public $rc;
private $ui;
private $lists;
private $folders;
private $cache = array();
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$this->require_plugin('libkolab');
$this->rc = rcube::get_instance();
$this->register_task('notes');
// load plugin configuration
$this->load_config();
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the notes module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('notes_disabled', false) || !$this->rc->config->get('notes_enabled', true)) {
return;
}
// load localizations
$this->add_texts('localization/', $args['task'] == 'notes' && !$args['action']);
+ $this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'notes') {
// register task actions
$this->register_action('index', array($this, 'notes_view'));
$this->register_action('fetch', array($this, 'notes_fetch'));
$this->register_action('get', array($this, 'note_record'));
$this->register_action('action', array($this, 'note_action'));
$this->register_action('list', array($this, 'list_action'));
}
+ else if ($args['task'] == 'mail') {
+ $this->add_hook('message_compose', array($this, 'mail_message_compose'));
+ }
if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'folder-acl')) {
require_once($this->home . '/kolab_notes_ui.php');
$this->ui = new kolab_notes_ui($this);
$this->ui->init();
}
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists($force = false)
{
// already read sources
if (isset($this->lists) && !$force)
return $this->lists;
// get all folders that have type "task"
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('note'));
$this->lists = $this->folders = array();
// find default folder
$default_index = 0;
foreach ($folders as $i => $folder) {
if ($folder->default)
$default_index = $i;
}
// put default folder on top of the list
if ($default_index > 0) {
$default_folder = $folders[$default_index];
unset($folders[$default_index]);
array_unshift($folders, $default_folder);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$listnames = array();
// include virtual folders for a full folder tree
if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
$folders = kolab_storage::folder_hierarchy($folders);
foreach ($folders as $folder) {
$utf7name = $folder->name;
$path_imap = explode($delim, $utf7name);
$editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP'); // pop off raw name part
$path_imap = join($delim, $path_imap);
$fullname = $folder->get_name();
$listname = kolab_storage::folder_displayname($fullname, $listnames);
// special handling for virtual folders
if ($folder->virtual) {
$list_id = kolab_storage::folder_id($utf7name);
$this->lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'virtual' => true,
'editable' => false,
);
continue;
}
if ($folder->get_namespace() == 'personal') {
$norename = false;
$readonly = false;
$alarms = true;
}
else {
$alarms = false;
$readonly = true;
if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$readonly = false;
}
$info = $folder->get_folder_info();
$norename = $readonly || $info['norename'] || $info['protected'];
}
$list_id = kolab_storage::folder_id($utf7name);
$item = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'editname' => $editname,
'editable' => !$readonly,
'norename' => $norename,
'parentfolder' => $path_imap,
'default' => $folder->default,
'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
$this->lists[$item['id']] = $item;
$this->folders[$item['id']] = $folder;
$this->folders[$folder->name] = $folder;
}
}
/**
* Get a list of available folders from this source
*/
public function get_lists()
{
$this->_read_lists();
// attempt to create a default folder for this user
if (empty($this->lists)) {
#if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
# $this->_read_lists(true);
}
return $this->lists;
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function notes_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('kolab_notes.notes');
}
/**
* Handler to retrieve note records for the given list and/or search query
*/
public function notes_fetch()
{
$search = rcube_utils::get_input_value('_q', RCUBE_INPUT_GPC, true);
$list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC);
$data = $this->notes_data($this->list_notes($list, $search), $tags);
$this->rc->output->command('plugin.data_ready', array('list' => $list, 'search' => $search, 'data' => $data, 'tags' => array_values($tags)));
}
/**
* Convert the given note records for delivery to the client
*/
protected function notes_data($records, &$tags)
{
$tags = array();
foreach ($records as $i => $rec) {
unset($records[$i]['description']);
$this->_client_encode($records[$i]);
foreach ((array)$rec['categories'] as $tag) {
$tags[] = $tag;
}
}
$tags = array_unique($tags);
return $records;
}
/**
* Read note records for the given list from the storage backend
*/
protected function list_notes($list_id, $search = null)
{
$results = array();
// query Kolab storage
$query = array();
// full text search (only works with cache enabled)
if (strlen($search)) {
$words = array_filter(rcube_utils::normalize_string(mb_strtolower($search), true));
foreach ($words as $word) {
if (strlen($word) > 2) { // only words > 3 chars are stored in DB
$query[] = array('words', '~', $word);
}
}
}
$this->_read_lists();
if ($folder = $this->folders[$list_id]) {
foreach ($folder->select($query) as $record) {
// post-filter search results
if (strlen($search)) {
$matches = 0;
$contents = mb_strtolower(
$record['title'] .
($this->is_html($record) ? strip_tags($record['description']) : $record['description']) .
join(' ', (array)$record['categories'])
);
foreach ($words as $word) {
if (mb_strpos($contents, $word) !== false) {
$matches++;
}
}
// skip records not matching all search words
if ($matches < count($words)) {
continue;
}
}
$record['list'] = $list_id;
$results[] = $record;
}
}
return $results;
}
/**
* Handler for delivering a full note record to the client
*/
public function note_record()
{
$data = $this->get_note(array(
'uid' => rcube_utils::get_input_value('_id', RCUBE_INPUT_GPC),
'list' => rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC),
));
// encode for client use
if (is_array($data)) {
$this->_client_encode($data);
}
$this->rc->output->command('plugin.render_note', $data);
}
/**
* Get the full note record identified by the given UID + Lolder identifier
*/
public function get_note($note)
{
if (is_array($note)) {
$uid = $note['uid'] ?: $note['id'];
$list_id = $note['list'];
}
else {
$uid = $note;
}
// deliver from in-memory cache
$key = $list_id . ':' . $uid;
if ($this->cache[$key]) {
return $this->cache[$key];
}
$this->_read_lists();
if ($list_id) {
if ($folder = $this->folders[$list_id]) {
return $folder->get_object($uid);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->folders as $list_id => $folder) {
if ($result = $folder->get_object($uid)) {
$result['list'] = $list_id;
return $result;
}
}
}
return false;
}
/**
* Helper method to encode the given note record for use in the client
*/
private function _client_encode(&$note)
{
foreach ($note as $key => $prop) {
if ($key[0] == '_' || $key == 'x-custom') {
unset($note[$key]);
}
}
foreach (array('created','changed') as $key) {
if (is_object($note[$key]) && $note[$key] instanceof DateTime) {
$note[$key.'_'] = $note[$key]->format('U');
$note[$key] = $this->rc->format_date($note[$key]);
}
}
// clean HTML contents
if (!empty($note['description']) && $this->is_html($note)) {
$note['html'] = $this->_wash_html($note['description']);
}
return $note;
}
/**
* Handler for client-initiated actions on a single note record
*/
public function note_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_POST);
$note = rcube_utils::get_input_value('_data', RCUBE_INPUT_POST, true);
$success = false;
switch ($action) {
case 'new':
$temp_id = $rec['tempid'];
case 'edit':
if ($success = $this->save_note($note)) {
$refresh = $this->get_note($note);
$refresh['tempid'] = $temp_id;
}
break;
case 'move':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->move_note($note, $note['to']))) {
$refresh = $this->get_note($note);
break;
}
}
break;
case 'delete':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->delete_note($note))) {
$refresh = $this->get_note($note);
break;
}
}
break;
}
// show confirmation/error message
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$this->rc->output->show_message('errorsaving', 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
$this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
}
}
/**
* Update an note record with the given data
*
* @param array Hash array with note properties (id, list)
* @return boolean True on success, False on error
*/
private function save_note(&$note)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
// moved from another folder
if ($note['_fromlist'] && ($fromfolder = $this->folders[$note['_fromlist']])) {
if (!$fromfolder->move($note['uid'], $folder->name))
return false;
unset($note['_fromlist']);
}
// load previous version of this record to merge
if ($note['uid']) {
$old = $folder->get_object($note['uid']);
if (!$old || PEAR::isError($old))
return false;
// merge existing properties if the update isn't complete
if (!isset($note['title']) || !isset($note['description']))
$note += $old;
}
// generate new note object from input
$object = $this->_write_preprocess($note, $old);
$saved = $folder->save($object, 'note', $note['uid']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving note object to Kolab server"),
true, false);
$saved = false;
}
else {
$note = $object;
$note['list'] = $list_id;
// cache this in memory for later read
$key = $list_id . ':' . $note['uid'];
$this->cache[$key] = $note;
}
return $saved;
}
/**
* Move the given note to another folder
*/
function move_note($note, $list_id)
{
$this->_read_lists();
$tofolder = $this->folders[$list_id];
$fromfolder = $this->folders[$note['list']];
if ($fromfolder && $tofolder) {
return $fromfolder->move($note['uid'], $tofolder->name);
}
return false;
}
/**
* Remove a single note record from the backend
*
* @param array Hash array with note properties (id, list)
* @param boolean Remove record irreversible (mark as deleted otherwise)
* @return boolean True on success, False on error
*/
public function delete_note($note, $force = true)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
return $folder->delete($note['uid'], $force);
}
/**
* Handler for client requests to list (aka folder) actions
*/
public function list_action()
{
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_GPC);
$list = rcube_utils::get_input_value('_list', RCUBE_INPUT_GPC, true);
$success = $update_cmd = false;
switch ($action) {
case 'form-new':
case 'form-edit':
$this->_read_lists();
echo $this->ui->list_editform($action, $this->lists[$list['id']], $this->folders[$list['id']]);
exit;
case 'new':
$list['type'] = 'note';
$list['subscribed'] = true;
$folder = kolab_storage::folder_update($list);
if ($folder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['id'] = kolab_storage::folder_id($folder);
$list['_reload'] = true;
}
break;
case 'edit':
$this->_read_lists();
$oldparent = $this->lists[$list['id']]['parentfolder'];
$newfolder = kolab_storage::folder_update($list);
if ($newfolder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['newid'] = kolab_storage::folder_id($newfolder);
$list['_reload'] = $list['parent'] != $oldparent;
// compose the new display name
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$path_imap = explode($delim, $newfolder);
$list['name'] = kolab_storage::object_name($newfolder);
$list['editname'] = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
$list['listname'] = str_repeat('&nbsp;&nbsp;&nbsp;', count($path_imap)) . '&raquo; ' . $list['editname'];
}
break;
case 'delete':
$this->_read_lists();
$folder = $this->folders[$list['id']];
if ($folder && kolab_storage::folder_delete($folder->name)) {
$success = true;
$update_cmd = 'plugin.destroy_list';
}
else {
$save_error = $this->gettext(kolab_storage::$last_error);
}
break;
}
$this->rc->output->command('plugin.unlock_saving');
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
if ($update_cmd) {
$this->rc->output->command($update_cmd, $list);
}
}
else {
$error_msg = $this->gettext('errorsaving') . ($save_error ? ': ' . $save_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
}
+ /**
+ * Hook to add note attachments to message compose if the according parameter is present.
+ * This completes the 'send note by mail' feature.
+ */
+ public function mail_message_compose($args)
+ {
+ if (!empty($args['param']['with_notes'])) {
+ $uids = explode(',', $args['param']['with_notes']);
+ $list = $args['param']['notes_list'];
+ $attachments = array();
+ foreach ($uids as $uid) {
+ if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
+ $args['attachments'][] = array(
+ 'name' => abbreviate_string($note['title'], 50, ''),
+ 'mimetype' => 'message/rfc822',
+ 'data' => $this->note2message($note),
+ );
+
+ if (empty($args['param']['subject'])) {
+ $args['param']['subject'] = $note['title'];
+ }
+ }
+ }
+
+ unset($args['param']['with_notes'], $args['param']['notes_list']);
+ }
+
+ return $args;
+ }
+
/**
* Determine whether the given note is HTML formatted
*/
private function is_html($note)
{
// check for opening and closing <html> or <body> tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m) && strpos($note['description'], '</'.$m[1].'>') > 0);
}
+ /**
+ * Build an RFC 822 message from the given note
+ */
+ private function note2message($note)
+ {
+ $message = new Mail_mime("\r\n");
+
+ $message->setParam('text_encoding', '8bit');
+ $message->setParam('html_encoding', 'quoted-printable');
+ $message->setParam('head_encoding', 'quoted-printable');
+ $message->setParam('head_charset', RCUBE_CHARSET);
+ $message->setParam('html_charset', RCUBE_CHARSET);
+ $message->setParam('text_charset', RCUBE_CHARSET);
+
+ $message->headers(array(
+ 'Subject' => $note['title'],
+ 'Date' => $note['changed']->format('r'),
+ ));
+ console($note);
+ if ($this->is_html($note)) {
+ $message->setHTMLBody($note['description']);
+
+ // add a plain text version of the note content as an alternative part.
+ $h2t = new rcube_html2text($note['description'], false, true, 0, RCUBE_CHARSET);
+ $plain_part = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 72), "\r\n", false, RCUBE_CHARSET);
+ $plain_part = trim(wordwrap($plain_part, 998, "\r\n", true));
+
+ // make sure all line endings are CRLF
+ $plain_part = preg_replace('/\r?\n/', "\r\n", $plain_part);
+
+ $message->setTXTBody($plain_part);
+ }
+ else {
+ $message->setTXTBody($note['description']);
+ }
+
+ return $message->getMessage();
+ }
+
/**
* Process the given note data (submitted by the client) before saving it
*/
private function _write_preprocess($note, $old = array())
{
$object = $note;
// TODO: handle attachments
// clean up HTML content
$object['description'] = $this->_wash_html($note['description']);
$is_html = true;
// try to be smart and convert to plain-text if no real formatting is detected
- if (preg_match('!<body><pre>(.*)</pre></body>!ims', $object['description'], $m)) {
+ if (preg_match('!<body><(?:p|pre)>(.*)</(?:p|pre)></body>!Uims', $object['description'], $m)) {
if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li)(\s+[a-z]|>)!im', $m[1], $n) || !strpos($m[1], '</'.$n[1].'>')) {
// $converter = new rcube_html2text($m[1], false, true, 0);
// $object['description'] = rtrim($converter->get_text());
$object['description'] = html_entity_decode(preg_replace('!<br(\s+/)>!', "\n", $m[1]));
$is_html = false;
}
}
// Add proper HTML header, otherwise Kontact renders it as plain text
if ($is_html) {
$object['description'] = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'."\n" .
str_replace('<head>', '<head><meta name="qrichtext" content="1" />', $object['description']);
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// make list of categories unique
if (is_array($object['categories'])) {
$object['categories'] = array_unique(array_filter($object['categories']));
}
unset($object['list'], $object['tempid'], $object['created'], $object['changed'], $object['created_'], $object['changed_']);
return $object;
}
/**
* Sanity checks/cleanups HTML content
*/
private function _wash_html($html)
{
// Add header with charset spec., washtml cannot work without that
$html = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></html>';
// clean HTML with washtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => array('html', 'head', 'meta', 'body', 'link'),
'html_attribs' => array('rel', 'type', 'name', 'http-equiv'),
);
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
$washer->add_callback('form', array($this, '_washtml_callback'));
$washer->add_callback('a', array($this, '_washtml_callback'));
// Remove non-UTF8 characters
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments (produced by washtml)
$html = preg_replace('/<!--[^>]+-->/', '', $html);
return $html;
}
/**
* Callback function for washtml cleaning class
*/
public function _washtml_callback($tagname, $attrib, $content, $washtml)
{
switch ($tagname) {
case 'form':
$out = html::div('form', $content);
break;
case 'a':
// strip temporary link tags from plain-text markup
$attrib = html::parse_attrib_string($attrib);
if (!empty($attrib['class']) && strpos($attrib['class'], 'x-templink') !== false) {
// remove link entirely
if (strpos($attrib['href'], html_entity_decode($content)) !== false) {
$out = $content;
break;
}
$attrib['class'] = trim(str_replace('x-templink', '', $attrib['class']));
}
$out = html::a($attrib, $content);
break;
default:
$out = '';
}
return $out;
}
}
diff --git a/plugins/kolab_notes/localization/en_US.inc b/plugins/kolab_notes/localization/en_US.inc
index 30ee3db5..66545c0d 100644
--- a/plugins/kolab_notes/localization/en_US.inc
+++ b/plugins/kolab_notes/localization/en_US.inc
@@ -1,35 +1,38 @@
<?php
$labels = array();
$labels['navtitle'] = 'Notes';
$labels['tags'] = 'Tags';
$labels['lists'] = 'Notebooks';
$labels['notes'] = 'Notes';
$labels['create'] = 'New Note';
+$labels['createnote'] = 'Create a new note';
+$labels['send'] = 'Send';
+$labels['sendnote'] = 'Send note by email';
$labels['newnote'] = 'New Note';
$labels['notags'] = 'No tags';
$labels['removetag'] = 'Remove tag';
$labels['created'] = 'Created';
$labels['changed'] = 'Last Modified';
$labels['title'] = 'Title';
$labels['now'] = 'Now';
$labels['sortby'] = 'Sort by';
$labels['createlist'] = 'New Notebook';
$labels['editlist'] = 'Edit Notebook';
$labels['listname'] = 'Name';
$labels['tabsharing'] = 'Sharing';
$labels['discard'] = 'Discard';
$labels['abort'] = 'Abort';
$labels['unsavedchanges'] = 'Unsaved Changes!';
$labels['savingdata'] = 'Saving data...';
$labels['recordnotfound'] = 'Record not found';
$labels['nochanges'] = 'No changes to be saved';
$labels['entertitle'] = 'Please enter a title for this note!';
$labels['deletenotesconfirm'] = 'Do you really want to delete the selected notes?';
$labels['deletenotebookconfirm'] = 'Do you really want to delete this notebook with all its notes? This action cannot be undone.';
$labels['discardunsavedchanges'] = 'The current note has not yet been saved. Discard the changes?';
$labels['invalidlistproperties'] = 'Invalid notebook properties! Please set a valid name.';
$labels['entertitle'] = 'Please enter a title for this note.';
$labels['aclnorights'] = 'You do not have administrator rights for this notebook.';
diff --git a/plugins/kolab_notes/notes.js b/plugins/kolab_notes/notes.js
index 4abb2d8c..dc6d7d51 100644
--- a/plugins/kolab_notes/notes.js
+++ b/plugins/kolab_notes/notes.js
@@ -1,1166 +1,1186 @@
/**
* Client scripts for the Kolab Notes plugin
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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);
});
}
});
// 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, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* 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){
$('<input>')
.attr('name', 'tags[]')
.attr('tabindex', '2')
.addClass('tag')
.val(val)
.appendTo(tagline);
});
if (!data.categories || !data.categories.length) {
$('<span>').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 '<a href="' + url + '" class="x-templink">' + p1 + p2 + '</a>';
};
return '<pre>' + Q(str).replace(link_pattern, link_replace) + '</pre>';
}
/**
* 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('<span class="tag">' + data.categories.join('</span><span class="tag">') + '</span>');
$('#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 = $('<li>').attr('rel', tag).data('value', tag)
.html(Q(tag) + '<span class="count"></span>')
.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 = $('<div class="tag-draghelper"></div>');
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/notes.css b/plugins/kolab_notes/skins/larry/notes.css
index edf9f022..97a7421c 100644
--- a/plugins/kolab_notes/skins/larry/notes.css
+++ b/plugins/kolab_notes/skins/larry/notes.css
@@ -1,335 +1,339 @@
/**
* Kolab Notes plugin styles for skin "Larry"
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#taskbar a.button-notes span.button-inner {
background-image: url('sprites.png');
background-position: 0 0;
}
#taskbar a.button-notes:hover span.button-inner,
#taskbar a.button-notes.button-selected span.button-inner {
background-image: url('sprites.png');
background-position: 0 -26px;
}
.notesview #sidebar {
position: absolute;
top: 42px;
left: 0;
bottom: 0;
width: 240px;
}
.notesview #notestoolbar {
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 40px;
white-space: nowrap;
}
.notesview #notestoolbar a.button.createnote {
background-image: url('sprites.png');
background-position: center -54px;
}
+.notesview #notestoolbar a.button.sendnote {
+ background-position: left -650px;
+}
+
.notesview #quicksearchbar {
top: 8px;
}
.notesview #searchmenulink {
width: 15px;
}
.notesview #mainview-right {
top: 42px;
}
.notesview #tagsbox {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 242px;
}
.notesview #notebooksbox {
position: absolute;
top: 300px;
left: 0;
width: 100%;
bottom: 0px;
}
.notesview #noteslistbox {
position: absolute;
top: 0;
left: 0;
width: 240px;
bottom: 0px;
}
.notesview #kolabnoteslist .title {
display: block;
padding: 4px 8px;
overflow: hidden;
text-overflow: ellipsis;
}
.notesview #kolabnoteslist .date {
display: block;
padding: 0px 8px 4px 8px;
color: #777;
font-weight: normal;
}
.notesview .boxpagenav a.icon.sortoptions {
background: url(sprites.png) center -93px no-repeat;
}
.notesview .toolbarmenu.iconized .selected span.icon {
background: url(sprites.png) -5px -109px no-repeat;
}
.notesview #notedetailsbox {
position: absolute;
top: 0;
left: 256px;
right: 0;
bottom: 0px;
}
.notesview #notedetailsbox .formbuttons {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 12px;
background: #f9f9f9;
}
.notesview #noteform,
.notesview #notedetails {
display: none;
position: absolute;
top: 82px;
left: 0;
bottom: 41px;
width: 100%;
}
.notesview #notedetails {
padding: 8px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.notesview #notedetails pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
margin: 0;
}
.notesview #notecontent {
position: relative;
width: 100%;
height: 100%;
border: 0;
border-radius: 0;
padding: 8px 0 8px 8px;
resize: none;
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
outline: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.2);
}
.notesview #notecontent:active,
.notesview #notecontent:focus {
-webkit-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
-o-box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
}
.notesview .defaultSkin table.mceLayout {
border: 0;
}
.notesview #notedetailstitle {
height: auto;
}
.notesview #notedetailstitle .tagedit-list,
.notesview #notedetailstitle input.inline-edit,
.notesview #notedetailstitle input.inline-edit:focus {
outline: none;
padding: 0;
margin: 0;
border: 0;
background: rgba(255,255,255,0.01);
-webkit-box-shadow: none;
-moz-box-shadow: none;
-o-box-shadow: none;
box-shadow: none;
}
.notesview #notedetailstitle input.notetitle,
.notesview #notedetailstitle input.notetitle:focus {
width: 100%;
font-size: 14px;
font-weight: bold;
color: #777;
}
.notesview #notedetailstitle .dates,
.notesview #notedetailstitle .tagline {
color: #999;
font-weight: normal;
font-size: 0.9em;
margin-top: 6px;
}
.notesview #notedetailstitle .dates {
margin-top: 4px;
margin-bottom: 4px;
}
.notesview #notedetailstitle .tagline {
position: relative;
cursor: text;
}
.notesview #notedetailstitle .tagline .placeholder {
position: absolute;
top: 4px;
left: 0;
z-index: 1;
}
.notesview #notedetailstitle .tagedit-list {
position: relative;
z-index: 2;
}
.notesview #notedetailstitle #tagedit-input {
background: none;
}
.notesview .tag-draghelper {
z-index: 1000;
}
.notesview #notedetailstitle .notecreated,
.notesview #notedetailstitle .notechanged {
display: inline-block;
padding-left: 0.4em;
padding-right: 2em;
color: #777;
}
.notesview #notebooks li {
margin: 0;
height: 20px;
padding: 6px 8px 2px 6px;
display: block;
position: relative;
white-space: nowrap;
}
.notesview #notebooks li.virtual {
height: 12px;
}
.notesview #notebooks li span.listname {
display: block;
position: absolute;
top: 7px;
left: 9px;
right: 6px;
cursor: default;
padding-bottom: 2px;
padding-right: 26px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notesview #notebooks li.virtual span.listname {
color: #aaa;
top: 3px;
}
.notesview #notebooks li.readonly,
.notesview #notebooks li.shared,
.notesview #notebooks li.other {
background-image: url('folder_icons.png');
background-position: right -1000px;
background-repeat: no-repeat;
}
.notesview #notebooks li.readonly {
background-position: 98% -21px;
}
.notesview #notebooks li.other {
background-position: 98% -52px;
}
.notesview #notebooks li.other.readonly {
background-position: 98% -77px;
}
.notesview #notebooks li.shared {
background-position: 98% -103px;
}
.notesview #notebooks li.shared.readonly {
background-position: 98% -130px;
}
.notesview #notebooks li.other.readonly span.listname,
.notesview #notebooks li.shared.readonly span.listname {
padding-right: 36px;
}
.notesview #notebooks li.selected > a {
background-color: transparent;
}
.notesview .uidialog .tabbed {
margin-top: -12px;
}
.notesview .uidialog .propform fieldset.tab {
display: block;
background: #efefef;
margin-top: 0.5em;
padding: 0.5em 1em;
min-height: 290px;
}
.notesview .uidialog .propform #noteslist-name {
width: 20em;
}
\ No newline at end of file
diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html
index a97fb837..75fa1889 100644
--- a/plugins/kolab_notes/skins/larry/templates/notes.html
+++ b/plugins/kolab_notes/skins/larry/templates/notes.html
@@ -1,137 +1,138 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="notesview noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<div id="notestoolbar" class="toolbar">
<roundcube:button command="createnote" type="link" class="button createnote disabled" classAct="button createnote" classSel="button createnote pressed" label="kolab_notes.create" title="kolab_notes.createnote" />
- <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" />
+ <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="print" />
+ <roundcube:button command="sendnote" type="link" class="button sendnote disabled" classAct="button sendnote" classSel="button sendnote pressed" label="kolab_notes.send" title="kolab_notes.sendnote" />
<roundcube:container name="toolbar" id="notestoolbar" />
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
</div>
<div id="sidebar">
<div id="tagsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.tags" id="taglist" /></h2>
<div class="scroller">
<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" />
</div>
</div>
<div id="notebooksbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.lists" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.notebooks" id="notebooks" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="kolab_notes.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="notesoptionslink" id="notesoptionsmenulink" type="link" title="kolab_notes.listactions" class="listbutton groupactions" onclick="UI.show_popup('notesoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
</div>
</div>
</div>
<div id="mainview-right">
<div id="noteslistbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="kolab_notes.notes" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.listing" id="kolabnoteslist" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="delete" type="link" title="delete" class="listbutton delete disabled" classAct="listbutton delete" innerClass="inner" content="-" />
<roundcube:object name="plugin.recordsCountDisplay" class="countdisplay" label="fromtoshort" />
</div>
<div class="boxpagenav">
<roundcube:button name="notessortmenulink" id="notessortmenulink" type="link" title="kolab_notes.sortby" class="icon sortoptions" onclick="UI.show_popup('notessortmenu');return false" innerClass="inner" content="v" />
</div>
</div>
<div id="notedetailsbox" class="uibox contentbox">
<roundcube:object name="plugin.notetitle" id="notedetailstitle" class="boxtitle" />
<roundcube:object name="plugin.editform" id="noteform" />
<roundcube:object name="plugin.detailview" id="notedetails" class="scroller" />
<div class="footerleft formbuttons">
<roundcube:button command="save" type="input" class="button mainaction" label="save" />
</div>
</div>
</div>
</div>
<roundcube:object name="message" id="messagestack" />
<div id="notesoptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
<li><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
</ul>
</div>
<div id="notessortmenu" class="popupmenu">
<ul class="toolbarmenu iconized">
<li><roundcube:button command="list-sort" prop="changed" type="link" label="kolab_notes.changed" class="icon active by-changed" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="title" type="link" label="kolab_notes.title" class="icon active by-title" innerclass="icon" /></li>
</ul>
</div>
<div id="notebookeditform" class="uidialog">
<roundcube:container name="notebookeditform" id="notebookeditform" />
<roundcube:label name="loading" />
</div>
<script type="text/javascript">
// UI startup
var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
rcmail.addEventListener('kolab_notes_editform_load', function(e){
UI.init_tabs($('#notebookeditform > form').addClass('propform tabbed'));
})
new rcube_splitter({ id:'notesviewsplitter', p1:'#sidebar', p2:'#mainview-right',
orientation:'v', relative:true, start:240, min:180, size:16, offset:2, render:layout_view }).init();
new rcube_splitter({ id:'noteslistsplitter2', p1:'#noteslistbox', p2:'#notedetailsbox',
orientation:'v', relative:true, start:242, min:180, size:16, offset:2, render:layout_view }).init();
new rcube_splitter({ id:'notesviewsplitterv', p1:'#tagsbox', p2:'#notebooksbox',
orientation:'h', relative:true, start:242, min:120, size:16, offset:6 }).init();
function layout_view()
{
var form = $('#noteform, #notedetails'),
content = $('#notecontent'),
header = $('#notedetailstitle'),
w, h;
form.css('top', header.outerHeight()+'px');
w = form.outerWidth();
h = form.outerHeight();
content.width(w).height(h);
$('#notecontent_tbl').width(w+'px').height('').css('margin-top', '-1px');
$('#notecontent_ifr').width(w+'px').height((h-54)+'px');
}
$(window).resize(function(e){
layout_view();
});
});
</script>
</body>
</html>
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jun 9, 10:32 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196877
Default Alt Text
(83 KB)

Event Timeline