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

Change subject: Store DM nodes in InternalList
......................................................................


Store DM nodes in InternalList

Also keep items in the order they appear in the document
and grouped by group and key.

Additions and removals are triggered by the new root/unroot events.

Change-Id: Ia3e90ccfdab88f352b89992b90554e5f03ff9952
---
M modules/ve/ce/nodes/ve.ce.DocumentNode.js
M modules/ve/dm/nodes/ve.dm.DocumentNode.js
M modules/ve/dm/nodes/ve.dm.MWReferenceNode.js
M modules/ve/dm/ve.dm.BranchNode.js
M modules/ve/dm/ve.dm.Document.js
M modules/ve/dm/ve.dm.InternalList.js
M modules/ve/test/dm/ve.dm.InternalList.test.js
M modules/ve/test/dm/ve.dm.Node.test.js
M modules/ve/test/dm/ve.dm.example.js
M modules/ve/test/ve.Node.test.js
M modules/ve/test/ve.qunit.js
M modules/ve/ve.Node.js
12 files changed, 304 insertions(+), 50 deletions(-)

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



diff --git a/modules/ve/ce/nodes/ve.ce.DocumentNode.js 
b/modules/ve/ce/nodes/ve.ce.DocumentNode.js
index 07adfa3..4c204cf 100644
--- a/modules/ve/ce/nodes/ve.ce.DocumentNode.js
+++ b/modules/ve/ce/nodes/ve.ce.DocumentNode.js
@@ -12,7 +12,7 @@
  * @extends ve.ce.BranchNode
  * @constructor
  * @param {ve.dm.DocumentNode} model Model to observe
