Adds rich text support to notes

- Still a few issues
- Converts plaintext notes to HTML on upgrade
This commit is contained in:
Dan Stillman 2008-10-13 19:51:53 +00:00
parent b46860f6a4
commit 651bcf2380
15 changed files with 282 additions and 46 deletions

View file

@ -192,7 +192,7 @@
</hbox>
</vbox>
<textbox id="editor" type="styled" hidden="true" flex="1"/>
<textbox id="editor" type="styled" mode="integration" hidden="true" flex="1"/>
<hbox style="margin-top: 15px">
<hbox>

View file

@ -155,11 +155,11 @@
textbox.setAttribute('readonly', 'true');
}
var scrollPos = textbox.inputField.scrollTop;
//var scrollPos = textbox.inputField.scrollTop;
if (this.item) {
textbox.value = this.item.getNote();
}
textbox.inputField.scrollTop = scrollPos;
//textbox.inputField.scrollTop = scrollPos;
this._id('linksbox').hidden = !(this.displayTags && this.displayRelated);
@ -322,7 +322,7 @@
<method name="disableUndo">
<body>
<![CDATA[
this.noteField.editor.enableUndo(true);
//this.noteField.editor.enableUndo(true);
]]>
</body>
</method>
@ -330,7 +330,7 @@
<method name="enableUndo">
<body>
<![CDATA[
this.noteField.editor.enableUndo(false);
//this.noteField.editor.enableUndo(false);
]]>
</body>
</method>
@ -357,7 +357,7 @@
<content>
<xul:vbox xbl:inherits="flex">
<xul:label id="citeLabel"/>
<xul:textbox id="noteField" multiline="true" type="timed" timeout="1000" flex="1"/>
<xul:textbox id="noteField" type="styled" mode="note" timeout="1000" flex="1"/>
<xul:hbox id="linksbox" hidden="true">
<xul:linksbox id="links" flex="1"/>
</xul:hbox>

View file

