jenkins-bot has submitted this change and it was merged.

Change subject: Enslave dm.Surface to dm.Document
......................................................................


Enslave dm.Surface to dm.Document

* Replace surface 'transact' event with 'documentUpdate' event
* Have surface listen for all document transactions and update selection
  as appropriate (as well as emitting 'documentUpdate')
* Implement change() in terms of setSelection()
** Queue 'contextChange' events so contextChange is only emitted once
** Use this.transacting flag to prevent setSelection() (which is called
   because the model emits transact events) from doing too much
** Behavioral change: lock/unlock now emitted separately for
   transaction and selection changes

Change-Id: I44425d260ec70758f99d13f99e41f5c993d260c2
---
M modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
M modules/ve/dm/ve.dm.Surface.js
M modules/ve/test/dm/ve.dm.Surface.test.js
3 files changed, 201 insertions(+), 80 deletions(-)

Approvals:
  Divec: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js 
b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
index 736cb9b..7f483c4 100644
--- a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
+++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
@@ -708,10 +708,10 @@
  * looks like wikitext, so it can trigger if the user types in a paragraph 
that has pre-existing
  * wikitext-like content.
  *
- * This method is bound to the 'transact' event on the surface model, and 
unbinds itself when
+ * This method is bound to the 'documentUpdate' event on the surface model, 
and unbinds itself when
  * the wikitext notification is displayed.
  *
- * @param {ve.dm.Transaction[]} transactions
+ * @param {ve.dm.Transaction} transaction
  */
 ve.init.mw.ViewPageTarget.prototype.checkForWikitextWarning = function () {
        var text, doc = this.surface.getView().getDocument(),
@@ -733,7 +733,7 @@
                        }
                );
                this.surface.getModel().disconnect(
-                       this, { 'transact': 'checkForWikitextWarning' }
+                       this, { 'documentUpdate': 'checkForWikitextWarning' }
                );
        }
 };
@@ -970,7 +970,7 @@
                                                'transact': 
'clearSaveDialogDiff'
                                        } );
                                        target.surface.getModel().connect( 
target, {
-                                               'transact': 
'checkForWikitextWarning',
+                                               'documentUpdate': 
'checkForWikitextWarning',
                                                'history': 
'updateToolbarSaveButtonState'
                                        } );
                                        target.$.append( target.surface.$ );
diff --git a/modules/ve/dm/ve.dm.Surface.js b/modules/ve/dm/ve.dm.Surface.js
index 9ad1095..e7d912b 100644
--- a/modules/ve/dm/ve.dm.Surface.js
+++ b/modules/ve/dm/ve.dm.Surface.js
@@ -29,6 +29,12 @@
        this.historyTrackingInterval = null;
        this.insertionAnnotations = new ve.dm.AnnotationSet( 
this.documentModel.getStore() );
        this.enabled = true;
+       this.transacting = false;
+       this.queueingContextChanges = false;
+       this.contextChangeQueued = false;
+
+       // Events
+       this.documentModel.connect( this, { 'transact': 'onDocumentTransact' } 
);
 };
 
 /* Inheritance */
@@ -53,6 +59,16 @@
 /**
  * @event transact
  * @param {ve.dm.Transaction[]} transactions Transactions that have just been 
processed
+ */
+
+/**
+ * @event documentUpdate
+ *
+ * Emitted when a transaction has been processed on the document and the 
selection has been
+ * translated to account for that transaction. You should only use this event 
if you need
+ * to access the selection; in most cases, you should use 
{ve.dm.Document#event-transact}.
+ *
+ * @param {ve.dm.Transaction} tx Transaction that was processed on the document
  */
 
 /**
@@ -289,84 +305,107 @@
 };
 
 /**
- * Apply a transactions and selection changes to the document.
+ * Start queueing up calls to {#emitContextChange} until 
{#stopQueueingContextChanges} is called.
+ * While queueing is active, contextChanges are also collapsed, so if 
{#emitContextChange} is called
+ * multiple times, only one contextChange event will be emitted by 
{#stopQueueingContextChanges}.
+ *
+ *     @example
+ *     this.emitContextChange(); // emits immediately
+ *     this.startQueueingContextChanges();
+ *     this.emitContextChange(); // doesn't emit
+ *     this.emitContextChange(); // doesn't emit
+ *     this.stopQueueingContextChanges(); // emits one contextChange event
  *
  * @method
- * @param {ve.dm.Transaction|ve.dm.Transaction[]|null} transactions One or 
more transactions to
- *  process, or null to process none
- * @param {ve.Range|undefined} selection
- * @fires lock
- * @fires select
- * @fires transact
- * @fires contextChange
- * @fires unlock
+ * @private
  */
