diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 3c369583..b92e71be 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -210,7 +210,7 @@ class kolab_notes extends rcube_plugin
$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(array_unique($tags))));
+ $this->rc->output->command('plugin.data_ready', array('list' => $list, 'search' => $search, 'data' => $data, 'tags' => array_values($tags)));
}
/**
@@ -221,10 +221,10 @@ class kolab_notes extends rcube_plugin
$tags = array();
foreach ($records as $i => $rec) {
- $this->_client_encode($records[$i]);
unset($records[$i]['description']);
+ $this->_client_encode($records[$i]);
- foreach ((array)$reg['categories'] as $tag) {
+ foreach ((array)$rec['categories'] as $tag) {
$tags[] = $tag;
}
}
@@ -311,7 +311,7 @@ class kolab_notes extends rcube_plugin
private function _client_encode(&$note)
{
foreach ($note as $key => $prop) {
- if ($key[0] == '_') {
+ if ($key[0] == '_' || $key == 'x-custom') {
unset($note[$key]);
}
}
@@ -323,6 +323,11 @@ class kolab_notes extends rcube_plugin
}
}
+ // clean HTML contents
+ if (!empty($note['description']) && preg_match('/<(html|body|div|p|span)(\s+[a-z]|>)/', $note['description'])) {
+ $note['html'] = $this->_wash_html($note['description']);
+ }
+
return $note;
}
@@ -331,12 +336,16 @@ class kolab_notes extends rcube_plugin
$action = rcube_utils::get_input_value('_do', RCUBE_INPUT_POST);
$note = rcube_utils::get_input_value('_data', RCUBE_INPUT_POST, true);
- $success =false;
+ $success = false;
switch ($action) {
- case 'save':
- console($action, $note);
- sleep(3);
- $success = true;
+ case 'new':
+ $temp_id = $rec['tempid'];
+
+ case 'edit':
+ if ($success = $this->save_note($note)) {
+ $refresh = $this->get_note($note);
+ $refresh['tempid'] = $temp_id;
+ }
break;
}
@@ -350,8 +359,133 @@ class kolab_notes extends rcube_plugin
// unlock client
$this->rc->output->command('plugin.unlock_saving');
- // $this->rc->output->command('plugin.update_note', $note);
+
+ 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
+ * @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['id'], $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;
+ // TODO: cache this in memory for later read
+ }
+
+ return $saved;
+ }
+
+
+ /**
+ * 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']);
+
+ // try to be smart and convert to plain-text if no real formatting is detected
+ if (preg_match('!
(.*)
!ims', $object['description'], $m)) {
+ if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li)(\s+[a-z]|>)!im', $m[1])) {
+ // $converter = new rcube_html2text($m[1], false, true, 0);
+ // $object['description'] = rtrim($converter->get_text());
+ $object['description'] = preg_replace('!
!', "\n", $m[1]);
+ }
+ }
+
+ // copy meta data (starting with _) from old object
+ foreach ((array)$old as $key => $val) {
+ if (!isset($object[$key]) && $key[0] == '_')
+ $object[$key] = $val;
+ }
+
+ 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 washhtml by Frederic Motte
+ $wash_opts = array(
+ 'show_washed' => false,
+ 'allow_remote' => 1,
+ 'charset' => RCUBE_CHARSET,
+ 'html_elements' => array('html', 'body', 'link'),
+ 'html_attribs' => array('rel', 'type'),
+ );
+
+ // initialize HTML washer
+ $washer = new rcube_washtml($wash_opts);
+
+ //$washer->add_callback('form', 'rcmail_washtml_callback');
+ //$washer->add_callback('style', 'rcmail_washtml_callback');
+
+ // Remove non-UTF8 characters (#1487813)
+ $html = rcube_charset::clean($html);
+
+ $html = $washer->wash($html);
+
+ // remove unwanted comments (produced by washtml)
+ $html = preg_replace('//', '', $html);
+
+ return $html;
+ }
+
}
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index cc22a550..264288d4 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -56,6 +56,22 @@ class kolab_notes_ui
// TODO: load config options and user prefs relevant for the UI
$settings = array();
+
+ // 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);
}
@@ -111,7 +127,9 @@ class kolab_notes_ui
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')));
diff --git a/plugins/kolab_notes/notes.js b/plugins/kolab_notes/notes.js
index 873c3941..d2b9319d 100644
--- a/plugins/kolab_notes/notes.js
+++ b/plugins/kolab_notes/notes.js
@@ -27,6 +27,7 @@ function rcube_kolab_notes_ui(settings)
var search_query;
var noteslist;
var notesdata = {};
+ var tagsfilter = [];
var tags = [];
var me = this;
@@ -46,12 +47,14 @@ function rcube_kolab_notes_ui(settings)
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('save', save_note, true);
+ rcmail.register_command('delete', delete_note, true);
rcmail.register_command('search', quicksearch, true);
rcmail.register_command('reset-search', reset_search, true);
// 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.unlock_saving', function(){
if (saving_lock) {
rcmail.set_busy(false, null, saving_lock);
@@ -78,6 +81,7 @@ function rcube_kolab_notes_ui(settings)
noteslist.addEventListener('select', function(list) {
var note;
if (list.selection.length == 1 && (note = notesdata[list.selection[0]])) {
+ // TODO: check for unsaved changes and warn
edit_note(note.uid, 'edit');
}
else {
@@ -87,6 +91,90 @@ function rcube_kolab_notes_ui(settings)
.init();
}
+ // 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',
+// extended_valid_elements: 'font[face|size|color|style],span[id|class|align|style]',
+ 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) {
+ window.open(link.get(0).href, '_blank');
+ }
+ });
+ }
+ };
+
+ // 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 (me.selected_list) {
rcmail.enable_command('createnote', true);
$('#rcmliknb'+me.selected_list).click();
@@ -194,6 +282,33 @@ function rcube_kolab_notes_ui(settings)
reset_view();
noteslist.clear();
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 && 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) {
+ reset_view();
+ }
+ }
}
/**
@@ -220,7 +335,7 @@ function rcube_kolab_notes_ui(settings)
notesdata[rec.id] = rec;
}
- tags = data.tags || [];
+ render_tagslist(data.tags || [], true)
rcmail.set_busy(false, 'loading', ui_loading);
}
@@ -236,13 +351,14 @@ function rcube_kolab_notes_ui(settings)
return;
}
- var list = me.notebooks[data.list] || me.notebooks[me.selected_list]
- var title = $('.notetitle', rcmail.gui_objects.noteviewtitle).val(data.title);
- var content = $('#notecontent').val(data.description);
+ var list = me.notebooks[data.list] || me.notebooks[me.selected_list];
+ content = $('#notecontent').val(data.description);
+ $('.notetitle', rcmail.gui_objects.noteviewtitle).val(data.title);
$('.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)
+ if (data.created || data.changed) {
$('.dates', rcmail.gui_objects.noteviewtitle).show();
+ }
$(rcmail.gui_objects.noteseditform).show();
@@ -265,7 +381,7 @@ function rcube_kolab_notes_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', 'kolab_notes') }
})
@@ -273,8 +389,124 @@ function rcube_kolab_notes_ui(settings)
.on('click', function(){ $('.tagline .placeholder').hide(); });
me.selected_note = data;
- rcmail.enable_command('save', list.editable && !data.readonly);
- content.select();
+ rcmail.enable_command('save', 'delete', list.editable && !data.readonly);
+
+ var html, node, editor = tinyMCE.get('notecontent');
+ if (editor) {
+ html = data.html || data.description;
+ if (!html.match(/<(html|body|p|div|span)/))
+ html = '' + Q(html) + '
';
+
+ editor.setContent(html);
+ node = editor.getContentAreaContainer().childNodes[0];
+ if (node) node.tabIndex = content.get(0).tabIndex;
+ editor.getBody().focus();
+ }
+
+ // Trigger resize (needed for proper editor resizing)
+ $(window).resize();
+ }
+
+ /**
+ *
+ */
+ 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');
+ }
+ });
+ }
+
+ /**
+ * Callback from server after saving a note record
+ */
+ function update_note(data)
+ {
+ data.id = rcmail.html_identifier_encode(data.uid);
+ notesdata[data.id] = data;
+ render_note(data);
+
+ // update list item
+ var row = noteslist.rows[data.id];
+ if (row) {
+ $('.title', row.obj).html(Q(data.title));
+ $('.date', row.obj).html(Q(data.changed || ''));
+ // TODO: move to top
+ }
+
+ render_tagslist(data.categories || []);
}
/**
@@ -283,10 +515,11 @@ function rcube_kolab_notes_ui(settings)
function reset_view()
{
me.selected_note = null;
+ noteslist.clear_selection();
$('.notetitle', rcmail.gui_objects.noteviewtitle).val('');
$('.tagline, .dates', rcmail.gui_objects.noteviewtitle).hide();
$(rcmail.gui_objects.noteseditform).hide();
- rcmail.enable_command('save', false);
+ rcmail.enable_command('save', 'delete', false);
}
/**
@@ -298,9 +531,10 @@ function rcube_kolab_notes_ui(settings)
return false;
}
+ var editor = tinyMCE.get('notecontent');
var savedata = {
title: trim($('.notetitle', rcmail.gui_objects.noteviewtitle).val()),
- description: $('#notecontent').val(),
+ description: editor ? editor.getContent({ format:'html' }) : $('#notecontent').val(),
list: me.selected_note.list || me.selected_list,
uid: me.selected_note.uid,
categories: []
@@ -325,8 +559,19 @@ function rcube_kolab_notes_ui(settings)
rcmail.lock_form(rcmail.gui_objects.noteseditform, true);
saving_lock = rcmail.set_busy(true, 'kolab_notes.savingdata');
- rcmail.http_post('action', { _data: savedata, _do:'save' }, true);
+ rcmail.http_post('action', { _data: savedata, _do: savedata.uid?'edit':'new' }, true);
}
+
+ function delete_note()
+ {
+ if (!me.selected_note) {
+ return false;
+ }
+
+ alert(me.selected_note.title)
+ reset_view();
+ }
+
}
diff --git a/plugins/kolab_notes/skins/larry/notes.css b/plugins/kolab_notes/skins/larry/notes.css
index 1f86084b..a3ba1589 100644
--- a/plugins/kolab_notes/skins/larry/notes.css
+++ b/plugins/kolab_notes/skins/larry/notes.css
@@ -104,18 +104,24 @@
background: #f9f9f9;
}
-.notesview #notecontent {
+.notesview #noteform {
position: absolute;
top: 82px;
left: 0;
bottom: 41px;
width: 100%;
+}
+
+.notesview #notecontent {
+ position: relative;
+ width: 100%;
+ height: 100%;
border: 0;
border-radius: 0;
padding: 8px 0 8px 8px;
resize: none;
- font-family: monospace;
- font-size: 9pt;
+ font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+ font-size: 12px;
outline: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
@@ -135,6 +141,10 @@
box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
}
+.notesview .defaultSkin table.mceLayout {
+ border: 0;
+}
+
.notesview #notedetailstitle {
height: 68px;
}
diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html
index 567941bc..c75a3b4c 100644
--- a/plugins/kolab_notes/skins/larry/templates/notes.html
+++ b/plugins/kolab_notes/skins/larry/templates/notes.html
@@ -24,7 +24,7 @@
@@ -84,11 +84,32 @@ $(document).ready(function(e){
UI.init();
new rcube_splitter({ id:'notesviewsplitter', p1:'#sidebar', p2:'#mainview-right',
- orientation:'v', relative:true, start:240, min:180, size:16, offset:2 }).init();
+ 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 }).init();
+ 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'),
+ 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();
+ });
});