@ -28,23 +28,33 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="styled-textbox">
<implementation>
<field name="_mode"/>
<field name="_format"/>
<field name="_loadHandler"/>
<field name="_commandString"/>
<field name="_eventHandler"/>
<field name="_timer"/>
<constructor><![CDATA[
this._browser = document.getAnonymousElementByAttribute(this, "anonid", "rt-view");
this.mode = this.getAttribute('mode');
this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view");
this._editor = null;
this._value = null;
this._rtfMap = {
"\\":"\\\\",
"&amp;":"&",
"&lt;":"<",
"&gt;":">",
"<em>":"\\i ",
"</em>":"\\i0 ",
"<i>":"\\i ",
"</i>":"\\i0 ",
"<strong>":"\\b ",
"</strong>":"\\b0 ",
"<b>":"\\b ",
"</b>":"\\b0 ",
"<u>":"\\ul ",
"</u>":"\\ul0 ",
"<br>":"\x0B",
"<br />":"\x0B",
"<sup>":"\\super ",
"</sup>":"\\super0 ",
"<sub>":"\\sub ",
@ -56,41 +66,108 @@
// not sure why an event is necessary here, but it is
var me = this;
this._loadHandler = function() {me._browserLoaded()};
this._browser.addEventListener("DOMContentLoaded", this._loadHandler, false);
this._loadHandler = function() {me._iframeLoaded()};
this._iframe.addEventListener("DOMContentLoaded", this._loadHandler, false);
]]></constructor>
<!-- Called when browser is loaded. Until the browser is loaded, we can't do
<!-- Called when iframe browser is loaded. Until the browser is loaded, we can't do
anything with it, so we just keep track of what's supposed to
happen. -->
<method name="_browserLoaded">
<method name="_iframeLoaded">
<body><![CDATA[
this._browser.removeEventListener("DOMContentLoaded", this._loadHandler, false);
this._iframe.removeEventListener("DOMContentLoaded", this._loadHandler, false);
var ios = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService);
var uri = ios.newURI("chrome://zotero/content/tiny_mce/integration.html", null, null);
var uri = ios.newURI("chrome://zotero/content/tiny_mce/" + this.mode + ".html", null, null);
var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"].
getService(Components.interfaces.nsIChromeRegistry);
var fileURI = chromeReg.convertChromeURL(uri);
this._browser.webNavigation.loadURI(fileURI.spec,
this._iframe.webNavigation.loadURI(fileURI.spec,
Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
// Register handler for deferred setting of content
var me = this;
var listener = function() {
me._browser.removeEventListener("DOMContentLoaded", listener, false);
var editor = me._browser.contentWindow.wrappedJSObject.tinyMCE.get("tinymce");
me._iframe.removeEventListener("DOMContentLoaded", listener, false);
var editor = me._iframe.contentWindow.wrappedJSObject.tinyMCE.get("tinymce");
editor.onInit.add(function() {
me._editor = editor;
if(me._value) me.value = me._value;
});
if (me._eventHandler) {
me._iframe.contentWindow.wrappedJSObject.handleEvent = me._eventHandler;
}
};
this._browser.addEventListener("DOMContentLoaded", listener, false);
this._iframe.addEventListener("DOMContentLoaded", listener, false);
]]></body>
</method>
<property name="mode">
<getter><![CDATA[
if (!this._mode) {
throw ("mode is not defined in styled-textbox.xml");
}
return this._mode;
]]></getter>
<setter><![CDATA[
Zotero.debug("Setting mode to " + val);
switch (val) {
case 'note':
var self = this;
this._eventHandler = function (event) {
Zotero.debug(event.type);
switch (event.type) {
case 'keypress':
// Ignore keypresses that don't change
// any text
if (!event.isChar &&
event.keyCode != event.DOM_VK_DELETE &&
event.keyCode != event.DOM_VK_BACK_SPACE) {
//Zotero.debug("Not a char");
return;
}
break;
case 'change':
Zotero.debug("Event type is " + event.type);
break;
default:
return;
}
if (self._timer) {
clearTimeout(self._timer);
}
// Get the command event
self._timer = self.timeout && setTimeout(function () {
var attr = self.getAttribute('oncommand');
attr = attr.replace('this', 'thisObj');
var func = new Function('thisObj', 'event', attr);
func(self, event);
}, self.timeout);
return true;
};
break;
case 'integration':
break;
default:
throw ("Invalid mode '" + val + "' in styled-textbox.xml");
}
return this._mode = val;
]]></setter>
</property>
<!-- Sets or returns formatting (currently, HTML or Integration) of rich text box -->
<property name="format">
<getter><![CDATA[
@ -104,30 +181,50 @@
<!-- Sets or returns contents of rich text box -->
<property name="value">
<getter><![CDATA[
this._editor.execCommand("mceCleanup");
var body = this._editor.getBody();
var output = body.innerHTML;
var output = this._editor.getBody();
output = output.innerHTML;
Zotero.debug("RAW");
Zotero.debug(output);
var output = this._editor.getContent();
Zotero.debug("XHTML");
Zotero.debug(output);
if(this._format == "Integration" || this._format == "RTF") {
// do appropriate replacement operations
for(var needle in this._rtfMap) {
output = output.replace(needle, this._rtfMap[needle], "g");
}
output = output.replace("<p>", "", "g");
output = output.replace("</p>", "\\par ", "g");
output = output.replace(/<\/?div[^>]*>/g, "");
output = Zotero.Utilities.prototype.trim(output);
output = Zotero.Utilities.prototype.unescapeHTML(output);
if(output.substr(-4) == "\\par") output = output.substr(0, output.length-4);
}
return output;
]]></getter>
<setter><![CDATA[
Zotero.debug("Setting value!");
if (self._timer) {
clearTimeout(self._timer);
}
if(!this._editor) {
// if not loaded, wait until it is to set
return this._value = val;
}
if (this.value == val) {
Zotero.debug("Value hasn't changed!");
return;
}
Zotero.debug("Value has changed");
var html = val;
if(this._format == "Integration" || this._format == "RTF") {
@ -169,11 +266,16 @@
return val;
]]></setter>
</property>
<property name="timeout"
onset="this.setAttribute('timeout', val); return val;"
onget="return parseInt(this.getAttribute('timeout')) || 0;"/>
</implementation>
<content>
<xul:iframe flex="1" anonid="rt-view" class="rt-view" type="content" style="min-height:130px"
xbl:inherits="onfocus,onblur,flex,width,height,hidden"/>
<xul:iframe flex="1" anonid="rt-view" class="rt-view" type="content"
xbl:inherits="onfocus,onblur,flex,width,height,hidden"
style="overflow: hidden"/>
</content>
</binding>
</bindings>

View file

@ -111,6 +111,6 @@
</hbox>
</vbox>
<textbox id="editor" type="styled" flex="1"/>
<textbox id="editor" type="styled" mode="integration" flex="1"/>
</vbox>
</dialog>

View file

@ -1,13 +1,32 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
html, body {
height: 100%;
margin: 0;
}
#tinymce_parent {
display: block;
height: 100%;
min-height: 130px;
}
#tinymce_tbl {
height: 100% !important;
width: 100% !important;
}
</style>
<script type="text/javascript" src="tiny_mce.js"></script>
<script type="text/javascript">
tinyMCE.init({
// General options
mode : "none",
theme : "advanced",
content_css : "../../../skin/default/zotero/tinymce-content.css",
content_css : "../../../skin/default/zotero/tinymce/integration-content.css",
// Theme options
theme_advanced_buttons1 : "bold,italic,underline,|,sub,sup,|,removeformat",
@ -20,7 +39,7 @@
tinyMCE.execCommand("mceAddControl", true, "tinymce");
</script>
</head>
<body style="border: 0; margin: 0;">
<div id="tinymce" style="width:100%"></div>
<body>
<div id="tinymce"></div>
</body>
</html>

View file

@ -2,24 +2,79 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>TinyMCE</title>
<style>
html, body {
height: 100%;
margin: 0;
}
#tinymce_parent {
display: block;
height: 100%;
}
#tinymce_tbl {
height: 100% !important;
width: 100% !important;
}
table.mceLayout > tbody > tr.mceLast {
position: absolute;
display: block;
top: 82px;
bottom: 2px;
left: 1px;
right: 1px;
}
td.mceIframeContainer {
display: block;
height: 100% !important;
width: 100% !important;
}
#tinymce_ifr {
height: 100% !important;
width: 100% !important;
}
</style>
<script type="text/javascript" src="tiny_mce.js"></script>
<script type="text/javascript">
tinyMCE.init({
// General options
mode : "textareas",
mode : "none",
theme : "advanced",
content_css : "../../../skin/default/zotero/tinymce/note-content.css",
button_tile_map : true,
language : "en", // TODO: localize
entity_encoding : "raw",
gecko_spellcheck : true,
handle_event_callback : function (event) {
if (handleEvent) {
handleEvent(event);
}
},
onchange_callback : function () {
var event = { type: 'change' };
if (handleEvent) {
handleEvent(event);
}
},
fix_list_elements : true,
fix_table_elements : true,
// Theme options
theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,removeformat",
theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,removeformat,code",
theme_advanced_buttons2 : "justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,blockquote,|,link,unlink",
theme_advanced_buttons3 : "formatselect,fontselect,fontsizeselect",
theme_advanced_toolbar_location : "top",
theme_advanced_toolbar_align : "left",
theme_advanced_resizing : true
});
tinyMCE.execCommand("mceAddControl", true, "tinymce");
</script>
</head>
<body>
<textarea id="tinymce" rows="15" cols="80" style="width: 100%"></textarea>
<div id="tinymce"></div>
</body>
</html>

View file

@ -1240,10 +1240,16 @@ Zotero.Item.prototype.save = function() {
sql = "INSERT INTO itemNotes "
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
var parent = this.isNote() ? this.getSource() : null;
var noteText = this._noteText ? this._noteText : '';
// Add <div> wrapper if not present
if (!noteText.match(/^<div class="zotero\-note znv[0-9]+">.*<\/div>$/)) {
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
}
var bindParams = [
itemID,
parent ? parent : null,
this._noteText ? this._noteText : '',
noteText,
this._noteTitle ? this._noteTitle : ''
];
Zotero.DB.query(sql, bindParams);
@ -1575,10 +1581,15 @@ Zotero.Item.prototype.save = function() {
sql = "REPLACE INTO itemNotes "
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
var parent = this.isNote() ? this.getSource() : null;
var noteText = this._noteText;
// Add <div> wrapper if not present
if (!noteText.match(/^<div class="zotero\-note znv[0-9]+">.*<\/div>$/)) {
noteText = '<div class="zotero-note znv1">' + noteText + '</div>';
}
var bindParams = [
this.id,
parent ? parent : null,
this._noteText,
noteText,
this._noteTitle
];
Zotero.DB.query(sql, bindParams);
@ -1983,6 +1994,8 @@ Zotero.Item.prototype.getNote = function() {
var sql = "SELECT note FROM itemNotes WHERE itemID=?";
var note = Zotero.DB.valueQuery(sql, this.id);
// Don't include <div> wrapper when returning value
note = note.replace(/^<div class="zotero-note znv[0-9]+">(.*)<\/div>$/, '$1');
this._noteText = note ? note : '';

View file

@ -30,6 +30,9 @@ Zotero.Notes = new function() {
* Return first line (or first MAX_LENGTH characters) of note content
**/
function noteToTitle(text) {
text = Zotero.Utilities.prototype.trim(text);
text = Zotero.Utilities.prototype.unescapeHTML(text);
var max = this.MAX_TITLE_LENGTH;
var t = text.substring(0, max);

View file

@ -995,6 +995,20 @@ Zotero.DBConnection.prototype._getDBConnection = function () {
};
this._connection.createFunction('regexp', 2, rx);
// text2html UDF
var rx = {
onFunctionCall: function (arg) {
var str = arg.getUTF8String(0);
str = Zotero.Utilities.prototype.htmlSpecialChars(str);
str = '<p>'
+ str.replace(/\n/g, '</p><p>')
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
.replace(/ /g, '&nbsp;&nbsp;')
+ '</p>';
return str.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>');
}
};
this._connection.createFunction('text2html', 1, rx);
return this._connection;
}

View file

@ -70,7 +70,14 @@ Zotero.Report = new function() {
// Independent note
if (arr['note']) {
content += '<p>' + escapeXML(arr['note']) + '</p>\n';
content += '\n';
if (arr.note.substr(0, 1024).match(/<p[^>]*>/)) {
content += arr.note + '\n';
}
// Wrap plaintext notes in <p>
else {
content += '<p class="plaintext">' + arr.note + '</p>\n';
}
}
}
@ -85,7 +92,15 @@ Zotero.Report = new function() {
content += '<ul class="notes">\n';
for each(var note in arr.reportChildren.notes) {
content += '<li id="i' + note.itemID + '">\n';
content += '<p>' + escapeXML(note.note) + '</p>\n';
content += note.note + '\n';
if (note.note.substr(0, 1024).match(/<p[^>]*>/)) {
content += note.note + '\n';
}
// Wrap plaintext notes in <p>
else {
content += '<p class="plaintext">' + note.note + '</p>\n';
}
// Child note tags
content += _generateTagsList(note);

View file

@ -1945,6 +1945,11 @@ Zotero.Schema = new function(){
if (i==42) {
Zotero.DB.query("UPDATE itemAttachments SET syncState=0");
}
// 1.5 Sync Preview 2.3
if (i==43) {
Zotero.DB.query("UPDATE itemNotes SET note='<div class=\"zotero-note znv1\">' || TEXT2HTML(note) || '</div>' WHERE note NOT LIKE '<div class=\"zotero-note %'");
}
}
_updateDBVersion('userdata', toVersion);

View file

@ -113,12 +113,8 @@ ul.notes > li p:last-child {
/* Preserve whitespace on notes */
ul.notes li p, li.note p {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
ul.notes li p.plaintext, li.note p.plaintext {
white-space: pre-wrap;
}
/* Display tags within child notes inline */

View file

@ -1,2 +1,2 @@
body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;}
body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;}
p, div {margin:0; padding:0}

View file

@ -0,0 +1,14 @@
body {
font-size: 11px;
font-family: Lucida Grande, Tahoma, Verdana, Helvetica, sans-serif;
}
/*
blockquote p:not(:empty):before {
content: '“'
}
blockquote p:not(:empty):after {
content: '”'
}
*/

View file

@ -1,4 +1,4 @@
-- 42
-- 43
-- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema()