commit 651bcf2380feef12a596daed68654d00a56b76ab parent b46860f6a42849f6f8eae6730231a27c94697607 Author: Dan Stillman <dstillman@zotero.org> Date: Mon, 13 Oct 2008 19:51:53 +0000 Adds rich text support to notes - Still a few issues - Converts plaintext notes to HTML on upgrade Diffstat:
16 files changed, 284 insertions(+), 48 deletions(-)
diff --git a/chrome/content/zotero/addCitationDialog.xul b/chrome/content/zotero/addCitationDialog.xul @@ -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> diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml @@ -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> diff --git a/chrome/content/zotero/bindings/styled-textbox.xml b/chrome/content/zotero/bindings/styled-textbox.xml @@ -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 = { "\\":"\\\\", - "&":"&", - "<":"<", - ">":">", + "<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> \ No newline at end of file diff --git a/chrome/content/zotero/editBibliographyDialog.xul b/chrome/content/zotero/editBibliographyDialog.xul @@ -111,6 +111,6 @@ </hbox> </vbox> - <textbox id="editor" type="styled" flex="1"/> + <textbox id="editor" type="styled" mode="integration" flex="1"/> </vbox> </dialog> diff --git a/chrome/content/zotero/tiny_mce/integration.html b/chrome/content/zotero/tiny_mce/integration.html @@ -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> diff --git a/chrome/content/zotero/tiny_mce/note.html b/chrome/content/zotero/tiny_mce/note.html @@ -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> diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -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 : ''; diff --git a/chrome/content/zotero/xpcom/data/notes.js b/chrome/content/zotero/xpcom/data/notes.js @@ -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); diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -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, ' ') + .replace(/ /g, ' ') + + '</p>'; + return str.replace(/<p>\s*<\/p>/g, '<p> </p>'); + } + }; + this._connection.createFunction('text2html', 1, rx); return this._connection; } diff --git a/chrome/content/zotero/xpcom/report.js b/chrome/content/zotero/xpcom/report.js @@ -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); diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -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); diff --git a/chrome/skin/default/zotero/report/detail.css b/chrome/skin/default/zotero/report/detail.css @@ -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 */ diff --git a/chrome/skin/default/zotero/tinymce-content.css b/chrome/skin/default/zotero/tinymce-content.css @@ -1,2 +0,0 @@ -body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;} -p, div {margin:0; padding:0} -\ No newline at end of file diff --git a/chrome/skin/default/zotero/tinymce/integration-content.css b/chrome/skin/default/zotero/tinymce/integration-content.css @@ -0,0 +1,2 @@ +body, td, pre {font-family:Times New Roman, Times, serif; font-size:14px; margin: 8px;} +p, div {margin:0; padding:0} +\ No newline at end of file diff --git a/chrome/skin/default/zotero/tinymce/note-content.css b/chrome/skin/default/zotero/tinymce/note-content.css @@ -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: '”' +} +*/ diff --git a/userdata.sql b/userdata.sql @@ -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()