-ve.dm.Surface.prototype.change = function ( transactions, selection ) {
-       if ( !this.enabled ) {
-               return;
+ve.dm.Surface.prototype.startQueueingContextChanges = function () {
+       if ( !this.queueingContextChanges ) {
+               this.queueingContextChanges = true;
+               this.contextChangeQueued = false;
        }
-       var i, len, left, right, leftAnnotations, rightAnnotations, 
insertionAnnotations,
+};
+
+/**
+ * Emit a contextChange event. If {#startQueueingContextChanges} has been 
called, then the event
+ * is deferred until {#stopQueueingContextChanges} is called.
+ *
+ * @method
+ * @private
+ * @fires contextChange
+ */
+ve.dm.Surface.prototype.emitContextChange = function () {
+       if ( this.queueingContextChanges ) {
+               this.contextChangeQueued = true;
+       } else {
+               this.emit( 'contextChange' );
+       }
+};
+
+/**
+ * Stop queueing contextChange events. If {#emitContextChange} was called 
previously, a contextChange
+ * event will now be emitted. Any future calls to {#emitContextChange} will 
once again emit the
+ * event immediately.
+ *
+ * @method
+ * @private
+ * @fires contextChange
+ */
+ve.dm.Surface.prototype.stopQueueingContextChanges = function () {
+       if ( this.queueingContextChanges ) {
+               this.queueingContextChanges = false;
+               if ( this.contextChangeQueued ) {
+                       this.contextChangeQueued = false;
+                       this.emit( 'contextChange' );
+               }
+       }
+};
+
+/**
+ * Change the selection
+ *
+ * @param {ve.Range} selection New selection
+ *
+ * @fires lock
+ * @fires unlock
+ * @fires select
+ * @fires contextChange
+ */
+ve.dm.Surface.prototype.setSelection = function ( selection ) {
+       var left, right, leftAnnotations, rightAnnotations, 
insertionAnnotations,
                selectedNodes = {},
                oldSelection = this.selection,
                contextChange = false,
                dataModelData = this.documentModel.data;
 
+       if ( !this.enabled ) {
+               return;
+       }
+
+       if ( this.transacting ) {
+               // Update the selection but don't do any processing
+               this.selection = selection;
+               return;
+       }
+
        // Stop observation polling, things changing right now are known already
        this.emit( 'lock' );
 
-       // Process transactions and apply selection changes
-       if ( transactions ) {
-               if ( transactions instanceof ve.dm.Transaction ) {
-                       transactions = [transactions];
-               }
-               for ( i = 0, len = transactions.length; i < len; i++ ) {
-                       if ( !transactions[i].isNoOp() ) {
-                               this.truncateUndoStack();
-                               this.smallStack.push( transactions[i] );
-                               this.documentModel.commit( transactions[i] );
-                               if ( !selection ) {
-                                       // translateRange only if selection is 
not provided because otherwise we are
-                                       // going to overwrite it
-                                       this.selection = 
transactions[i].translateRange( this.selection );
-                               }
-                       }
-               }
+       // Detect if selected nodes changed
+       selectedNodes.start = this.documentModel.getNodeFromOffset( 
selection.start );
+       if ( selection.getLength() ) {
+               selectedNodes.end = this.documentModel.getNodeFromOffset( 
selection.end );
+       }
+       if (
+               selectedNodes.start !== this.selectedNodes.start ||
+               selectedNodes.end !== this.selectedNodes.end
+       ) {
+               contextChange = true;
        }
 
-       if ( selection ) {
-               // Detect if selected nodes changed
-               selectedNodes.start = this.documentModel.getNodeFromOffset( 
selection.start );
-               if ( selection.getLength() ) {
-                       selectedNodes.end = 
this.documentModel.getNodeFromOffset( selection.end );
-               }
-               if (
-                       selectedNodes.start !== this.selectedNodes.start ||
-                       selectedNodes.end !== this.selectedNodes.end
-               ) {
-                       contextChange = true;
-               }
-               this.selectedNodes = selectedNodes;
-               this.selection = selection;
-       }
-
-       // Emit select event if selection range changed
-       if ( !oldSelection || !oldSelection.equals( this.selection ) ) {
-               this.emit( 'select', this.selection.clone() );
-       }
-
-       // Only emit a transact event if transactions were actually processed
-       if ( transactions ) {
-               this.emit( 'transact', transactions );
-               // Detect context change, if not detected already, when element 
attributes have changed
-               if ( !contextChange ) {
-                       for ( i = 0, len = transactions.length; i < len; i++ ) {
-                               if ( 
transactions[i].hasElementAttributeOperations() ) {
-                                       contextChange = true;
-                                       break;
-                               }
-                       }
-               }
-       }
+       // Update state
+       this.selectedNodes = selectedNodes;
+       this.selection = selection;
 
        // Figure out which annotations to use for insertions
        if ( this.selection.isCollapsed() ) {
@@ -401,13 +440,81 @@
                contextChange = true;
        }
 
-       // Only emit one context change event
+       // Emit events
+       if ( !oldSelection || !oldSelection.equals( this.selection ) ) {
+               this.emit( 'select', this.selection.clone() );
+       }
        if ( contextChange ) {
-               this.emit( 'contextChange' );
+               this.emitContextChange();
        }
 
        // Continue observation polling, we want to know about things that 
change from here on out
        this.emit( 'unlock' );
+};
+
+/**
+ * Apply a transactions and selection changes to the document.
+ *
+ * @method
+ * @param {ve.dm.Transaction|ve.dm.Transaction[]|null} transactions One or 
more transactions to
+ *  process, or null to process none
+ * @param {ve.Range|undefined} selection
+ * @fires lock
+ * @fires contextChange
+ * @fires unlock
+ */
+ve.dm.Surface.prototype.change = function ( transactions, selection ) {
+       var i, len, selectionAfter, selectionBefore = this.selection, 
contextChange = false;
+
+       if ( !this.enabled ) {
+               return;
+       }
+
+       this.startQueueingContextChanges();
+
+       // Process transactions
+       if ( transactions ) {
+               if ( transactions instanceof ve.dm.Transaction ) {
+                       transactions = [transactions];
+               }
+               this.transacting = true;
+               this.emit( 'lock' );
+               for ( i = 0, len = transactions.length; i < len; i++ ) {
+                       if ( !transactions[i].isNoOp() ) {
+                               this.truncateUndoStack();
+                               this.smallStack.push( transactions[i] );
+                               // The .commit() call below indirectly invokes 
setSelection()
+                               this.documentModel.commit( transactions[i] );
+                               if ( 
transactions[i].hasElementAttributeOperations() ) {
+                                       contextChange = true;
+                               }
+                       }
+               }
+               this.emit( 'unlock' );
+               this.transacting = false;
+       }
+       selectionAfter = this.selection;
+
+       // Apply selection change
+       if ( selection ) {
+               this.setSelection( selection );
+       } else if ( transactions ) {
+               // Call setSelection() to trigger selection processing that was 
bypassed earlier
+               this.setSelection( this.selection );
+       }
+
+       // If the selection changed while applying the transactions but not 
while applying the
+       // selection change, setSelection() won't have emitted a 'select' 
event. We don't want that
+       // to happen, so emit one anyway.
+       if ( !selectionBefore.equals( selectionAfter ) && 
selectionAfter.equals( this.selection ) ) {
+               this.emit( 'select', this.selection.clone() );
+       }
+
+       if ( contextChange ) {
+               this.emitContextChange();
+       }
+
+       this.stopQueueingContextChanges();
 };
 
 /**
@@ -499,3 +606,15 @@
        }
        return null;
 };
+
+/**
+ * Respond to transactions processed on the document by translating the 
selection and updating
+ * other state.
+ *
+ * @param {ve.dm.Transaction} tx Transaction that was processed
+ * @fires documentUpdate
+ */
+ve.dm.Surface.prototype.onDocumentTransact = function ( tx ) {
+       this.setSelection( tx.translateRange( this.selection ) );
+       this.emit( 'documentUpdate', tx );
+};
diff --git a/modules/ve/test/dm/ve.dm.Surface.test.js 
b/modules/ve/test/dm/ve.dm.Surface.test.js
index bd99959..bd0df64 100644
--- a/modules/ve/test/dm/ve.dm.Surface.test.js
+++ b/modules/ve/test/dm/ve.dm.Surface.test.js
@@ -33,25 +33,27 @@
 } );
 
 QUnit.test( 'change', 3, function ( assert ) {
-       var surface = new ve.dm.SurfaceStub(),
-               tx = new ve.dm.Transaction(),
+       var tx, surface = new ve.dm.SurfaceStub(),
                events = {
-                       'transact': 0,
+                       'documentUpdate': 0,
                        'select': 0
                };
 
-       surface.on( 'transact', function () {
-               events.transact++;
+       // docmentUpdate doesn't fire for no-op transactions, so make sure 
there's something there
+       tx = ve.dm.Transaction.newFromInsertion( surface.getDocument(), 3, [ 
'i' ] );
+
+       surface.on( 'documentUpdate', function () {
+               events.documentUpdate++;
        } );
        surface.on( 'select', function () {
                events.select++;
        } );
-       surface.change( tx );
-       assert.deepEqual( events, { 'transact': 1, 'select': 0 }, 'transaction 
without selection' );
+       surface.change( tx.clone() );
+       assert.deepEqual( events, { 'documentUpdate': 1, 'select': 0 }, 
'transaction without selection' );
        surface.change( null, new ve.Range( 2, 2 ) );
-       assert.deepEqual( events, { 'transact': 1, 'select': 1 }, 'selection 
without transaction' );
-       surface.change( tx, new ve.Range( 3, 3 ) );
-       assert.deepEqual( events, { 'transact': 2, 'select': 2 }, 'transaction 
and selection' );
+       assert.deepEqual( events, { 'documentUpdate': 1, 'select': 1 }, 
'selection without transaction' );
+       surface.change( tx.clone(), new ve.Range( 3, 3 ) );
+       assert.deepEqual( events, { 'documentUpdate': 2, 'select': 2 }, 
'transaction and selection' );
 } );
 
 QUnit.test( 'breakpoint', 7, function ( assert ) {

-- 
To view, visit https://gerrit.wikimedia.org/r/88485
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I44425d260ec70758f99d13f99e41f5c993d260c2
Gerrit-PatchSet: 15
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Catrope <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to