- * @param {ve.ui.Surface} surface Surface document is part of
+ * @param {ve.ce.Surface} surface Surface document is part of
  * @param {Object} [config] Config options
  */
 ve.ce.DocumentNode = function VeCeDocumentNode( model, surface, config ) {
@@ -22,6 +22,9 @@
        // Properties
        this.surface = surface;
 
+       // Set root
+       this.setRoot( this );
+
        // DOM Changes
        this.$.addClass( 've-ce-documentNode' );
        this.$.attr( { 'contentEditable': 'true', 'spellcheck': 'true' } );
diff --git a/modules/ve/dm/nodes/ve.dm.DocumentNode.js 
b/modules/ve/dm/nodes/ve.dm.DocumentNode.js
index 367cf75..43b54d4 100644
--- a/modules/ve/dm/nodes/ve.dm.DocumentNode.js
+++ b/modules/ve/dm/nodes/ve.dm.DocumentNode.js
@@ -16,6 +16,9 @@
 ve.dm.DocumentNode = function VeDmDocumentNode( children ) {
        // Parent constructor
        ve.dm.BranchNode.call( this, children );
+
+       // Properties
+       this.root = this;
 };
 
 /* Inheritance */
diff --git a/modules/ve/dm/nodes/ve.dm.MWReferenceNode.js 
b/modules/ve/dm/nodes/ve.dm.MWReferenceNode.js
index bda162c..fe02097 100644
--- a/modules/ve/dm/nodes/ve.dm.MWReferenceNode.js
+++ b/modules/ve/dm/nodes/ve.dm.MWReferenceNode.js
@@ -17,6 +17,12 @@
 ve.dm.MWReferenceNode = function VeDmMWReferenceNode( length, element ) {
        // Parent constructor
        ve.dm.LeafNode.call( this, 0, element );
+
+       // Event handlers
+       this.connect( this, {
+               'root': 'onRoot',
+               'unroot': 'onUnroot'
+       } );
 };
 
 /* Inheritance */
@@ -41,16 +47,19 @@
                // TODO: this should use mw.attrs.name once available from 
Parsoid
                name = $( domElements[0] ).children( 'a' ).attr( 'href' ),
                // TODO: should also store and use mw.attrs.group once 
available from Parsoid
-               key = name !== null ? name : ve.getHash( body );
+               listGroup = this.name, // + '/' + mw.attrs.group
+               listKey = name !== null ? name : ve.getHash( body );
 
-       listIndex = converter.internalList.queueItemHtml( key, body );
+       listIndex = converter.internalList.queueItemHtml( listGroup, listKey, 
body );
 
        dataElement = {
                'type': this.name,
                'attributes': {
                        'mw': mw,
                        'about': about,
-                       'listIndex': listIndex
+                       'listIndex': listIndex,
+                       'listGroup': listGroup,
+                       'listKey': listKey
                }
        };
        return dataElement;
@@ -91,7 +100,29 @@
  * @returns {ve.dm.InternalItemNode} Item node
  */
 ve.dm.MWReferenceNode.prototype.getInternalItem = function () {
-       return this.doc.getInternalList().getItemNode( this.getAttribute( 
'listIndex' ) );
+       return this.getDocument().getInternalList().getItemNode( 
this.getAttribute( 'listIndex' ) );
+};
+
+/**
+ * Handle the node being attached to the root
+ * @method
+ */
+ve.dm.MWReferenceNode.prototype.onRoot = function () {
+       if ( this.getRoot() === this.getDocument().getDocumentNode() ) {
+               this.getDocument().getInternalList().addNode(
+                       this.element.attributes.listGroup, 
this.element.attributes.listKey, this
+               );
+       }
+};
+
+/**
+ * Handle the node being detatched from the root
+ * @method
+ */
+ve.dm.MWReferenceNode.prototype.onUnroot = function () {
+       this.getDocument().getInternalList().removeNode(
+               this.element.attributes.listGroup, 
this.element.attributes.listKey, this
+       );
 };
 
 /* Registration */
diff --git a/modules/ve/dm/ve.dm.BranchNode.js 
b/modules/ve/dm/ve.dm.BranchNode.js
index 8ef3fc9..a892bef 100644
--- a/modules/ve/dm/ve.dm.BranchNode.js
+++ b/modules/ve/dm/ve.dm.BranchNode.js
@@ -126,18 +126,18 @@
                args = Array.prototype.slice.call( arguments ),
                diff = 0;
 
+       removals = this.children.splice.apply( this.children, args );
+       for ( i = 0, length = removals.length; i < length; i++ ) {
+               removals[i].detach();
+               diff -= removals[i].getOuterLength();
+       }
+
        if ( args.length >= 3 ) {
                length = args.length;
                for ( i = 2; i < length; i++ ) {
                        args[i].attach( this );
                        diff += args[i].getOuterLength();
                }
-       }
-
-       removals = this.children.splice.apply( this.children, args );
-       for ( i = 0, length = removals.length; i < length; i++ ) {
-               removals[i].detach();
-               diff -= removals[i].getOuterLength();
        }
 
        this.adjustLength( diff, true );
diff --git a/modules/ve/dm/ve.dm.Document.js b/modules/ve/dm/ve.dm.Document.js
index b36d3ed..9443022 100644
--- a/modules/ve/dm/ve.dm.Document.js
+++ b/modules/ve/dm/ve.dm.Document.js
@@ -32,7 +32,7 @@
         */
        var i, node, children, meta,
                doc = parentDocument || this,
-               root = doc.getDocumentNode(),
+               root = this.getDocumentNode(),
                textLength = 0,
                inTextNode = false,
                // Stack of stacks, each containing a
@@ -40,8 +40,8 @@
                currentStack = stack[1],
                parentStack = stack[0],
                currentNode = this.documentNode;
-       this.documentNode.setDocument( doc );
        this.documentNode.setRoot( root );
+       this.documentNode.setDocument( doc );
        this.internalList = internalList ? internalList.clone( this ) : new 
ve.dm.InternalList( this );
 
        // Properties
@@ -75,8 +75,7 @@
                        if ( !inTextNode ) {
                                // Create a lengthless text node
                                node = new ve.dm.TextNode();
-                               // Set the root pointer now, to prevent 
cascading updates
-                               node.setRoot( root );
+                               node.setDocument( doc );
                                // Put the node on the current inner stack
                                currentStack.push( node );
                                currentNode = node;
@@ -120,8 +119,7 @@
                                node = ve.dm.nodeFactory.create(
                                        this.data.getData( i ).type, [], 
this.data.getData( i )
                                );
-                               // Set the root pointer now, to prevent 
cascading updates
-                               node.setRoot( root );
+                               node.setDocument( doc );
                                // Put the childless node on the current inner 
stack
                                currentStack.push( node );
                                if ( ve.dm.nodeFactory.canNodeHaveChildren( 
node.getType() ) ) {
@@ -162,9 +160,16 @@
                currentNode.setLength( textLength );
                // Don't bother updating currentNode et al, we don't use them 
below
        }
+
+       // State variable that allows nodes to know that they are being
+       // appended in order. Used by ve.dm.InternalList.
+       this.buildingNodeTree = true;
+
        // The end state is stack = [ [this.documentNode] [ array, of, its, 
children ] ]
        // so attach all nodes in stack[1] to the root node
        ve.batchSplice( this.documentNode, 0, 0, stack[1] );
+
+       this.buildingNodeTree = false;
 };
 
 /* Inheritance */
diff --git a/modules/ve/dm/ve.dm.InternalList.js 
b/modules/ve/dm/ve.dm.InternalList.js
index 6e332a0..d5dfaed 100644
--- a/modules/ve/dm/ve.dm.InternalList.js
+++ b/modules/ve/dm/ve.dm.InternalList.js
@@ -21,11 +21,12 @@
        // Properties
        this.document = doc;
        this.store = new ve.dm.IndexValueStore();
-       this.itemsHtml = [];
+       this.itemHtmlQueue = [];
        this.listNode = null;
+       this.nodes = {};
 
        // Event handlers
-       //this.document.connect( this, { 'transact', 'onTransact' } );
+       // this.getDocument().connect( this, { 'transact': 'onTransact' } );
 };
 
 /* Inheritance */
@@ -37,18 +38,19 @@
 /**
  * Queues up an item's html for parsing later.
  *
- * If an item with the specified key already exists it will be ignored.
+ * If an item with the specified group and key already exists it will be 
ignored.
  *
  * @method
+ * @param {string} groupName Item group
  * @param {string} key Item key
  * @param {string} html Item contents
  * @returns {number} Index of the item in the index-value store, and also the 
list
  */
-ve.dm.InternalList.prototype.queueItemHtml = function ( key, html ) {
-       var index = this.getStore().indexOfHash( key );
+ve.dm.InternalList.prototype.queueItemHtml = function ( groupName, key, html ) 
{
+       var index = this.getStore().indexOfHash( groupName + '/' + key );
        if ( index === null ) {
-               index = this.getStore().index( html, key );
-               this.itemsHtml.push( index );
+               index = this.getStore().index( html, groupName + '/' + key );
+               this.itemHtmlQueue.push( index );
        }
        return index;
 };
@@ -58,8 +60,8 @@
  * @method
  * @returns {Object} Name-indexed object containing HTMLElements
  */
-ve.dm.InternalList.prototype.getItemsHtml = function () {
-       return this.getStore().values( this.itemsHtml );
+ve.dm.InternalList.prototype.getItemHtmlQueue = function () {
+       return this.getStore().values( this.itemHtmlQueue );
 };
 
 /**
@@ -124,12 +126,12 @@
  */
 ve.dm.InternalList.prototype.convertToData = function ( converter ) {
        var i, length, itemData,
-               itemsHtml = this.getItemsHtml(), list = [];
+               itemHtmlQueue = this.getItemHtmlQueue(), list = [];
 
-       if ( itemsHtml.length ) {
+       if ( itemHtmlQueue.length ) {
                list.push( { 'type': 'internalList' } );
-               for ( i = 0, length = itemsHtml.length; i < length; i++ ) {
-                       itemData = converter.getDataFromDomRecursion( $( 
'<div>' ).html( itemsHtml[i] )[0] );
+               for ( i = 0, length = itemHtmlQueue.length; i < length; i++ ) {
+                       itemData = converter.getDataFromDomRecursion( $( 
'<div>' ).html( itemHtmlQueue[i] )[0] );
                        list = list.concat(
                                [{ 'type': 'internalItem' }],
                                itemData,
@@ -139,8 +141,94 @@
                list.push( { 'type': '/internalList' } );
        }
        // After conversion we no longer need the HTML
-       this.itemsHtml = [];
+       this.itemHtmlQueue = [];
        return list;
+};
+
+/**
+ * Add a node.
+ * @method
+ * @param {string} groupName Item group
+ * @param {string} key Item name
+ * @param {ve.dm.Node} node Item node
+ */
+ve.dm.InternalList.prototype.addNode = function ( groupName, key, node ) {
+       var i, len, start, keyNodes, group = this.nodes[groupName];
+       // The group may not exist yet
+       if ( group === undefined ) {
+               group = this.nodes[groupName] = {
+                       keyNodes: {},
+                       keyOrder: []
+               };
+       }
+       keyNodes = group.keyNodes[key];
+       // The key may not exist yet
+       if ( keyNodes === undefined ) {
+               keyNodes = group.keyNodes[key] = [];
+       }
+
+       if ( ve.indexOf( key, group.keyOrder ) === -1 ) {
+               group.keyOrder.push( key );
+       }
+       if ( node.getDocument().buildingNodeTree ) {
+               // If the document is building the original node tree
+               // then every item is being added in order, so we don't
+               // need to worry about sorting.
+               keyNodes.push( node );
+       } else {
+               // TODO: We could use binary search insertion sort
+               start = node.getRange().start;
+               for ( i = 0, len = keyNodes.length; i < len; i++ ) {
+                       if ( start < keyNodes[i].getRange().start ) {
+                               break;
+                       }
+               }
+               // 'i' is now the insertion point, so add the node here
+               keyNodes.splice( i, 0, node );
+
+               this.sortGroupKeys( group );
+       }
+};
+
+/**
+ * Remove a node.
+ * @method
+ * @param {string} groupName Item group
+ * @param {string} key Item name
+ * @param {ve.dm.Node} node Item node
+ */
+ve.dm.InternalList.prototype.removeNode = function ( groupName, key, node ) {
+       var i, len, j,
+               group = this.nodes[groupName],
+               keyNodes = group.keyNodes[key];
+       for ( i = 0, len = keyNodes.length; i < len; i++ ) {
+               if ( keyNodes[i] === node ) {
+                       keyNodes.splice( i, 1 );
+                       if ( keyNodes.length === 0 ) {
+                               delete group.keyNodes[key];
+                               j = ve.indexOf( key, group.keyOrder );
+                               group.keyOrder.splice( j, 1 );
+                       } else {
+                               // If the key has been emptied then removing it 
from the keyOrder
+                               // array is sufficient to keep it ordered, 
otherwise we need to
+                               // re-sort the keys, as we don't know the 
position of the new
+                               // 0th item in the key
+                               this.sortGroupKeys( group );
+                       }
+                       break;
+               }
+       }
+};
+
+/**
+ * Sort the keyOrder array within a group object.
+ * @param {Object} group Group object
+ */
+ve.dm.InternalList.prototype.sortGroupKeys = function ( group ) {
+       // Sort keyOrder
+       group.keyOrder.sort( function ( key1, key2 ) {
+               return group.keyNodes[key1][0].getRange().start - 
group.keyNodes[key2][0].getRange().start;
+       } );
 };
 
 /**
@@ -165,11 +253,11 @@
  */
 ve.dm.InternalList.prototype.merge = function ( other ) {
        var i, len, index, storeMapping = this.getStore().merge( 
other.getStore() ), mapping = {};
-       for ( i = 0, len = other.itemsHtml.length; i < len; i++ ) {
-               other.itemsHtml[i] = storeMapping[other.itemsHtml[i]];
-               index = ve.indexOf( other.itemsHtml[i], this.itemsHtml );
+       for ( i = 0, len = other.itemHtmlQueue.length; i < len; i++ ) {
+               other.itemHtmlQueue[i] = storeMapping[other.itemHtmlQueue[i]];
+               index = ve.indexOf( other.itemHtmlQueue[i], this.itemHtmlQueue 
);
                if ( index === -1 ) {
-                       index = this.itemsHtml.push( other.itemsHtml[i] ) - 1;
+                       index = this.itemHtmlQueue.push( other.itemHtmlQueue[i] 
) - 1;
                }
                mapping[i] = index;
        }
diff --git a/modules/ve/test/dm/ve.dm.InternalList.test.js 
b/modules/ve/test/dm/ve.dm.InternalList.test.js
index 683195f..e655fcc 100644
--- a/modules/ve/test/dm/ve.dm.InternalList.test.js
+++ b/modules/ve/test/dm/ve.dm.InternalList.test.js
@@ -15,13 +15,13 @@
        assert.deepEqual( internalList.getDocument(), doc, 'Returns original 
document' );
 } );
 
-QUnit.test( 'queueItemHtml/getItemsHtml', 4, function ( assert ) {
+QUnit.test( 'queueItemHtml/getItemHtmlQueue', 4, function ( assert ) {
        var doc = ve.dm.example.createExampleDocument(),
                internalList = doc.getInternalList();
-       assert.equal( internalList.queueItemHtml( 'foo', 'Bar' ), 0, 'First 
queued item returns index 0' );
-       assert.equal( internalList.queueItemHtml( 'foo', 'Baz' ), 0, 'Duplicate 
key returns index 0' );
-       assert.equal( internalList.queueItemHtml( 'bar', 'Baz' ), 1, 'Second 
queued item returns index 1' );
-       assert.deepEqual( internalList.getItemsHtml(), ['Bar', 'Baz'], 
'getItemsHtml returns stored HTML items' );
+       assert.equal( internalList.queueItemHtml( 'reference', 'foo', 'Bar' ), 
0, 'First queued item returns index 0' );
+       assert.equal( internalList.queueItemHtml( 'reference', 'foo', 'Baz' ), 
0, 'Duplicate key returns index 0' );
+       assert.equal( internalList.queueItemHtml( 'reference', 'bar', 'Baz' ), 
1, 'Second queued item returns index 1' );
+       assert.deepEqual( internalList.getItemHtmlQueue(), ['Bar', 'Baz'], 
'getItemHtmlQueue returns stored HTML items' );
 } );
 
 QUnit.test( 'convertToData', 2, function ( assert ) {
@@ -49,10 +49,10 @@
        ve.dm.converter.internalList = internalList;
        ve.dm.converter.contextStack = [];
 
-       internalList.queueItemHtml( 'foo', 'Bar' );
-       internalList.queueItemHtml( 'bar', 'Baz' );
+       internalList.queueItemHtml( 'reference', 'foo', 'Bar' );
+       internalList.queueItemHtml( 'reference', 'bar', 'Baz' );
        assert.deepEqual( internalList.convertToData( ve.dm.converter ), 
expectedData, 'Data matches' );
-       assert.deepEqual( internalList.getItemsHtml(), [], 'Items html is 
emptied after conversion' );
+       assert.deepEqual( internalList.getItemHtmlQueue(), [], 'Items html is 
emptied after conversion' );
 } );
 
 QUnit.test( 'clone', 3, function ( assert ) {
@@ -67,3 +67,75 @@
 
        assert.equal( internalListClone2.getDocument(), doc2, 'Cloning with 
document parameter' );
 } );
+
+QUnit.test( 'addNode/removeNode', 4, function ( assert ) {
+       var doc = ve.dm.example.createExampleDocument(),
+               internalList = doc.getInternalList(),
+               expectedNodes = {
+                       'ref': {
+                               'keyNodes': {
+                                       'ref-1': [ 
doc.documentNode.children[0], doc.documentNode.children[2] ],
+                                       'ref-2': [ doc.documentNode.children[1] 
],
+                                       'ref-3': [ doc.documentNode.children[3] 
]
+                               },
+                               'keyOrder': [ 'ref-1', 'ref-2', 'ref-3' ]
+                       }
+               };
+
+       internalList.addNode( 'ref', 'ref-1', doc.documentNode.children[0] );
+       internalList.addNode( 'ref', 'ref-2', doc.documentNode.children[1] );
+       internalList.addNode( 'ref', 'ref-1', doc.documentNode.children[2] );
+       internalList.addNode( 'ref', 'ref-3', doc.documentNode.children[3] );
+
+       assert.deepEqualWithNodeTree(
+               internalList.nodes,
+               expectedNodes,
+               'Nodes added in order'
+       );
+
+       doc = ve.dm.example.createExampleDocument();
+       internalList = doc.getInternalList();
+
+       internalList.addNode( 'ref', 'ref-3', doc.documentNode.children[3] );
+       internalList.addNode( 'ref', 'ref-1', doc.documentNode.children[2] );
+       internalList.addNode( 'ref', 'ref-2', doc.documentNode.children[1] );
+       internalList.addNode( 'ref', 'ref-1', doc.documentNode.children[0] );
+
+       assert.deepEqualWithNodeTree(
+               internalList.nodes,
+               expectedNodes,
+               'Nodes added in reverse order'
+       );
+
+       internalList.removeNode( 'ref', 'ref-1', doc.documentNode.children[0] );
+
+       assert.deepEqualWithNodeTree(
+               internalList.nodes,
+               {
+                       'ref': {
+                               'keyNodes': {
+                                       'ref-2': [ doc.documentNode.children[1] 
],
+                                       'ref-1': [ doc.documentNode.children[2] 
],
+                                       'ref-3': [ doc.documentNode.children[3] 
]
+                               },
+                               'keyOrder': [ 'ref-2', 'ref-1', 'ref-3' ]
+                       }
+               },
+               'Keys re-ordered after one item of key removed'
+       );
+
+       internalList.removeNode( 'ref', 'ref-3', doc.documentNode.children[3] );
+       internalList.removeNode( 'ref', 'ref-1', doc.documentNode.children[2] );
+       internalList.removeNode( 'ref', 'ref-2', doc.documentNode.children[1] );
+
+       assert.deepEqualWithNodeTree(
+               internalList.nodes,
+               {
+                       'ref': {
+                               'keyNodes': {},
+                               'keyOrder': []
+                       }
+               },
+               'All nodes removed'
+       );
+} );
diff --git a/modules/ve/test/dm/ve.dm.Node.test.js 
b/modules/ve/test/dm/ve.dm.Node.test.js
index 2df25b5..a12229e 100644
--- a/modules/ve/test/dm/ve.dm.Node.test.js
+++ b/modules/ve/test/dm/ve.dm.Node.test.js
@@ -86,7 +86,7 @@
                node2 = new ve.dm.NodeStub();
        node1.attach( node2 );
        assert.strictEqual( node1.getParent(), node2 );
-       assert.strictEqual( node1.getRoot(), node2 );
+       assert.strictEqual( node1.getRoot(), null );
 } );
 
 QUnit.test( 'detach', 2, function ( assert ) {
@@ -95,7 +95,7 @@
        node1.attach( node2 );
        node1.detach();
        assert.strictEqual( node1.getParent(), null );
-       assert.strictEqual( node1.getRoot(), node1 );
+       assert.strictEqual( node1.getRoot(), null );
 } );
 
 QUnit.test( 'canBeMergedWith', 4, function ( assert ) {
diff --git a/modules/ve/test/dm/ve.dm.example.js 
b/modules/ve/test/dm/ve.dm.example.js
index 991e133..d3e8eae 100644
--- a/modules/ve/test/dm/ve.dm.example.js
+++ b/modules/ve/test/dm/ve.dm.example.js
@@ -1215,6 +1215,8 @@
                                'attributes': {
                                        'about': '#mwt5',
                                        'listIndex': 0,
+                                       'listGroup': 'mwReference',
+                                       'listKey': '#cite_note-bar-1',
                                        'mw': { 'body': { 'html': 'Bar' } }
                                },
                                'htmlAttributes': [
@@ -1247,6 +1249,8 @@
                                'attributes': {
                                        'about': '#mwt6',
                                        'listIndex': 1,
+                                       'listGroup': 'mwReference',
+                                       'listKey': '#cite_note-quux-2',
                                        'mw': { 'body': { 'html': 'Quux' } }
                                },
                                'htmlAttributes': [
@@ -1279,6 +1283,8 @@
                                'attributes': {
                                        'about': '#mwt7',
                                        'listIndex': 0,
+                                       'listGroup': 'mwReference',
+                                       'listKey': '#cite_note-bar-1',
                                        'mw': { 'body': { 'html': '' } }
                                },
                                'htmlAttributes': [
@@ -1311,6 +1317,8 @@
                                'attributes': {
                                        'about': '#mwt8',
                                        'listIndex': 2,
+                                       'listGroup': 'mwReference',
+                                       'listKey': '#cite_note-3',
                                        'mw': { 'body': { 'html': 'No name' } }
                                },
                                'htmlAttributes': [
diff --git a/modules/ve/test/ve.Node.test.js b/modules/ve/test/ve.Node.test.js
index 104a122..514e60a 100644
--- a/modules/ve/test/ve.Node.test.js
+++ b/modules/ve/test/ve.Node.test.js
@@ -32,5 +32,5 @@
 
 QUnit.test( 'getRoot', 1, function ( assert ) {
        var node = new ve.NodeStub();
-       assert.strictEqual( node.getRoot(), node );
+       assert.strictEqual( node.getRoot(), null );
 } );
diff --git a/modules/ve/test/ve.qunit.js b/modules/ve/test/ve.qunit.js
index aacba38..e1ef030 100644
--- a/modules/ve/test/ve.qunit.js
+++ b/modules/ve/test/ve.qunit.js
@@ -84,6 +84,20 @@
 }
 
 /**
+ * Callback for ve.copyArray/Object to convert nodes to a comparable summary
+ *
+ * @method
+ * @private
+ * @param {ve.dm.Node|Object} value Value in the object/array
+ * @returns {Object} Node summary if value is a node, otherwise just the value
+ */
+function convertNodes( value ) {
+       return value instanceof ve.dm.Node || value instanceof ve.ce.Node ?
+               getNodeTreeSummary( value ) :
+               value;
+}
+
+/**
  * Assertion helpers for VisualEditor test suite.
  * @class ve.QUnit.assert
  */
@@ -153,4 +167,17 @@
        QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
 };
 
+/**
+ * Assert that two objects which may contain dom elements are equal.
+ * @method
+ * @static
+ */
+QUnit.assert.deepEqualWithNodeTree = function ( actual, expected, message ) {
+       // Recursively copy objects or arrays, converting any dom elements 
found to comparable summaries
+       actual = ve.isArray( actual ) ? ve.copyArray( actual, convertNodes ) : 
ve.copyObject( actual, convertNodes );
+       expected = ve.isArray( expected ) ? ve.copyArray( expected, 
convertNodes ) : ve.copyObject( expected, convertNodes );
+
+       QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+};
+
 }( QUnit ) );
diff --git a/modules/ve/ve.Node.js b/modules/ve/ve.Node.js
index 7f5da4d..6cbab14 100644
--- a/modules/ve/ve.Node.js
+++ b/modules/ve/ve.Node.js
@@ -17,7 +17,7 @@
        // Properties
        this.type = this.constructor.static.name;
        this.parent = null;
-       this.root = this;
+       this.root = null;
        this.doc = null;
 };
 
@@ -29,6 +29,14 @@
 /**
  * @event detach
  * @param {ve.Node} parent
+ */
+
+/**
+ * @event root
+ */
+
+/**
+ * @event unroot
  */
 
 /* Abstract Methods */
@@ -132,9 +140,18 @@
  *
  * @method
  * @param {ve.Node} root Node to use as root
+ * @emits root
+ * @emits unroot
  */
 ve.Node.prototype.setRoot = function ( root ) {
-       this.root = root;
+       if ( root !== this.root ) {
+               this.root = root;
+               if ( this.getRoot() ) {
+                       this.emit( 'root' );
+               } else {
+                       this.emit( 'unroot' );
+               }
+       }
 };
 
 /**
@@ -182,8 +199,8 @@
 ve.Node.prototype.detach = function () {
        var parent = this.parent;
        this.parent = null;
-       this.setRoot( this );
-       this.setDocument();
+       this.setRoot( null );
+       this.setDocument( null );
        this.emit( 'detach', parent );
 };
 

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia3e90ccfdab88f352b89992b90554e5f03ff9952
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
Gerrit-Reviewer: Catrope <[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