Skip to content
Browse files

The Blip Editor has eventually become stable; added some workarounds …

…for Opera; all in all v0.3 is ready for public testing, setup and migration will be tomorrow.
  • Loading branch information...
1 parent 9e0cfcb commit 7c3d36c2c29ba94690a72d0ad08e8aa277217098 @p2k p2k committed
View
12 media/css/pygowave-client-style.css
@@ -308,6 +308,18 @@
right: 0;
top: 2px;
cursor: pointer;
+ z-index: 2;
+}
+
+.blip_editor_widget .gadget_element .delete_box_opera
+{
+ width: 16px;
+ height: 16px;
+ background-image: url(../images/delete_gadget.png);
+ background-repeat: no-repeat;
+ float:right;
+ cursor: pointer;
+ z-index: 2;
}
.search_widget
View
107 pygowave_client/src/controller/controller.js
@@ -102,6 +102,7 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this._iview.addEvent('addParticipant', this._onAddParticipant.bind(this));
this._iview.addEvent('leaveWavelet', this._onLeaveWavelet.bind(this));
this._iview.addEvent('refreshGadgetList', this._onRefreshGadgetList.bind(this));
+ this._iview.addEvent('ready', this._onViewReady.bind(this));
this.waves = new Hash();
this.waves.set(model.id(), model);
@@ -110,6 +111,8 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this.new_participants = new Array();
this.participants = new Hash();
this._cachedGadgetList = null;
+ this._deferredMessageBundles = new Array();
+ this._processingDeferred = false;
// The connection object must be stored in this.conn and must have a sendJson and subscribeWavelet method (defined below).
this.conn = new STOMPClient(); // STOMP is used as communication protocol
@@ -202,22 +205,10 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
this._requestParticipantInfo(wavelet_id);
break;
case "OPERATION_MESSAGE_BUNDLE_ACK":
- 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); // Send cached
- else {
- // All done, we can do a check-up
- wavelet_model.checkSync(msg.property.blipsums);
- this.wavelets[wavelet_id].pending = false;
- }
+ this._queueMessageBundle(wavelet_model, "ACK", msg.property.version, msg.property.blipsums);
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))
- wavelet_model.checkSync(msg.property.blipsums);
+ this._queueMessageBundle(wavelet_model, msg.property.operations, msg.property.version, msg.property.blipsums);
break;
case "PARTICIPANT_INFO":
this._processParticipantsInfo(msg.property);
@@ -427,32 +418,92 @@ pygowave.controller = $defined(pygowave.controller) ? pygowave.controller : new
}
},
/**
+ * Queue a message bundle if the view is busy. Process it, if it is or
+ * goes ready.
+ *
+ * @function {private} _queueMessageBundle
+ * @param {pygowave.model.Wavelet} wavelet Reference to a Wavelet model
+ * @param {Object[]} serial_ops Serialized operations
+ * @param {int} version New version after this bundle
+ * @param {Object} blipsums Checksums to compare the wavelet to
+ */
+ _queueMessageBundle: function (wavelet, serial_ops, version, blipsums) {
+ while (this._processingDeferred); // Busy waiting
+ if (this._iview.isBusy()) {
+ this._deferredMessageBundles.push({
+ wavelet: wavelet,
+ serial_ops: serial_ops,
+ version: version,
+ blipsums: blipsums
+ });
+ }
+ else
+ this._processMessageBundle(wavelet, serial_ops, version, blipsums);
+ },
+ /**
* Process a message bundle from the server. Do transformation and
* apply it to the model.
*
* @function {private} _processOperations
* @param {pygowave.model.Wavelet} wavelet Wavelet model
* @param {Object[]} serial_ops Serialized operations
+ * @param {int} version New version after this bundle
+ * @param {Object} blipsums Checksums to compare the wavelet to
*/
- _processOperations: function (wavelet, serial_ops) {
+ _processMessageBundle: function (wavelet, serial_ops, version, blipsums) {
var mpending = this.wavelets[wavelet.id()].mpending;
var mcached = this.wavelets[wavelet.id()].mcached;
- var delta = new pygowave.operations.OpManager(wavelet.waveId(), wavelet.id());
- delta.unserialize(serial_ops);
-
- var ops = new Array();
- // Iterate over all operations
- for (var incoming = new _Iterator(delta.operations); incoming.hasNext(); ) {
- // Transform pending operations, iterate over results
- for (var tr = new _Iterator(mpending.transform(incoming.next())); tr.hasNext(); ) {
- // Transform cached operations, save results
- ops.extend(mcached.transform(tr.next()));
+ if (serial_ops != "ACK") {
+ var delta = new pygowave.operations.OpManager(wavelet.waveId(), wavelet.id());
+ delta.unserialize(serial_ops);
+
+ var ops = new Array();
+
+ // Iterate over all operations
+ for (var incoming = new _Iterator(delta.operations); incoming.hasNext(); ) {
+ // Transform pending operations, iterate over results
+ for (var tr = new _Iterator(mpending.transform(incoming.next())); tr.hasNext(); ) {
+ // Transform cached operations, save results
+ ops.extend(mcached.transform(tr.next()));
+ }
}
+
+ // Apply operations
+ wavelet.applyOperations(ops);
+
+ // Set version and checkup
+ wavelet.options.version = version;
+ if (!this.hasPendingOperations(wavelet.id()))
+ wavelet.checkSync(blipsums);
+ }
+ else { // ACK message
+ wavelet.options.version = version;
+ mpending.fetch(); // Clear
+ if (!mcached.isEmpty())
+ this._transferOperations(wavelet.id()); // Send cached
+ else {
+ // All done, we can do a check-up
+ wavelet.checkSync(blipsums);
+ this.wavelets[wavelet.id()].pending = false;
+ }
+ }
+ },
+ /**
+ * Callback from view if it goes ready.
+ *
+ * @function {private} _onViewReady
+ */
+ _onViewReady: function () {
+ if (this._deferredMessageBundles.length > 0 && !this._processingDeferred) {
+ this._processingDeferred = true;
+ for (var it = new _Iterator(this._deferredMessageBundles); it.hasNext(); ) {
+ var bundle = it.next();
+ this._processMessageBundle(bundle.wavelet, bundle.serial_ops, bundle.version, bundle.blipsums);
+ }
+ this._deferredMessageBundles.empty();
+ this._processingDeferred = false;
}
-
- // Apply operations
- wavelet.applyOperations(ops);
},
/**
View
95 pygowave_client/src/view/blip_editor.js
@@ -63,9 +63,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
initialize: function (view, blip, parentElement, where) {
this._view = view;
this._blip = blip;
- this._lastRange = [0, 0];
+ this._lastRange = null;
+ this._lastValidRange = null;
this._lastContent = "";
this._errDiv = null;
+ this._firstKeyPress = false;
var contentElement = new Element('div', {
'class': 'blip_editor_widget',
@@ -130,7 +132,6 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
mousedown: this._onMouseDown
})
- this._processing = false;
if (ok)
this._onSyncCheckTimer = this._onSyncCheck.periodical(2000, this);
@@ -146,7 +147,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
dispose: function () {
this.parent();
- this._lastRange = [0, 0];
+ this._lastRange = null;
this._lastContent = "";
this._blip.removeEvents({
insertText: this._onInsertText,
@@ -323,12 +324,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* Returns the current selected text range as [start, end]. This
* ignores all element nodes except gadget elements (which is treated as 1 character).
* @function {public Array} currentTextRange
- * @param {optional pygowave.view.Selection} sel Selection object
- * (will be fetched if not provided).
+ * @param {optional Element} target Target from the event object to check
+ * against the scope
*/
- currentTextRange: function (sel) {
- if (!$defined(sel))
- sel = Selection.currentSelection(this.contentElement);
+ currentTextRange: function (target) {
+ var sel = Selection.currentSelection(this.contentElement, target);
if (!sel.isValid()) {
this.fireEvent("currentTextRangeChanged", [null, null]);
@@ -346,10 +346,10 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* Returns the last text range which was succesfully retrieved
* as [start, end].
*
- * @function {public Array} lastTextRange
+ * @function {public Array} lastValidTextRange
*/
- lastTextRange: function () {
- return this._lastRange;
+ lastValidTextRange: function () {
+ return this._lastValidRange;
},
/**
* Check if at a line start and insert a newline if not.
@@ -363,7 +363,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
var text = this.contentToString();
if (index > 0 && text.substr(index-1, 1) != "\n") {
this._onInsertText(index, "\n");
- this._processing = true;
+ this._view.setBusy();
this._view.fireEvent(
'textInserted',
[
@@ -373,7 +373,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
"\n"
]
);
- this._processing = false;
+ this._view.unsetBusy();
return false;
}
return true;
@@ -441,24 +441,33 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
},
_onKeyDown: function (e) {
- this._processing = true;
+ this._firstKeyPress = true;
+ this._view.setBusy();
},
_onKeyPress: function (e) {
- this._processing = true;
+ if (Browser.Engine.presto && !this._firstKeyPress)
+ this._processKey(e);
+ this._firstKeyPress = false;
+ },
+
+ _onKeyUp: function(e) {
+ this._processKey(e);
+ this._view.unsetBusy();
},
/**
- * Callback from underlying DOM element on key release. Generates events
- * for the controller (which in turn generates operations).
+ * Callback from underlying DOM element on key release/press. Generates
+ * events for the controller (which in turn generates operations).
*
- * @function {private} _onKeyUp
+ * @function {private} _processKey
* @param {Object} e Event object
*/
- _onKeyUp: function(e) {
- this._processing = true;
+ _processKey: function(e) {
+ var newRange = this.currentTextRange(e.target);
+ if (!$defined(newRange))
+ return;
var newContent = this.contentToString();
- var newRange = this.currentTextRange();
// Notes: Does some checks before acting; the context menu is not handeled properly.
if (newContent.length != this._lastContent.length || newContent != this._lastContent) {
@@ -488,13 +497,15 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._lastContent = newContent;
this._lastRange = newRange;
- this._processing = false;
+ this._lastValidRange = newRange;
},
_onMouseDown: function (e) {
// Currently not needed
},
_onMouseUp: function(e) {
- this._lastRange = this.currentTextRange();
+ this._lastRange = this.currentTextRange(e.target);
+ if ($defined(this._lastRange))
+ this._lastValidRange = this._lastRange;
},
_onContextMenu: function (e) {
return false; // Context menu is blocked
@@ -549,7 +560,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
_onInsertText: function (index, text) {
//TODO: this function assumes no formatting elements
- this._processing = true;
+ this._view.setBusy();
text = text.replace(/ /g, "\u00a0\u00a0").replace(/^ | $/g, "\u00a0"); // Convert spaces to protected spaces
var length = text.length;
@@ -618,10 +629,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
}
}
- if (index < this._lastRange[0])
+ if ($defined(this._lastRange) && index < this._lastRange[0]) {
this._lastRange = this.currentTextRange();
+ if ($defined(this._lastRange))
+ this._lastValidRange = this._lastRange;
+ }
- this._processing = false;
+ this._view.unsetBusy();
},
/**
* Callback from model on text deletion
@@ -632,7 +646,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
*/
_onDeleteText: function (index, length) {
// Safe for formatting elements
- this._processing = true;
+ this._view.setBusy();
var rlength = length; // Remaining length
var ret = this._walkDown(this.contentElement, index);
@@ -669,10 +683,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
offset = 0;
}
- if (index < this._lastRange[0])
+ if ($defined(this._lastRange) && index < this._lastRange[0]) {
this._lastRange = this.currentTextRange();
+ if ($defined(this._lastRange))
+ this._lastValidRange = this._lastRange;
+ }
- this._processing = false;
+ this._view.unsetBusy();
},
/**
* Callback from model if an element was inserted.
@@ -680,7 +697,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* @param {int} index Offset where the element is inserted
*/
_onInsertElement: function (index) {
- this._processing = true;
+ this._view.setBusy();
var elt = this._blip.elementAt(index);
@@ -696,10 +713,13 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this._elements.push(ew);
- if (index < this._lastRange[0])
+ if ($defined(this._lastRange) && index < this._lastRange[0]) {
this._lastRange = this.currentTextRange();
+ if ($defined(this._lastRange))
+ this._lastValidRange = this._lastRange;
+ }
- this._processing = false;
+ this._view.unsetBusy();
},
/**
* Callback from model if an element was deleted.
@@ -707,14 +727,17 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* @param {int} index Offset where the element is deleted
*/
_onDeleteElement: function (index) {
- this._processing = true;
+ this._view.setBusy();
this.deleteElementWidgetAt(index, true);
- if (index < this._lastRange[0])
+ if ($defined(this._lastRange) && index < this._lastRange[0]) {
this._lastRange = this.currentTextRange();
+ if ($defined(this._lastRange))
+ this._lastValidRange = this._lastRange;
+ }
- this._processing = false;
+ this._view.unsetBusy();
},
/**
* Callback from model if blip has gone out of sync with the server
@@ -889,7 +912,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* @function {private} _onSyncCheck
*/
_onSyncCheck: function () {
- if (this._processing)
+ if (this._view.isBusy())
return;
var content = this.contentToString();
View
2 pygowave_client/src/view/gadgets.js
@@ -227,7 +227,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
contentElement.contentEditable = "false";
this._deleteBox = new Element('div', {
- 'class': 'delete_box',
+ 'class': 'delete_box' + (Browser.Engine.presto ? '_opera' : ''),
'title': gettext("Delete Gadget")
}).inject(contentElement);
this._deleteBox.addEvent('click', function () {
View
20 pygowave_client/src/view/selection.js
@@ -368,10 +368,12 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* @function {static public Selection} currentSelection
* @param {optional Element} scope Limit the scope to this element, i.e.
* return an invalid selection if it is not inside this element.
+ * @param {optional Element} target Target from the event object to check
+ * against the scope
* @param {optional Document} ownerDocument Owner document for the selection.
* Defaults to the current window's document.
*/
- Selection.currentSelection = function (scope, ownerDocument) {
+ Selection.currentSelection = function (scope, target, ownerDocument) {
if (!$defined(ownerDocument))
ownerDocument = window.document;
if (!$defined(scope))
@@ -388,8 +390,8 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
if (Browser.Engine.trident)
return Selection.tridentFindRange(ownerDocument, scope, rng);
else {
- // Check scope
if (scope != ownerDocument.body) {
+ // Check selection scope
var elt = rng.startContainer;
while ($type(elt) != "element" || (elt != scope && elt.get('tag') != "html"))
elt = elt.parentNode;
@@ -400,6 +402,20 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
elt = elt.parentNode;
if (elt != scope)
return new Selection();
+
+ // Also check target
+ if ($defined(target)) {
+ var elt = target;
+ while ($type(elt) != "element" || (elt != scope && elt.get('tag') != "html"))
+ elt = elt.parentNode;
+ if (elt != scope)
+ return new Selection();
+ elt = rng.endContainer;
+ while ($type(elt) != "element" || (elt != scope && elt.get('tag') != "html"))
+ elt = elt.parentNode;
+ if (elt != scope)
+ return new Selection();
+ }
}
return new Selection(
rng.startContainer,
View
38 pygowave_client/src/view/view.js
@@ -341,6 +341,11 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
* @param {String} waveletId ID of the Wavelet
* @param {Boolean} forced True if the user explicitly clicked refresh
*/
+
+ /**
+ * Fired if the view was busy and now is ready to receive modification.
+ * @event onReady
+ */
// ---------------------------
/**
@@ -361,6 +366,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
this.model = model;
this.container = container;
this.waveletWidgets = new Hash();
+ this._busyCounter = 0;
var rwv = this.model.rootWavelet();
if (rwv != null) {
@@ -597,7 +603,7 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
);
return false;
}
- var textrange = editor.lastTextRange();
+ var textrange = editor.lastValidTextRange();
if (!$defined(textrange)) {
this.showMessage(
gettext("You must place your cursor inside the text to mark the Gadget's insertion point."),
@@ -620,6 +626,36 @@ pygowave.view = $defined(pygowave.view) ? pygowave.view : new Hash();
{"url": url}
]);
return true;
+ },
+
+ /**
+ * Returns weather the view is busy processing one or more user
+ * interactions.
+ *
+ * @function {public Boolean} isBusy
+ */
+ isBusy: function () {
+ return (this._busyCounter != 0);
+ },
+ /**
+ * Set the view busy. Called by interactive widgets to block controller
+ * events.
+ * @function {public} setBusy
+ */
+ setBusy: function () {
+ this._busyCounter++;
+ },
+ /**
+ * Unset the view busy. Called by interactive widgets to free controller
+ * events. May fire a ready event.
+ * @function {public} unsetBusy
+ */
+ unsetBusy: function () {
+ this._busyCounter--;
+ if (this._busyCounter < 0)
+ this._busyCounter = 0;
+ if (this._busyCounter == 0)
+ this.fireEvent('ready');
}
});

0 comments on commit 7c3d36c

Please sign in to comment.
Something went wrong with that request. Please try again.