Use tinyMCE editor for notes contents; implement data saving and tags listing/filtering

This commit is contained in:
Thomas Bruederli 2014-03-31 16:31:13 +02:00
parent 27a669a195
commit aa4d0e2b94
5 changed files with 455 additions and 27 deletions

View file

@ -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,7 +359,132 @@ 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('!<body><pre>(.*)</pre></body>!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('!<br(\s+/)>!', "\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><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></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;
}
}

View file

@ -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')));

View file

@ -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 = '<pre>' + Q(html) + '</pre>';
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 = $('<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');
}
});
}
/**
* 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();
}
}

View file

@ -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;
}

View file

@ -24,7 +24,7 @@
<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" />
<roundcube:object name="plugin.tagslist" id="tagslist" class="tagcloud" />
</div>
</div>
@ -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();
});
});
</script>