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> </hbox>
</vbox> </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 style="margin-top: 15px">
<hbox> <hbox>

View file

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

View file

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

View file

@ -111,6 +111,6 @@
</hbox> </hbox>
</vbox> </vbox>
<textbox id="editor" type="styled" flex="1"/> <textbox id="editor" type="styled" mode="integration" flex="1"/>
</vbox> </vbox>
</dialog> </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"> <!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"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <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" src="tiny_mce.js"></script>
<script type="text/javascript"> <script type="text/javascript">
tinyMCE.init({ tinyMCE.init({
// General options // General options
mode : "none", mode : "none",
theme : "advanced", theme : "advanced",
content_css : "../../../skin/default/zotero/tinymce-content.css", content_css : "../../../skin/default/zotero/tinymce/integration-content.css",
// Theme options // Theme options
theme_advanced_buttons1 : "bold,italic,underline,|,sub,sup,|,removeformat", theme_advanced_buttons1 : "bold,italic,underline,|,sub,sup,|,removeformat",
@ -20,7 +39,7 @@
tinyMCE.execCommand("mceAddControl", true, "tinymce"); tinyMCE.execCommand("mceAddControl", true, "tinymce");
</script> </script>
</head> </head>
<body style="border: 0; margin: 0;"> <body>
<div id="tinymce" style="width:100%"></div> <div id="tinymce"></div>
</body> </body>
</html> </html>

View file

@ -2,24 +2,79 @@
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>TinyMCE</title> <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" src="tiny_mce.js"></script>
<script type="text/javascript"> <script type="text/javascript">
tinyMCE.init({ tinyMCE.init({
// General options // General options
mode : "textareas", mode : "none",
theme : "advanced", 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 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_buttons2 : "justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,blockquote,|,link,unlink",
theme_advanced_buttons3 : "formatselect,fontselect,fontsizeselect", theme_advanced_buttons3 : "formatselect,fontselect,fontsizeselect",
theme_advanced_toolbar_location : "top", theme_advanced_toolbar_location : "top",
theme_advanced_toolbar_align : "left", theme_advanced_toolbar_align : "left",
theme_advanced_resizing : true theme_advanced_resizing : true
}); });
tinyMCE.execCommand("mceAddControl", true, "tinymce");
</script> </script>
</head> </head>
<body> <body>
<textarea id="tinymce" rows="15" cols="80" style="width: 100%"></textarea> <div id="tinymce"></div>
</body> </body>
</html> </html>

View file

@ -1240,10 +1240,16 @@ Zotero.Item.prototype.save = function() {
sql = "INSERT INTO itemNotes " sql = "INSERT INTO itemNotes "
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)"; + "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
var parent = this.isNote() ? this.getSource() : null; 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 = [ var bindParams = [
itemID, itemID,
parent ? parent : null, parent ? parent : null,
this._noteText ? this._noteText : '', noteText,
this._noteTitle ? this._noteTitle : '' this._noteTitle ? this._noteTitle : ''
]; ];
Zotero.DB.query(sql, bindParams); Zotero.DB.query(sql, bindParams);
@ -1575,10 +1581,15 @@ Zotero.Item.prototype.save = function() {
sql = "REPLACE INTO itemNotes " sql = "REPLACE INTO itemNotes "
+ "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)"; + "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)";
var parent = this.isNote() ? this.getSource() : null; 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 = [ var bindParams = [
this.id, this.id,
parent ? parent : null, parent ? parent : null,
this._noteText, noteText,
this._noteTitle this._noteTitle
]; ];
Zotero.DB.query(sql, bindParams); Zotero.DB.query(sql, bindParams);
@ -1983,6 +1994,8 @@ Zotero.Item.prototype.getNote = function() {
var sql = "SELECT note FROM itemNotes WHERE itemID=?"; var sql = "SELECT note FROM itemNotes WHERE itemID=?";
var note = Zotero.DB.valueQuery(sql, this.id); 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 : ''; 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 * Return first line (or first MAX_LENGTH characters) of note content
**/ **/
function noteToTitle(text) { function noteToTitle(text) {
text = Zotero.Utilities.prototype.trim(text);
text = Zotero.Utilities.prototype.unescapeHTML(text);
var max = this.MAX_TITLE_LENGTH; var max = this.MAX_TITLE_LENGTH;
var t = text.substring(0, max); var t = text.substring(0, max);

View file

@ -995,6 +995,20 @@ Zotero.DBConnection.prototype._getDBConnection = function () {
}; };
this._connection.createFunction('regexp', 2, rx); 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; return this._connection;
} }

View file

@ -70,7 +70,14 @@ Zotero.Report = new function() {
// Independent note // Independent note
if (arr['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'; content += '<ul class="notes">\n';
for each(var note in arr.reportChildren.notes) { for each(var note in arr.reportChildren.notes) {
content += '<li id="i' + note.itemID + '">\n'; 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 // Child note tags
content += _generateTagsList(note); content += _generateTagsList(note);

View file

@ -1945,6 +1945,11 @@ Zotero.Schema = new function(){
if (i==42) { if (i==42) {
Zotero.DB.query("UPDATE itemAttachments SET syncState=0"); 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); _updateDBVersion('userdata', toVersion);

View file

@ -113,12 +113,8 @@ ul.notes > li p:last-child {
/* Preserve whitespace on notes */ /* Preserve whitespace on notes */
ul.notes li p, li.note p { ul.notes li p.plaintext, li.note p.plaintext {
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap;
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+ */
} }
/* Display tags within child notes inline */ /* 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} 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 -- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema() -- here must be mirrored in transition steps in schema.js::_migrateSchema()