Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Seems to work across more Browsers now (Chrome, Opera, IE, Safari), s…

…ome more testing & bugfixes needed; fixed nasty linebreak problem; added rudimentary checksumming; added some error messages for the editor; added development status page; gadget support still missing
  • Loading branch information...
commit cd42f0e7f83dee2745eb729d7ab56cd9704a3e5e 1 parent 3375475
@p2k p2k authored
View
7 amqp_rpc_server.py
@@ -323,9 +323,12 @@ def handle_participant_message(self, wavelet, pconn, message):
Delta.createByOpManager(newdelta, wavelet.version).save()
+ # Create tentative checksums
+ blipsums = wavelet.blipsums()
+
# Respond
- self.emit(pconn, "OPERATION_MESSAGE_BUNDLE_ACK", wavelet.version)
- self.broadcast(wavelet, "OPERATION_MESSAGE_BUNDLE", {"version": wavelet.version, "operations": newdelta.serialize()}, [pconn])
+ self.emit(pconn, "OPERATION_MESSAGE_BUNDLE_ACK", {"version": wavelet.version, "blipsums": blipsums})
+ self.broadcast(wavelet, "OPERATION_MESSAGE_BUNDLE", {"version": wavelet.version, "operations": newdelta.serialize(), "blipsums": blipsums}, [pconn])
logger.info("[%s/%d@%s] Processed delta #%d -> v%d" % (participant.name, pconn.id, wavelet.wave.id, version, wavelet.version))
View
47 media/css/pygowave-client-style.css
@@ -160,6 +160,7 @@
width: 100%;
height: auto;
padding: 0;
+ background-color: #FFFFFF;
}
.blip_container_widget hr
@@ -181,6 +182,7 @@
border-bottom: 2px solid #ddd;
overflow: auto;
font-size: 1.3em;
+ outline: none;
}
.blip_editor_widget p
@@ -192,10 +194,12 @@
padding: 0;
}
-.blip_line_placeholder
+.blip_editor_placeholder
{
- background-color: yellow;
- overflow: hidden;
+ margin-left: 2px;
+ width: 5px;
+ height: 1.1em;
+ background-color: rgb(201, 226, 252);
}
.search_widget
@@ -334,3 +338,40 @@
width: 40%;
height: 100%;
}
+
+.error_overlay
+{
+ width: 100%;
+ overflow: hidden;
+}
+
+.error_overlay .bg_div
+{
+ background: url(../images/ui-bg_diagonals-thick_18_b81900_40x40.png) repeat scroll 50% 50%;
+ height: 100%;
+ clear: both;
+}
+
+.error_overlay .msg_div
+{
+ background-color: #CCCCCC;
+ width: 100%;
+ height: 31px;
+ background-image: url(../images/blip_error.png);
+ background-repeat: no-repeat;
+ background-position: 4px 4px;
+}
+
+.error_overlay .msg
+{
+ float: left;
+ padding-left: 31px;
+ padding-top: 7px;
+ padding-bottom: 7px;
+}
+
+.error_overlay button
+{
+ margin-top: 3px;
+ margin-bottom: 0px;
+}
View
6 media/css/style.css
@@ -355,6 +355,12 @@ p
background-position: 0 3px;
}
+.compat_table .os.used
+{
+ text-align: left;
+ height: 24px;
+}
+
.compat_table .os.win {background-image: url(../images/compat/win.png);}
.compat_table .os.lin {background-image: url(../images/compat/linux.png);}
.compat_table .os.mac {background-image: url(../images/compat/mac.png);}
View
BIN  media/images/blip_error.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
163 media/js/sha1.js
@@ -0,0 +1,163 @@
+/**
+*
+* Secure Hash Algorithm (SHA1)
+* http://www.webtoolkit.info/
+*
+**/
+
+function SHA1 (msg) {
+ function rotate_left(n,s) {
+ var t4 = ( n<<s ) | (n>>>(32-s));
+ return t4;
+ };
+
+ function lsb_hex(val) {
+ var str="";
+ var i;
+ var vh;
+ var vl;
+
+ for( i=0; i<=6; i+=2 ) {
+ vh = (val>>>(i*4+4))&0x0f;
+ vl = (val>>>(i*4))&0x0f;
+ str += vh.toString(16) + vl.toString(16);
+ }
+ return str;
+ };
+
+ function cvt_hex(val) {
+ var str="";
+ var i;
+ var v;
+
+ for( i=7; i>=0; i-- ) {
+ v = (val>>>(i*4))&0x0f;
+ str += v.toString(16);
+ }
+ return str;
+ };
+
+ function Utf8Encode(string) {
+ string = string.replace(/\r\n/g,"\n");
+ var utftext = "";
+
+ for (var n = 0; n < string.length; n++) {
+ var c = string.charCodeAt(n);
+ if (c < 128) {
+ utftext += String.fromCharCode(c);
+ }
+ else if((c > 127) && (c < 2048)) {
+ utftext += String.fromCharCode((c >> 6) | 192);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ else {
+ utftext += String.fromCharCode((c >> 12) | 224);
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ }
+
+ return utftext;
+ };
+
+ var blockstart;
+ var i, j;
+ var W = new Array(80);
+ var H0 = 0x67452301;
+ var H1 = 0xEFCDAB89;
+ var H2 = 0x98BADCFE;
+ var H3 = 0x10325476;
+ var H4 = 0xC3D2E1F0;
+ var A, B, C, D, E;
+ var temp;
+
+ msg = Utf8Encode(msg);
+
+ var msg_len = msg.length;
+
+ var word_array = new Array();
+ for( i=0; i<msg_len-3; i+=4 ) {
+ j = msg.charCodeAt(i)<<24 | msg.charCodeAt(i+1)<<16 |
+ msg.charCodeAt(i+2)<<8 | msg.charCodeAt(i+3);
+ word_array.push( j );
+ }
+
+ switch( msg_len % 4 ) {
+ case 0:
+ i = 0x080000000;
+ break;
+ case 1:
+ i = msg.charCodeAt(msg_len-1)<<24 | 0x0800000;
+ break;
+ case 2:
+ i = msg.charCodeAt(msg_len-2)<<24 | msg.charCodeAt(msg_len-1)<<16 | 0x08000;
+ break;
+ case 3:
+ i = msg.charCodeAt(msg_len-3)<<24 | msg.charCodeAt(msg_len-2)<<16 | msg.charCodeAt(msg_len-1)<<8 | 0x80;
+ break;
+ }
+
+ word_array.push( i );
+
+ while( (word_array.length % 16) != 14 ) word_array.push( 0 );
+
+ word_array.push( msg_len>>>29 );
+ word_array.push( (msg_len<<3)&0x0ffffffff );
+
+ for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ) {
+ for( i=0; i<16; i++ ) W[i] = word_array[blockstart+i];
+ for( i=16; i<=79; i++ ) W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
+
+ A = H0;
+ B = H1;
+ C = H2;
+ D = H3;
+ E = H4;
+
+ for( i= 0; i<=19; i++ ) {
+ temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
+ E = D;
+ D = C;
+ C = rotate_left(B,30);
+ B = A;
+ A = temp;
+ }
+
+ for( i=20; i<=39; i++ ) {
+ temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
+ E = D;
+ D = C;
+ C = rotate_left(B,30);
+ B = A;
+ A = temp;
+ }
+
+ for( i=40; i<=59; i++ ) {
+ temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
+ E = D;
+ D = C;
+ C = rotate_left(B,30);
+ B = A;
+ A = temp;
+ }
+
+ for( i=60; i<=79; i++ ) {
+ temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
+ E = D;
+ D = C;
+ C = rotate_left(B,30);
+ B = A;
+ A = temp;
+ }
+
+ H0 = (H0 + A) & 0x0ffffffff;
+ H1 = (H1 + B) & 0x0ffffffff;
+ H2 = (H2 + C) & 0x0ffffffff;
+ H3 = (H3 + D) & 0x0ffffffff;
+ H4 = (H4 + E) & 0x0ffffffff;
+ }
+
+ var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
+
+ return temp.toLowerCase();
+}
View
30 pygowave_client/cache/model/model.js
@@ -452,6 +452,12 @@ pygowave.model = (function() {
* @param {int} index Offset where the text is deleted
* @param {int} length Number of characters to delete
*/
+
+ /**
+ * Fired if the Blip has gone out of sync with the server.
+ *
+ * @event onOutOfSync
+ */
,
/**
@@ -480,6 +486,7 @@ pygowave.model = (function() {
this._content = content;
this._elements = [];
this._annotations = [];
+ this._outofsync = false;
},
/**
@@ -575,6 +582,29 @@ pygowave.model = (function() {
*/
content: function () {
return this._content;
+ },
+
+ /**
+ * Calculate a checksum of this Blip and compare it against the given
+ * checksum. Fires {@link pygowave.model.Blip.onOutOfSync onOutOfSync} if the checksum is wrong. Returns
+ * true if the checksum is ok.
+ *
+ * Note: Currently this only calculates the SHA-1 of the Blip's text. This
+ * is tentative and subject to change
+ *
+ * @function {public Boolean} checkSync
+ * @param {String} sum Input checksum to compare against
+ */
+ checkSync: function (sum) {
+ if (this._outofsync)
+ return false;
+ if (SHA1(this._content) != sum) {
+ this.fireEvent("outOfSync");
+ this._outofsync = true;
+ return false;
+ }
+ else
+ return true;
}
});
View
32 pygowave_client/src/controller/controller.js
@@ -161,7 +161,7 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
* @param {int} code Error code provided by socket API.
*/
onConnError: function (code) {
- alert("Error: " + code);
+ this._iview.showControllerError(gettext("A connection error occured.<br/><br/>Error code: %d").sprintf(code));
},
/**
@@ -196,16 +196,22 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this._requestParticipantInfo(wavelet_id);
break;
case "OPERATION_MESSAGE_BUNDLE_ACK":
- wavelet_model.options.version = msg.property;
+ wavelet_model.options.version = msg.property.version;
this.wavelets[wavelet_id].mpending.fetch(); // Clear
if (!this.wavelets[wavelet_id].mcached.isEmpty())
- this._transferOperations(wavelet_id);
- else
+ this._transferOperations(wavelet_id); // Send cached
+ else {
+ // All done, we can do a check-up
+ this._checkBlips(wavelet_id, msg.property.blipsums);
this.wavelets[wavelet_id].pending = false;
+ }
break;
case "OPERATION_MESSAGE_BUNDLE":
this._processOperations(wavelet_model, msg.property.operations);
wavelet_model.options.version = msg.property.version;
+ // Do a check-up if possible
+ if (!this.hasPendingOperations(wavelet_id))
+ this._checkBlips(wavelet_id, msg.property.blipsums);
break;
case "PARTICIPANT_INFO":
this._processParticipantsInfo(msg.property);
@@ -441,6 +447,24 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
}
}
},
+ /**
+ * Calculate and compare checksums of all Blips to the given map.
+ * Show an error message, usually when it's already too late ;)
+ *
+ * @function _checkBlips
+ * @param {String} waveletId ID of the Wavelet
+ * @param {Object} blipsums Checksums to compare to
+ */
+ _checkBlips: function (waveletId, blipsums) {
+ var wavelet_model = this.wavelets[waveletId].model;
+ for (var it = new _Iterator(blipsums); it.hasNext(); ) {
+ var checksum = it.next();
+ var blipId = it.key();
+ var blip = wavelet_model.blipById(blipId);
+ if ($defined(blip))
+ blip.checkSync(checksum);
+ }
+ },
/**
* Callback from view on text insertion.
View
31 pygowave_client/src/model/model.py
@@ -24,6 +24,8 @@
from pycow.decorators import Class, Implements
from pycow.utils import Events, Options, Hash
+# BIG NOTE: Python-compatibility currently broken, because a SHA1 function is missing
+
__all__ = ["WaveModel", "Participant"]
@Implements(Options, Events)
@@ -432,6 +434,12 @@ class Blip(object):
@param {int} index Offset where the text is deleted
@param {int} length Number of characters to delete
"""
+
+ """
+ Fired if the Blip has gone out of sync with the server.
+
+ @event onOutOfSync
+ """
# ---------------------------
def __init__(self, wavelet, id, options, content = "", parent = None):
@@ -459,6 +467,8 @@ def __init__(self, wavelet, id, options, content = "", parent = None):
self._content = content
self._elements = []
self._annotations = []
+
+ self._outofsync = False
def id(self):
"""
@@ -541,6 +551,27 @@ def content(self):
@function {public String} content
"""
return self._content
+
+ def checkSync(self, sum):
+ """
+ Calculate a checksum of this Blip and compare it against the given
+ checksum. Fires {@link pygowave.model.Blip.onOutOfSync onOutOfSync} if the checksum is wrong. Returns
+ true if the checksum is ok.
+
+ Note: Currently this only calculates the SHA-1 of the Blip's text. This
+ is tentative and subject to change
+
+ @function {public Boolean} checkSync
+ @param {String} sum Input checksum to compare against
+ """
+ if self._outofsync:
+ return False
+ if SHA1(self._content) != sum:
+ self.fireEvent("outOfSync")
+ self._outofsync = True
+ return False
+ else:
+ return True
@Implements(Options, Events)
@Class
View
421 pygowave_client/src/view/blip_editor.js
@@ -55,9 +55,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._blip = blip;
this._lastRange = [0, 0];
this._lastContent = "";
+ this._errDiv = null;
var contentElement = new Element('div', {
- 'class': 'blip_editor_widget'
+ 'class': 'blip_editor_widget',
+ 'spellcheck': 'false'
});
this._onInsertText = this._onInsertText.bind(this);
@@ -67,10 +69,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._onKeyUp = this._onKeyUp.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onWindowResize = this._onWindowResize.bind(this);
+ this._onOutOfSync = this._onOutOfSync.bind(this);
this.parent(parentElement, contentElement, where);
-
- contentElement.contentEditable = "true";
},
/**
* Overridden from {@link pygowave.view.Widget.dispose Widget.dispose}.<br/>
@@ -97,16 +100,23 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
blip.addEvents({
insertText: this._onInsertText,
- deleteText: this._onDeleteText
+ deleteText: this._onDeleteText,
+ outOfSync: this._onOutOfSync
});
+
this.contentElement.addEvents({
keydown: this._onKeyDown,
keypress: this._onKeyPress,
keyup: this._onKeyUp,
contextmenu: this._onContextMenu,
- mouseup: this._onMouseUp
+ mouseup: this._onMouseUp,
+ mousedown: this._onMouseDown
});
+ this._processing = false;
+ window.addEvent('resize', this._onWindowResize);
+ this._onSyncCheckTimer = this._onSyncCheck.periodical(2000, this);
+
return this;
},
/**
@@ -123,7 +133,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._lastContent = "";
this._blip.removeEvents({
insertText: this._onInsertText,
- deleteText: this._onDeleteText
+ deleteText: this._onDeleteText,
+ outOfSync: this._onOutOfSync
});
this.contentElement.removeEvents({
keydown: this._onKeyDown,
@@ -132,6 +143,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
contextmenu: this._onContextMenu,
mouseup: this._onMouseUp
});
+ window.removeEvent('resize', this._onWindowResize);
+ $clear(this._onSyncCheckTimer);
},
/**
@@ -142,29 +155,96 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
reloadContent: function () {
// Clean up first
- for (var it = new _Iterator(this.contentElement.getChildren()); it.hasNext(); ) {
- var oldelt = it.next();
- if (oldelt.get('tag') == 'p')
- oldelt.eliminate("isEmpty");
- oldelt.dispose();
- }
+ for (var it = new _Iterator(this.contentElement.getChildren()); it.hasNext(); )
+ it.next().destroy();
+
+ this.contentElement.setStyle("opacity", 1);
+ this._removeErrorOverlay();
+
// Build up
var content = this._blip.content().replace(/ /g, "\u00a0");
var lines = content.split("\n"), line, pg;
for (var i = 0; i < lines.length; i++) {
line = lines[i];
pg = new Element("p");
- if (line == "") { // Annotate empty nodes
- //pg.store("isEmpty", true);
- //line = "\u00a0";
- Element("div", {'class': 'blip_line_placeholder', 'text': '*'}).inject(pg);
- }
- else {
- //pg.store("isEmpty", false);
+ if (line != "")
pg.set('text', line);
- }
+ if (!Browser.Engine.trident) // IE allows empty paragraphs
+ new Element("br").inject(pg); // Others use an implicit br
pg.inject(this.contentElement);
}
+
+ this._lastContent = this.contentToString();
+ if (this._blip.content() != this._lastContent)
+ this._displayErrorOverlay("render_fail");
+ else
+ this.contentElement.contentEditable = "true";
+ },
+ /**
+ * Places an overlay over the editor and displays an error message with
+ * a button.
+ * @function {private} _displayErrorOverlay
+ * @param {String} type Error type. May be 'resync', 'render_fail'.
+ */
+ _displayErrorOverlay: function (type) {
+ this._removeErrorOverlay();
+
+ this._errDiv = new Element("div", {'class': 'error_overlay'}).inject(this.parentElement);
+ var msgdiv = new Element("div", {'class': 'msg_div'}).inject(this._errDiv);
+ var bg_div = new Element("div", {'class': 'bg_div'}).inject(this._errDiv);
+ bg_div.setStyle("opacity", 0.5);
+ var coords = this.contentElement.getCoordinates(false);
+
+ switch (type) {
+ case "resync":
+ new Element("div", {'class': 'msg', 'text': gettext("Sorry, but this Blip Editor has gone out of sync with the internal representation. Please click resync.")}).inject(msgdiv);
+ new Element("button", {'class': 'mochaButton', 'text': gettext("Resync")})
+ .inject(msgdiv).addEvent('click', this.reloadContent.bind(this));
+ break;
+
+ case "render_fail":
+ new Element("div", {'class': 'msg', 'text': gettext("The text content could not be rendered correctly. This may be a bug or you are using an unsupported browser.")}).inject(msgdiv);
+ new Element("button", {'class': 'mochaButton', 'text': gettext("Dismiss")})
+ .inject(msgdiv).addEvent('click', this._removeErrorOverlay.bind(this));
+ $clear(this._onSyncCheckTimer);
+ break;
+
+ case "checksum_fail":
+ new Element("div", {'class': 'msg', 'text': gettext("Unfortunately, this Blip has gone out of sync with the server. You have to reload the page to be able to use it again.")}).inject(msgdiv);
+ new Element("button", {'class': 'mochaButton', 'text': gettext("Reload")})
+ .inject(msgdiv).addEvent('click', function () {
+ window.location.href = window.location.href;
+ });
+ $clear(this._onSyncCheckTimer);
+ break;
+ }
+
+ this._errDiv.setStyles({
+ position: "absolute",
+ top: coords.top,
+ left: coords.left,
+ width: coords.width,
+ height: coords.height,
+ opacity: 0
+ });
+ new Fx.Tween(this._errDiv, {duration: 250}).start("opacity", 0, 1);
+ new Fx.Tween(this.contentElement, {duration: 250}).start("opacity", 1, 0.5);
+ this.contentElement.contentEditable = "false";
+ },
+ /**
+ * Removes the error overlay displayed by _displayErrorOverlay.
+ * @function {private} _removeErrorOverlay
+ */
+ _removeErrorOverlay: function () {
+ if (!$defined(this._errDiv))
+ return;
+ this.contentElement.setStyle("opacity", 1);
+ var toKill = this._errDiv;
+ this._errDiv = null;
+ new Fx.Tween(toKill, {duration: 250}).start("opacity", 1, 0).chain(function () {
+ toKill.destroy();
+ toKill = null;
+ });
},
/**
@@ -181,8 +261,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
else
cleaned += "\n";
- if (!elt.retrieve("isEmpty", false))
- cleaned += elt.get('text').replace(/&nbsp;/gi, " ");
+ cleaned += elt.get('text').replace(/\u00a0/gi, " ");
}
return cleaned;
},
@@ -204,61 +283,24 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
var start = this._walkUp(sel.startNode(), sel.startOffset());
var end = this._walkUp(sel.endNode(), sel.endOffset());
- dbgprint(start, end);
+ var dbg = $('debug_cursor_pos');
+ if ($defined(dbg))
+ dbg.set('text', start + " / " + end);
return [start, end];
},
- /**
- * Check if an empty node was filled.
- * @function {private} _checkInsertion
- * @param {pygowave.view.Selection} sel Selection object
- */
- _checkEmptyNodes: function (sel) {
- if (!sel.isValid())
- return;
- sel.moveDown(); // Move down, only treat text nodes of p's
-
- var elt = sel.endNode(), parent, txt;
- if ($type(elt) != 'element') {
- parent = elt.parentNode;
- txt = parent.get('text');
- if (parent.retrieve('isEmpty', false) && txt != "\u00a0") {
- txt = elt.data;
- if (txt == "") {
- txt.appendData("\u00a0");
- }
- else if (txt.substring(sel.endOffset()) == "\u00a0") {
- elt.deleteData(txt.length-1, 1);
- parent.eliminate('isEmpty');
- }
- else {
- elt.deleteData(0, 1);
- sel._startOffset -= 1; //HACK
- if (sel.startNode() == sel.endNode())
- sel._endOffset -= 1; //HACK
- parent.eliminate('isEmpty');
- }
- }
- else if (txt == "") {
- txt.appendData("\u00a0");
- parent.store('isEmpty', true);
- }
- }
- },
_onKeyDown: function (e) {
- // Currently not needed
+ this._processing = true;
},
_onKeyPress: function (e) {
- // Currently not needed
+ this._processing = true;
},
_onKeyUp: function(e) {
- var sel = Selection.currentSelection(this.contentElement);
- this._checkEmptyNodes(sel);
-
+ this._processing = true;
var newContent = this.contentToString();
- var newRange = this.currentTextRange(sel);
+ var newRange = this.currentTextRange();
// This code should be accurate for now. However, the context menu is not handeled properly.
if (e.key == "backspace" || e.key == "delete") {
@@ -321,6 +363,10 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._lastContent = newContent;
this._lastRange = newRange;
+ this._processing = false;
+ },
+ _onMouseDown: function (e) {
+ // Currently not needed
},
_onMouseUp: function(e) {
this._lastRange = this.currentTextRange();
@@ -330,12 +376,18 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
},
_onInsertText: function (index, text) {
+ //TODO: this function assumes no formatting elements
+ this._processing = true;
+
text = text.replace(/ /g, "\u00a0"); // Convert spaces to protected spaces
var ret = this._walkDown(this.contentElement, index);
- var elt = ret[0], offset = ret[1];
- if ($type(elt) != "textnode" && $type(elt) != "whitespace") { // Should be impossible...
- var newtn = document.createTextNode("");
- elt.parentNode.insertBefore(newtn, elt);
+ var elt = ret[0], offset = ret[1], newelt, newtn;
+ if ($type(elt) == "element" && elt.get('tag') == "p") { // Empty paragraphs
+ newtn = document.createTextNode("");
+ if (!Browser.Engine.trident)
+ elt.insertBefore(newtn, elt.firstChild);
+ else
+ elt.appendChild(newtn);
elt = newtn;
offset = 0;
}
@@ -343,62 +395,98 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
var parent = elt.parentNode; // Parent paragraph
for (var i = 0; i < lines.length; i++) {
if (lines[i].length > 0) {
- if (offset == elt.data.length && Browser.Engine.trident) { // Buggy in IE
+ if (!$defined(elt.data)) {
+ dbgprint("wtf!");
+ }
+ if (offset == elt.data.length && Browser.Engine.trident) { // Appending is buggy in IE
elt.appendData(" ");
elt.insertData(offset, lines[i]);
elt.deleteData(elt.data.length-1, 1);
}
else
elt.insertData(offset, lines[i]);
- if (parent.retrieve("isEmpty", false)) {
- // Remove implicit newline
- elt.deleteData(elt.data.length-1, 1);
- parent.eliminate("isEmpty");
- }
}
if (i < lines.length-1) { // Handle newlines
- rest = elt.data.substring(offset+lines[i].length);
- if (rest.length > 0)
- elt.deleteData(offset+lines[i].length, rest.length); // Cutoff remainder
- elt = new Element("p");
- if (rest == "") {
- elt.set('text', "\u00a0");
- elt.store("isEmpty", true);
+ if (offset < elt.data.length && offset+lines[i].length == 0) {
+ // Special case: effectively append an empty node before the current node
+ newelt = new Element("p").inject(parent, 'before');
+ if (!Browser.Engine.trident)
+ new Element("br").inject(newelt);
+ newelt = elt;
}
- else
- elt.set('text', rest);
- elt.inject(parent, 'after');
- parent = elt;
- elt = parent.firstChild; // Set to textnode
+ else {
+ newelt = new Element("p").inject(parent, 'after');
+ if (offset < elt.data.length) {
+ if (offset+lines[i].length > 0)
+ elt.splitText(offset+lines[i].length); // Split
+ }
+
+ // Move siblings to new node
+ if (!Browser.Engine.trident) {
+ new Element("br").inject(newelt);
+ while ($defined(elt.nextSibling) && elt.nextSibling != parent.lastChild)
+ newelt.insertBefore(elt.nextSibling, newelt.firstChild);
+ }
+ else {
+ while ($defined(elt.nextSibling)) {
+ if ($defined(newelt.firstChild))
+ newelt.insertBefore(elt.nextSibling, newelt.firstChild);
+ else
+ newelt.appendChild(elt.nextSibling);
+ }
+ }
+
+ if (!$defined(newelt.firstChild))
+ newelt.appendChild(document.createTextNode(""));
+ }
+
+ elt = newelt.firstChild;
offset = 0;
}
}
+ this._processing = false;
},
_onDeleteText: function (index, length) {
+ // Safe for formatting elements
+ this._processing = true;
+
var ret = this._walkDown(this.contentElement, index);
var elt = ret[0], next = null;
var offset = ret[1], dellength = 0;
- while (length > 0) {
- next = elt.nextSibling;
- if ($type(elt) == "element") {
- if (elt.get("tag") == "br") {
- elt.parentNode.removeChild(elt);
- length--;
- offset = 0;
- }
- }
- else if ($type(elt) == "textnode") {
+ if ($type(elt) == "element" && elt.get('tag') == "br") // Got the implicit br (TODO should be impossible to reach; check again)
+ elt = elt.parentElement;
+ while (length > 0 && $defined(elt)) {
+ next = this._nextTextOrPara(elt);
+ if ($type(elt) == "textnode") {
dellength = elt.data.length-offset;
if (dellength > length)
dellength = length;
if (dellength > 0)
elt.deleteData(offset, dellength);
length -= dellength;
+ if (length > 0 && $type(next) == "element" && next.get('tag') == "p") {
+ // Merge, then next is empty and will be deleted
+ if (!Browser.Engine.trident) {
+ while ($defined(next.firstChild) && next.firstChild != next.lastChild)
+ elt.parentNode.insertBefore(next.firstChild, elt.parentNode.lastChild);
+ }
+ else {
+ while ($defined(next.firstChild))
+ elt.parentNode.appendChild(next.firstChild);
+ }
+ }
+ }
+ else if ($type(elt) == "element" && elt.get('tag') == "p") { // Empty p
+ elt.dispose();
+ length--;
}
elt = next;
- if (!$defined(elt))
- break; // Deleted past end...
+ offset = 0;
}
+ this._processing = false;
+ },
+ _onOutOfSync: function () {
+ this._displayErrorOverlay('checksum_fail');
},
/**
@@ -468,42 +556,127 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
_walkDown: function (body, offset) {
var elt = body.firstChild;
- var next;
+
+ var next = this._nextTextOrPara(elt, true, true); // Try to descend to first text node
+ if ($type(next) == "textnode")
+ elt = next;
while (offset > 0 && elt != null) {
- next = null;
- if ($type(elt) == "element") {
- if (elt.get('tag') != "iframe" && (elt.get('tag') != "p" || !elt.retrieve("isEmpty", false)))
- next = elt.firstChild; // descend
- }
-
- if (next != null) {
- elt = next;
- continue;
+ next = this._nextTextOrPara(elt, true, true);
+ if ($type(elt) == "element") { // p or iframe
+ offset--;
+ if (offset == 0 && elt.get('tag') == "p" && (next == null || $type(next) == "element")) // Empty p
+ break;
}
-
- if ($type(elt) == "textnode") {
+ else if ($type(elt) == "textnode") {
if (offset <= elt.data.length)
- return [elt, offset];
+ break;
offset -= elt.data.length;
}
- else if ($type(elt) == "element" && (elt.get('tag') == "iframe" || elt.get('tag') == "p"))
- offset--;
-
+ elt = next;
+ }
+
+ if (offset == 0 && $type(elt) == "element" && elt.get('tag') == "p") {
+ next = this._nextTextOrPara(elt, true, true); // Try to descend to first text node
+ if ($type(next) == "textnode")
+ elt = next;
+ }
+
+ return [elt, offset];
+ },
+ /**
+ * Find the next TextNode or Paragraph.
+ *
+ * @function {private Node} _nextTextOrPara
+ * @param {Node} elt Start node
+ * @param {optional Boolean} descend Set to true if you want to descend
+ * first if possible, rather than moving to the next sibling.
+ * @param {optional Boolean} iframe_stop Set to true if you also want
+ * iframes to be returned.
+ */
+ _nextTextOrPara: function (elt, descend, iframe_stop) {
+ if (!$defined(descend))
+ descend = false;
+ if (!$defined(iframe_stop))
+ iframe_stop = false;
+
+ var next;
+ if (descend
+ && $type(elt) == "element"
+ && elt.get('tag') != "iframe"
+ && $defined(elt.firstChild))
+ next = elt.firstChild;
+ else
next = elt.nextSibling;
- if (next == null) { // ascend
+
+ while (!$defined(next) || $type(next) == "element") {
+ if (!$defined(next)) { // ascend
elt = elt.parentNode;
- elt = elt.nextSibling;
- if ($type(elt) == "element" && elt.get('tag') == "p") {
- elt = elt.firstChild; // auto-descent
- offset--;
- }
+ if (elt == this.contentElement)
+ return null; // The end
+ next = elt.nextSibling;
+ continue;
}
- else
+ if (next.get('tag') == "p")
+ return next; // Done
+ else { // descend or move forward
elt = next;
+ if (iframe_stop && elt.get('tag') == "iframe")
+ return elt;
+ if (elt.get('tag') != "iframe" && $defined(elt.firstChild))
+ next = elt.firstChild;
+ else
+ next = elt.nextSibling;
+ }
}
- return [elt, offset];
+ return next;
+ },
+ /**
+ * Callback on window resize.
+ *
+ * @function {private} _onWindowResize
+ */
+ _onWindowResize: function () {
+ if ($defined(this._errDiv)) {
+ var coords = this.contentElement.getCoordinates(false);
+ this._errDiv.setStyles({
+ top: coords.top,
+ left: coords.left,
+ width: coords.width,
+ height: coords.height
+ });
+ }
+ },
+ /**
+ * This function is called periodically to check Blip rendering
+ * consistency.
+ * @function {private} _onSyncCheck
+ */
+ _onSyncCheck: function () {
+ if (this._processing)
+ return;
+
+ var content = this.contentToString();
+ if (content != this._blip.content()) {
+ if (!$defined(this._errDiv)) {
+ window.console.info("diff: "+this._diff(content, this._blip.content()));
+ this._displayErrorOverlay("resync");
+ }
+ }
+ else if ($defined(this._errDiv))
+ this._removeErrorOverlay();
+ },
+ _diff: function (a, b) {
+ if (a.length > b.length)
+ return "a > b";
+ if (a.length < b.length)
+ return "a < b";
+ for (var i = 0; i < a.length; i++) {
+ if (a[i] != b[i])
+ return "a[%d] != b[%d] / %d != %d".sprintf(i,i,a.charCodeAt(i),b.charCodeAt(i));
+ }
+ return "a == b";
}
});
View
42 pygowave_client/src/view/selection.js
@@ -237,6 +237,25 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
rng.setEnd(this._endNode, this._endOffset);
}
return rng;
+ },
+ /**
+ * Set the user's current selection to this selection.
+ * @function {public} select
+ */
+ select: function () {
+ var range = this.toNativeRange();
+ if (range.select){
+ $try(function(){
+ range.select();
+ });
+ } else {
+ var win = this._doc.window;
+ var s = $defined(win.getSelection) ? win.getSelection() : this._doc.selection;
+ if (s.addRange) {
+ s.removeAllRanges();
+ s.addRange(range);
+ }
+ }
}
});
/**
@@ -251,6 +270,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
var elt = scope;
var ret = new Selection(null, 0, null, 0, ownerDocument);
var todo = 'Start';
+ var emptyElement = false;
var rng = ownerDocument.body.createTextRange();
while (true) {
setRangeToNode(rng, elt);
@@ -276,8 +296,22 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
pos = textrange.compareEndPoints(todo + 'ToEnd', rng);
if (pos <= 0) { // left of/hit - descend/found
if ($type(elt) == "element") {
- elt = elt.firstChild;
- continue;
+ if ($defined(elt.firstChild)) {
+ elt = elt.firstChild;
+ continue;
+ }
+ else { // Empty element; treat as hit
+ emptyElement = true;
+ if (todo == 'Start') {
+ ret.setStart(elt, 0);
+ todo = 'End';
+ continue;
+ }
+ else {
+ ret.setEnd(elt, 0);
+ break;
+ }
+ }
}
else {
// In textnode
@@ -323,8 +357,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
elt = elt.nextSibling;
}
- if (!textrange.isEqual(ret.toNativeRange()))
- alert("IE bug: Could not create Selection object from TextRange!");
+ if (!emptyElement && !textrange.isEqual(ret.toNativeRange()))
+ alert(gettext("Internet Explorer bug:\nCould not create Selection object from TextRange!"));
return ret;
};
View
25 pygowave_server/models.py
@@ -23,6 +23,7 @@
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.db import transaction
+from django.utils.hashcompat import sha_constructor as sha1
from django.utils import simplejson
@@ -238,7 +239,7 @@ def serialize_blips(self):
for blip in self.blips.all():
blipmap[blip.id] = blip.serialize()
return blipmap
-
+
def applyOperations(self, ops):
"""
Apply the operations on the wavelet.
@@ -254,6 +255,16 @@ def applyOperations(self, ops):
blip.text = blip.text[:op.index] + op.property + blip.text[op.index:]
blip.save()
+ def blipsums(self):
+ """
+ Calculates the checksums of all Blips.
+
+ """
+ blipsums = {}
+ for blip in self.blips.all():
+ blipsums[blip.id] = blip.checksum()
+ return blipsums
+
class DataDocument(models.Model):
"""
A DataDocument contains arbitrary data for storage of metadata in wavelets.
@@ -349,9 +360,19 @@ def serialize(self):
"lastModifiedTime": datetime2milliseconds(self.last_modified),
"childBlipIds": map(lambda c: c.id, self.children.all()),
"waveId": self.wavelet.wave.id,
- "submitted": bool(self.submitted)
+ "submitted": bool(self.submitted),
+ "checksum": self.checksum() # Note: This is tentative and subject to change
}
+ def checksum(self):
+ """
+ Calculate a checksum of this Blip.
+ Note: Currently this is only the SHA-1 of the Blip's text. This is
+ tentative and subject to change
+
+ """
+ return sha1(self.text.encode("utf-8")).hexdigest()
+
def __unicode__(self):
return u"Blip %s on %s" % (self.id, unicode(self.wavelet))
View
26 templates/pygowave_server/project_status.html
@@ -8,7 +8,7 @@
{% endblocktrans %}
</p>
<p>
-{% with "20.08.2009"|todate:"%d.%m.%Y"|date as date %}
+{% with "21.08.2009"|todate:"%d.%m.%Y"|date as date %}
{% blocktrans %}
PyGoWave has been tested and should work on the following browsers and
operating systems (the percentages reflect the user base as of {{ date }}).
@@ -16,44 +16,52 @@
{% endwith %}
<table class="compat_table">
<tr>
+ <td class="browser"></td>
+ <td class="used"></td>
+ <td class="name"></td>
+ <td class="os win used">60.2%</td>
+ <td class="os mac used">21.6%</td>
+ <td class="os lin used">16.7%</td>
+ </tr>
+ <tr>
<td class="browser"><img alt="Firefox" src="{{ MEDIA_URL }}images/compat/firefox.png" height="16" width="16" /></td>
- <td class="used">48.2%</td>
+ <td class="used">48.1%</td>
<td class="name">Mozilla Firefox</td>
<td class="os win">3.5.2</td>
+ <td class="os mac">(untested)</td>
<td class="os lin">3.5.2</td>
- <td class="os mac">?</td>
</tr>
<tr>
<td class="browser"><img alt="Chrome" src="{{ MEDIA_URL }}images/compat/chrome.png" height="16" width="16" /></td>
<td class="used">25.6%</td>
<td class="name">Google Chrome</td>
<td class="os win">2.0.172.39</td>
+ <td class="os mac">(untested)</td>
<td class="os lin">3.0.198.1</td>
- <td class="os mac">?</td>
</tr>
<tr>
<td class="browser"><img alt="Safari" src="{{ MEDIA_URL }}images/compat/safari.png" height="16" width="16" /></td>
<td class="used">13.6%</td>
<td class="name">Safari</td>
<td class="os win">4.0.530.17</td>
- <td class="os"></td>
- <td class="os mac">?</td>
+ <td class="os mac">(untested)</td>
+ <td class="os">-</td>
</tr>
<tr>
<td class="browser"><img alt="Safari" src="{{ MEDIA_URL }}images/compat/opera.png" height="16" width="16" /></td>
<td class="used">4.6%</td>
<td class="name">Opera</td>
<td class="os win">10.0 b3</td>
+ <td class="os mac">(untested)</td>
<td class="os lin">9.64</td>
- <td class="os mac">?</td>
</tr>
<tr>
<td class="browser"><img alt="IE" src="{{ MEDIA_URL }}images/compat/ie.png" height="16" width="16" /></td>
<td class="used">4.4%</td>
<td class="name">Internet Explorer</td>
<td class="os win">8.0.6001</td>
- <td class="os"></td>
- <td class="os"></td>
+ <td class="os">-</td>
+ <td class="os">-</td>
</tr>
</table>
</p>
View
3  templates/pygowave_server/waves/on_the_wave.html
@@ -29,6 +29,7 @@
<link rel="stylesheet" href="{{ MEDIA_URL }}css/MooEditable.css" type="text/css" />
<script type="text/javascript" src="{{ MEDIA_URL }}js/MooEditable.js"></script>
<script type="text/javascript" src="{{ MEDIA_URL }}js/pycow.js"></script>
+ <script type="text/javascript" src="{{ MEDIA_URL }}js/sha1.js"></script>
<script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
{% client_scripts %}
@@ -59,7 +60,7 @@
Again, this application is experimental and may not work as expected.
{% endblocktrans %}
</div>
-<h4>{% trans "Playground" %}</h4>
+<h4>{% trans "Playground" %} (Cursor: <span id="debug_cursor_pos"></span>)</h4>
<div class="section">
<div id="wave_container">
<!-- The content is dynamically rendered through our client script. -->
Please sign in to comment.
Something went wrong with that request. Please try again.