jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/340160 )

Change subject: VisualDiff: Show attribute changes in a sidebar
......................................................................


VisualDiff: Show attribute changes in a sidebar

Bug: T151404
Bug: T156189
Change-Id: I846bfaa42c855c4d206aaa1ec64edf279581eafd
---
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
M i18n/en.json
M i18n/qqq.json
M src/dm/ve.dm.Model.js
M src/dm/ve.dm.VisualDiff.js
M src/ui/dialogs/ve.ui.DiffDialog.js
M src/ui/elements/ve.ui.DiffElement.js
M src/ui/styles/elements/ve.ui.DiffElement.css
A src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js
M tests/index.html
M tests/ui/ve.ui.DiffElement.test.js
13 files changed, 450 insertions(+), 38 deletions(-)

Approvals:
  Tchanders: Looks good to me, approved
  jenkins-bot: Verified
  Jforrester: Looks good to me, but someone else must approve



diff --git a/build/modules.json b/build/modules.json
index 243a87d..4eca486 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -608,7 +608,8 @@
                        "src/ve.DiffTreeNode.js",
                        "src/ve.DiffMatchPatch.js",
                        "src/dm/ve.dm.VisualDiff.js",
-                       "src/ui/elements/ve.ui.DiffElement.js"
+                       "src/ui/elements/ve.ui.DiffElement.js",
+                       "src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js"
                ],
                "styles": [
                        "src/ui/styles/elements/ve.ui.DiffElement.css"
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 7f1c718..f90c840 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -504,6 +504,7 @@
                <script src="../../src/ve.DiffMatchPatch.js"></script>
                <script src="../../src/dm/ve.dm.VisualDiff.js"></script>
                <script 
src="../../src/ui/elements/ve.ui.DiffElement.js"></script>
+               <script 
src="../../src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js"></script>
 
                <!-- visualEditor.diffing.standalone -->
                <script src="../../src/ui/dialogs/ve.ui.DiffDialog.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index 0836650..116eaab 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -505,6 +505,7 @@
                <script src="../../src/ve.DiffMatchPatch.js"></script>
                <script src="../../src/dm/ve.dm.VisualDiff.js"></script>
                <script 
src="../../src/ui/elements/ve.ui.DiffElement.js"></script>
+               <script 
src="../../src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js"></script>
 
                <!-- visualEditor.diffing.standalone -->
                <script src="../../src/ui/dialogs/ve.ui.DiffDialog.js"></script>
diff --git a/i18n/en.json b/i18n/en.json
index f709f40..0d14fff 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -34,6 +34,10 @@
        "visualeditor-annotationbutton-subscript-tooltip": "Subscript",
        "visualeditor-annotationbutton-superscript-tooltip": "Superscript",
        "visualeditor-annotationbutton-underline-tooltip": "Underline",
+       "visualeditor-changedesc-changed": "$1 changed from $2 to $3",
+       "visualeditor-changedesc-set": "$1 set to $2",
+       "visualeditor-changedesc-unknown": "$1 changed",
+       "visualeditor-changedesc-unset": "$1 unset from $2",
        "visualeditor-clearbutton-tooltip": "Clear styling",
        "visualeditor-clipboard-copy": "Copy",
        "visualeditor-clipboard-cut": "Cut",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 7303ff6..f05b8dc 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -42,6 +42,10 @@
        "visualeditor-annotationbutton-subscript-tooltip": "Tooltip text for 
subscript button.\n{{Related|Visualeditor-annotationbutton}}",
        "visualeditor-annotationbutton-superscript-tooltip": "Tooltip text for 
superscript 
button.\n{{Related|Visualeditor-annotationbutton}}\n{{Identical|Superscript}}",
        "visualeditor-annotationbutton-underline-tooltip": "Tooltip text for 
underline 
button.\n{{Related|Visualeditor-annotationbutton}}\n{{Identical|Underline}}",
+       "visualeditor-changedesc-changed": "Fallback description of an 
attribute value change for visual diffing, if no specific i18n exists for that 
kind of change.\n\nValues:\n* $1 – Attribute name, like 'lang' or 'dir'\n* $2 – 
Previous attribute value, like 'ar' or 'rtl'\n* $3 – New attribute value, like 
'fa' or 'auto'",
+       "visualeditor-changedesc-set": "Fallback description of an attribute 
value being set for visual diffing, if no specific i18n exists for that kind of 
change.\n\nValues:\n* $1 – Attribute name, like 'lang' or 'dir'\n* $2 – New 
attribute value, like 'fa' or 'auto'",
+       "visualeditor-changedesc-unknown": "Fallback description of an 
attribute value change for visual diffing which can't be otherwise described, 
if no specific i18n exists for that kind of change.\n\nValues:\n* $1 – 
Attribute name, like 'lang' or 'dir'",
+       "visualeditor-changedesc-unset": "Fallback description of an attribute 
value being unset for visual diffing, if no specific i18n exists for that kind 
of change.\n\nValues:\n* $1 – Attribute name, like 'lang' or 'dir'\n* $2 – 
Previous attribute value, like 'ar' or 'rtl'",
        "visualeditor-clearbutton-tooltip": "Tooltip text for the clear styling 
button. This clears \"styling\" like bold or italics from the current 
selection, but not \"formatting\" like being a list, a heading or a table.",
        "visualeditor-clipboard-copy": "Label for copy 
command.\n{{Identical|Copy}}",
        "visualeditor-clipboard-cut": "Label for cut 
command.\n{{Identical|Cut}}",
diff --git a/src/dm/ve.dm.Model.js b/src/dm/ve.dm.Model.js
index ce7f73b..2199e7a 100644
--- a/src/dm/ve.dm.Model.js
+++ b/src/dm/ve.dm.Model.js
@@ -241,6 +241,43 @@
        return this.allowedRdfaTypes;
 };
 
+/**
+ * Describe attribute changes in the model
+ *
+ * @param {Object} attributeChanges Attribute changes, keyed list containing 
objects with from and to properties
+ * @return {string[]} Descriptions
+ */
+ve.dm.Model.static.describeChanges = function ( attributeChanges ) {
+       var key, change,
+               descriptions = [];
+       for ( key in attributeChanges ) {
+               change = this.describeChange( key, attributeChanges[ key ] );
+               if ( change ) {
+                       descriptions.push( change );
+               }
+       }
+       return descriptions;
+};
+
+/**
+ * Describe a single attribute change in the model
+ *
+ * @param {string} key Attribute key
+ * @param {Object} change Change object with from and to properties
+ * @return {string} Description
+ */
+ve.dm.Model.static.describeChange = function ( key, change ) {
+       if ( ( typeof change.from === 'object' && change.from !== null ) || ( 
typeof change.to === 'object' && change.to !== null ) ) {
+               return ve.msg( 'visualeditor-changedesc-unknown', key );
+       } else if ( change.from === undefined ) {
+               return ve.msg( 'visualeditor-changedesc-set', key, change.to );
+       } else if ( change.to === undefined ) {
+               return ve.msg( 'visualeditor-changedesc-unset', key, 
change.from );
+       } else {
+               return ve.msg( 'visualeditor-changedesc-changed', key, 
change.from, change.to );
+       }
+};
+
 /* Methods */
 
 /**
diff --git a/src/dm/ve.dm.VisualDiff.js b/src/dm/ve.dm.VisualDiff.js
index 3ad5eea..a2763c5 100644
--- a/src/dm/ve.dm.VisualDiff.js
+++ b/src/dm/ve.dm.VisualDiff.js
@@ -239,6 +239,8 @@
                newDocChildTree,
                removeLength,
                insertLength,
+               oldStore = this.oldDoc.getStore(),
+               newStore = this.newDoc.getStore(),
                diffLength = 0,
                keepLength = 0,
                diffInfo = [];
@@ -307,6 +309,7 @@
        function getCleanDiff( diff ) {
                var i, ilen, action, data, firstWordbreak, lastWordbreak,
                        start, end, aItem, bItem, aAction, bAction, aData, 
bData,
+                       aAnnotations, bAnnotations, annotationChanges,
                        previousData = null,
                        previousAction = null,
                        cleanDiff = [],
@@ -433,7 +436,7 @@
                        aAction = aItem[ 0 ];
                        bAction = bItem[ 0 ];
                        // If they have the same length content and they are a 
consecutive
-                       // remove and insert, and they have the same contentm 
then mark the
+                       // remove and insert, and they have the same content 
then mark the
                        // old one as a change-remove (-2) and the new one as a 
change-insert
                        // (2)
                        if (
@@ -441,8 +444,24 @@
                                ( ( aAction === -1 && bAction === 1 ) || ( 
aAction === 1 && bAction === -1 ) ) &&
                                aData.every( compareData )
                        ) {
-                               cleanDiff[ i ][ 0 ] = aAction === -1 ? -2 : 2;
-                               cleanDiff[ i + 1 ][ 0 ] = bAction === -1 ? -2 : 
2;
+                               aAnnotations = new ve.dm.ElementLinearData( 
oldStore, aData ).getAnnotationsFromRange( new ve.Range( 0, aData.length ), 
true );
+                               bAnnotations = new ve.dm.ElementLinearData( 
newStore, bData ).getAnnotationsFromRange( new ve.Range( 0, bData.length ), 
true );
+
+                               annotationChanges = [];
+                               bAnnotations.get().forEach( function ( b ) { // 
eslint-disable-line no-loop-func
+                                       var sameName = 
aAnnotations.getAnnotationsByName( b.name );
+                                       if ( sameName.getLength() && 
!aAnnotations.containsComparable( b ) ) {
+                                               // Annotations which have the 
same type, but are non-comparable, e.g. link with a different href
+                                               annotationChanges.push( { 
oldAnnotation: sameName.get( 0 ), newAnnotation: b } );
+                                       }
+                               } );
+
+                               if ( annotationChanges.length ) {
+                                       cleanDiff[ i + 1 ].annotationChanges = 
annotationChanges;
+                                       cleanDiff[ i ][ 0 ] = aAction === -1 ? 
-2 : 2;
+                                       cleanDiff[ i + 1 ][ 0 ] = bAction === 
-1 ? -2 : 2;
+                               }
+
                                // No need to check bItem against the following 
item
                                i += 1;
                        }
@@ -478,7 +497,12 @@
                                // There is no content change
                                diffInfo[ i ] = {
                                        typeChange: oldNode.type !== 
newNode.type,
-                                       attributeChange: !ve.compare( 
oldNode.getAttributes(), newNode.getAttributes() )
+                                       attributeChange: !ve.compare( 
oldNode.getAttributes(), newNode.getAttributes() ) ?
+                                       {
+                                               oldAttributes: 
oldNode.getAttributes(),
+                                               newAttributes: 
newNode.getAttributes()
+                                       } :
+                                       false
                                };
                                continue;
 
@@ -512,7 +536,12 @@
                                diffInfo[ i ] = {
                                        linearDiff: linearDiff,
                                        typeChange: oldNode.type !== 
newNode.type,
-                                       attributeChange: !ve.compare( 
oldNode.getAttributes(), newNode.getAttributes() )
+                                       attributeChange: !ve.compare( 
oldNode.getAttributes(), newNode.getAttributes() ) ?
+                                       {
+                                               oldAttributes: 
oldNode.getAttributes(),
+                                               newAttributes: 
newNode.getAttributes()
+                                       } :
+                                       false
                                };
 
                                if ( linearDiff ) {
diff --git a/src/ui/dialogs/ve.ui.DiffDialog.js 
b/src/ui/dialogs/ve.ui.DiffDialog.js
index 8bd3dd1..e44669b 100644
--- a/src/ui/dialogs/ve.ui.DiffDialog.js
+++ b/src/ui/dialogs/ve.ui.DiffDialog.js
@@ -46,6 +46,8 @@
        // Parent method
        ve.ui.DiffDialog.parent.prototype.initialize.apply( this, arguments );
 
+       this.diffElement = null;
+
        this.content = new OO.ui.PanelLayout( {
                padded: true,
                expanded: false
@@ -60,17 +62,41 @@
 ve.ui.DiffDialog.prototype.getSetupProcess = function ( data ) {
        return ve.ui.DiffDialog.super.prototype.getSetupProcess.call( this, 
data )
                .next( function () {
+                       this.diffElement = new ve.ui.DiffElement( new 
ve.dm.VisualDiff( data.oldDoc, data.newDoc ) );
 
-                       var visualDiff, diffElement;
-                       visualDiff = new ve.dm.VisualDiff( data.oldDoc, 
data.newDoc );
-                       diffElement = new ve.ui.DiffElement( visualDiff );
-
-                       this.content.$element.empty().append(
-                               diffElement.$element
+                       this.content.$element.append(
+                               this.diffElement.$element
                        );
                }, this );
 };
 
+/**
+ * @inheritdoc
+ */
+ve.ui.DiffDialog.prototype.getReadyProcess = function ( data ) {
+       return ve.ui.DiffDialog.super.prototype.getReadyProcess.call( this, 
data )
+               .next( function () {
+                       var dialog = this;
+                       setTimeout( function () {
+                               dialog.withoutSizeTransitions( function () {
+                                       
dialog.diffElement.positionDescriptions();
+                                       dialog.updateSize();
+                               } );
+                       }, OO.ui.theme.getDialogTransitionDuration() );
+               }, this );
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.DiffDialog.prototype.getTeardownProcess = function ( data ) {
+       return ve.ui.DiffDialog.super.prototype.getTeardownProcess.call( this, 
data )
+               .next( function () {
+                       this.diffElement.destroy();
+                       this.diffElement.$element.remove();
+               }, this );
+};
+
 /* Registration */
 
 ve.ui.windowFactory.register( ve.ui.DiffDialog );
diff --git a/src/ui/elements/ve.ui.DiffElement.js 
b/src/ui/elements/ve.ui.DiffElement.js
index 5080673..4e60ab9 100644
--- a/src/ui/elements/ve.ui.DiffElement.js
+++ b/src/ui/elements/ve.ui.DiffElement.js
@@ -20,8 +20,7 @@
        // Parent constructor
        ve.ui.DiffElement.super.call( this );
 
-       // CSS
-       this.$element.addClass( 've-ui-diffElement' );
+       this.elementId = 0;
 
        // Documents
        this.oldDoc = visualDiff.oldDoc;
@@ -35,22 +34,170 @@
        this.insert = diff.docChildrenInsert;
        this.remove = diff.docChildrenRemove;
 
-       // HTML
+       this.$overlays = $( '<div>' ).addClass( 've-ui-diffElement-overlays' );
+       this.$content = $( '<div>' ).addClass( 've-ui-diffElement-content' );
+       this.$document = $( '<div>' ).addClass( 've-ui-diffElement-document' );
+       this.$sidebar = $( '<div>' ).addClass( 've-ui-diffElement-sidebar' );
+
+       this.descriptions = new ve.ui.ChangeDescriptionsSelectWidget();
+       this.descriptions.connect( this, { highlight: 'onDescriptionsHighlight' 
} );
+       this.descriptionItemsStack = [];
+       this.onWindowResizeDebounced = ve.debounce( this.onWindowResize.bind( 
this ), 250 );
+       $( this.getElementWindow() ).on( 'resize', this.onWindowResizeDebounced 
);
+
+       this.$document.on( {
+               mousemove: this.onDocumentMouseMove.bind( this )
+       } );
+
+       // DOM
+       this.$element
+               .append(
+                       this.$overlays,
+                       this.$content.append( this.$document ),
+                       this.$sidebar.append( this.descriptions.$element )
+               )
+               .addClass( 've-ui-diffElement' );
        this.renderDiff();
+       this.$element.toggleClass( 've-ui-diffElement-hasDescriptions', 
!this.descriptions.isEmpty() );
 };
 
 /* Inheritance */
 
 OO.inheritClass( ve.ui.DiffElement, OO.ui.Element );
 
+/* Static methods */
+
+/**
+ * Compare attribute sets between two elements
+ *
+ * @param {Object} oldAttributes Old attributes
+ * @param {Object} newAttributes New attributes
+ * @return {Object} Keyed set of attributes
+ */
+ve.ui.DiffElement.static.compareAttributes = function ( oldAttributes, 
newAttributes ) {
+       var key,
+               attributeChanges = {};
+
+       function compareKeys( a, b ) {
+               if ( typeof a === 'object' && typeof b === 'object' ) {
+                       return ve.compare( a, b );
+               } else {
+                       return a === b;
+               }
+       }
+
+       for ( key in oldAttributes ) {
+               if ( !compareKeys( oldAttributes[ key ], newAttributes[ key ] ) 
) {
+                       attributeChanges[ key ] = { from: oldAttributes[ key ], 
to: newAttributes[ key ] };
+               }
+       }
+       for ( key in newAttributes ) {
+               if ( !oldAttributes.hasOwnProperty( key ) && newAttributes[ key 
] !== undefined ) {
+                       attributeChanges[ key ] = { from: oldAttributes[ key ], 
to: newAttributes[ key ] };
+               }
+       }
+       return attributeChanges;
+};
+
 /* Methods */
+
+/**
+ * Get a diff element in the document from its elementId
+ *
+ * @param {number} elementId ID
+ * @return {jQuery} Element
+ */
+ve.ui.DiffElement.prototype.getDiffElementById = function ( elementId ) {
+       return this.$document.find( '[data-diff-id=' + elementId + ']' );
+};
+
+/**
+ * Handle description item hightlight events
+ *
+ * @param {OO.ui.OptionWidget} item Description item
+ */
+ve.ui.DiffElement.prototype.onDescriptionsHighlight = function ( item ) {
+       var i, l, elementRects, overlayRect;
+       if ( this.lastItem ) {
+               this.getDiffElementById( this.lastItem.getData() ).css( 
'outline', '' );
+               this.$overlays.empty();
+       }
+       if ( item ) {
+               overlayRect = this.$overlays[ 0 ].getBoundingClientRect();
+               elementRects = ve.ce.FocusableNode.static.getRectsForElement( 
this.getDiffElementById( item.getData() ), overlayRect ).rects;
+               for ( i = 0, l = elementRects.length; i < l; i++ ) {
+                       this.$overlays.append(
+                               $( '<div>' ).addClass( 
've-ui-diffElement-highlight' ).css( {
+                                       top: elementRects[ i ].top,
+                                       left: elementRects[ i ].left,
+                                       width: elementRects[ i ].width,
+                                       height: elementRects[ i ].height
+                               } )
+                       );
+               }
+               this.lastItem = item;
+       }
+};
+
+/**
+ * Handle document mouse move events
+ *
+ * @param {jQuery.Event} e Mouse move event
+ */
+ve.ui.DiffElement.prototype.onDocumentMouseMove = function ( e ) {
+       var elementId = $( e.target ).closest( '[data-diff-id]' ).attr( 
'data-diff-id' );
+       if ( elementId !== undefined ) {
+               this.descriptions.highlightItem(
+                       this.descriptions.getItemFromData( +elementId )
+               );
+       } else {
+               this.descriptions.highlightItem();
+       }
+};
+
+/**
+ * Handle window resize events
+ *
+ * @param {jQuery.Event} event Window resize event
+ */
+ve.ui.DiffElement.prototype.onWindowResize = function () {
+       this.positionDescriptions();
+};
+
+/**
+ * Reposition the description items so they are not above their position in 
the document
+ */
+ve.ui.DiffElement.prototype.positionDescriptions = function () {
+       var diffElement = this;
+       this.descriptions.getItems().forEach( function ( item ) {
+               var elementRect, itemRect;
+
+               item.$element.css( 'margin-top', '' );
+
+               itemRect = item.$element[ 0 ].getBoundingClientRect();
+               elementRect = diffElement.getDiffElementById( item.getData() )[ 
0 ].getBoundingClientRect();
+
+               if ( elementRect.top > itemRect.top ) {
+                       item.$element.css( 'margin-top', elementRect.top - 
itemRect.top - 5 );
+               }
+
+       } );
+       this.$document.css( 'min-height', this.$sidebar.height() );
+};
+
+/**
+ * Destroy the diff and remove global handlers
+ */
+ve.ui.DiffElement.prototype.destroy = function () {
+       $( this.getElementWindow() ).off( 'resize', 
this.onWindowResizeDebounced );
+};
 
 /**
  * Render the diff
  */
 ve.ui.DiffElement.prototype.renderDiff = function () {
        var i, j, k, ilen, jlen, klen, nodes, move, elements, spacerNode, 
noChanges,
-               element = this.$element[ 0 ],
+               documentNode = this.$document[ 0 ],
                anyChanges = false,
                spacer = false,
                diffQueue = [];
@@ -126,14 +273,14 @@
                        anyChanges = true;
                        elements = this[ diffQueue[ i ][ 0 ] ].apply( this, 
diffQueue[ i ].slice( 1 ) );
                        while ( elements.length ) {
-                               element.appendChild(
-                                       element.ownerDocument.adoptNode( 
elements[ 0 ] )
+                               documentNode.appendChild(
+                                       documentNode.ownerDocument.adoptNode( 
elements[ 0 ] )
                                );
                                elements.shift();
                        }
                } else if ( !spacer ) {
                        spacer = true;
-                       element.appendChild( spacerNode.cloneNode( true ) );
+                       documentNode.appendChild( spacerNode.cloneNode( true ) 
);
                }
        }
 
@@ -141,8 +288,8 @@
                noChanges = document.createElement( 'div' );
                noChanges.setAttribute( 'class', 've-ui-diffElement-no-changes' 
);
                noChanges.appendChild( document.createTextNode( ve.msg( 
'visualeditor-diff-no-changes' ) ) );
-               element.innerHTML = '';
-               element.appendChild( noChanges );
+               documentNode.innerHTML = '';
+               documentNode.appendChild( noChanges );
        }
 };
 
@@ -358,7 +505,7 @@
         * @param {Object} diffInfo Information relating to this node's change
         */
        function highlightChangedNode( nodeIndex, diffInfo ) {
-               var node, nodeRangeStart, nodeDiffData, annotatedData;
+               var node, nodeRangeStart, nodeDiffData, annotatedData, item;
 
                // The new node was changed.
                // Get data for this node
@@ -376,10 +523,9 @@
                        nodeData[ nodeRangeStart ] = this.addAttributesToNode(
                                nodeData[ nodeRangeStart ], this.newDoc, { 
'data-diff-action': 'structural-change' }
                        );
+                       item = this.compareNodeAttributes( nodeData, 
nodeRangeStart, this.newDoc, diffInfo.attributeChange );
+                       this.descriptionItemsStack.unshift( item );
                }
-
-               // TODO: render type and attribute changes according to 
diffInfo.typeChange
-               // and diffInfo.attributeChange
        }
 
        // Iterate backwards over trees so that changes are made from right to 
left
@@ -438,6 +584,10 @@
                }
        }
 
+       // Push new description items from the queue
+       this.descriptions.addItems( this.descriptionItemsStack );
+       this.descriptionItemsStack = [];
+
        element = document.createElement( 'div' );
        element.setAttribute( 'class', 've-ui-diffElement-doc-child-change' );
        if ( move ) {
@@ -455,6 +605,48 @@
        }
 
        return [ element ];
+};
+
+/**
+ * Compare attributes of two nodes
+ *
+ * @param {Array} data Linear data containing new node
+ * @param {number} offset Offset in data
+ * @param {ve.dm.Document} doc Document model
+ * @param {Object} attributeChange Attribute change object containing 
oldAttributes and newAttributes
+ * @return {OO.ui.OptionWidget} Change description item
+ */
+ve.ui.DiffElement.prototype.compareNodeAttributes = function ( data, offset, 
doc, attributeChange ) {
+       var changes, item,
+               attributeChanges = this.constructor.static.compareAttributes( 
attributeChange.oldAttributes, attributeChange.newAttributes );
+
+       changes = ve.dm.modelRegistry.lookup( data[ offset ].type 
).static.describeChanges( attributeChanges, attributeChange.newAttributes );
+       item = this.getChangeDescriptionItem( changes );
+       data[ offset ] = this.addAttributesToNode( data[ offset ], doc, { 
'data-diff-id': item.getData() } );
+       return item;
+};
+
+/**
+ * Get a change description item from a set of changes
+ *
+ * @param {Object} changes Changes
+ * @return {OO.ui.OptionWidget} Change description item
+ */
+ve.ui.DiffElement.prototype.getChangeDescriptionItem = function ( changes ) {
+       var i, l, item,
+               elementId = this.elementId,
+               $label = $( [] );
+
+       for ( i = 0, l = changes.length; i < l; i++ ) {
+               $label = $label.add( $( '<div>' ).text( changes[ i ] ) );
+       }
+       item = new OO.ui.OptionWidget( {
+               label: $label,
+               data: elementId,
+               classes: [ 've-ui-diffElement-attributeChange' ]
+       } );
+       this.elementId++;
+       return item;
 };
 
 /**
@@ -476,13 +668,18 @@
 
        if ( node.originalDomElementsIndex ) {
                domElements = ve.copy( nodeDoc.getStore().value( 
node.originalDomElementsIndex ) );
-               domElements[ 0 ] = domElements[ 0 ].cloneNode( true );
+               domElements.map( function ( element ) {
+                       return element.cloneNode( true );
+               } );
        } else {
                domElements = [ document.createElement( 'span' ) ];
        }
        for ( key in attributes ) {
                if ( attributes[ key ] !== undefined ) {
-                       domElements[ 0 ].setAttribute( key, attributes[ key ] );
+                       // eslint-disable-next-line no-loop-func
+                       domElements.forEach( function ( element ) {
+                               element.setAttribute( key, attributes[ key ] );
+                       } );
                }
        }
        originalDomElementsIndex = this.newDoc.getStore().index(
@@ -501,11 +698,13 @@
  * @return {Array} Data with annotations added
  */
 ve.ui.DiffElement.prototype.annotateNode = function ( linearDiff ) {
-       var i, ilen, range, type, typeAsString, annType, domElementType,
+       var i, ilen, range, type, typeAsString, annType, domElementType, 
changes, item,
+               items = [],
                start = 0, // The starting index for a range for building an 
annotation
                end, transaction, annotatedLinearDiff,
                domElement, domElements, originalDomElementsIndex,
-               diffDoc, diffDocData;
+               diffDoc, diffDocData,
+               diffElement = this;
 
        // Make a new document from the diff
        diffDocData = linearDiff[ 0 ][ 1 ];
@@ -547,6 +746,28 @@
                                domElement = document.createElement( 
domElementType );
                                domElement.setAttribute( 'data-diff-action', 
typeAsString );
                                domElements = [ domElement ];
+
+                               if ( linearDiff[ i ].annotationChanges ) {
+                                       changes = [];
+                                       linearDiff[ i 
].annotationChanges.forEach( function ( annotationChange ) { // 
eslint-disable-line no-loop-func
+                                               var attributeChanges;
+                                               if ( 
annotationChange.oldAnnotation && annotationChange.newAnnotation ) {
+                                                       attributeChanges = 
diffElement.constructor.static.compareAttributes(
+                                                               
annotationChange.oldAnnotation.getAttributes(),
+                                                               
annotationChange.newAnnotation.getAttributes()
+                                                       );
+                                                       changes = 
changes.concat( ve.dm.modelRegistry.lookup( 
annotationChange.newAnnotation.getType() ).static.describeChanges(
+                                                               
attributeChanges, annotationChange.newAnnotation.getAttributes()
+                                                       ) );
+                                               }
+                                       } );
+                                       if ( changes.length ) {
+                                               item = 
diffElement.getChangeDescriptionItem( changes );
+                                               domElement.setAttribute( 
'data-diff-id', item.getData() );
+                                               items.push( item );
+                                       }
+                               }
+
                                originalDomElementsIndex = 
diffDoc.getStore().index(
                                        domElements,
                                        domElements.map( ve.getNodeHtml ).join( 
'' )
@@ -566,6 +787,7 @@
                }
                start = end;
        }
+       this.descriptionItemsStack.unshift.apply( this.descriptionItemsStack, 
items );
 
        // Merge the stores and get the data
        this.newDoc.getStore().merge( diffDoc.getStore() );
diff --git a/src/ui/styles/elements/ve.ui.DiffElement.css 
b/src/ui/styles/elements/ve.ui.DiffElement.css
index fd7de8f..eb032f0 100644
--- a/src/ui/styles/elements/ve.ui.DiffElement.css
+++ b/src/ui/styles/elements/ve.ui.DiffElement.css
@@ -6,7 +6,60 @@
  */
 
 .ve-ui-diffElement {
+       position: relative;
+}
+
+.ve-ui-diffElement-content {
        margin-left: 15px;
+}
+
+.ve-ui-diffElement-hasDescriptions .ve-ui-diffElement-content {
+       margin-right: 16em;
+       padding-right: 1em;
+       border-right: 1px solid #ccc;
+}
+
+.ve-ui-diffElement-hasDescriptions .ve-ui-diffElement-sidebar {
+       position: absolute;
+       top: 0;
+       right: 0;
+       width: 15em;
+}
+
+.ve-ui-diffElement-overlays {
+       position: absolute;
+       opacity: 0.5;
+       z-index: 1;
+       pointer-events: none;
+}
+
+.ve-ui-changeDescriptionsSelectWidget > .oo-ui-optionWidget {
+       cursor: default;
+}
+
+.ve-ui-changeDescriptionsSelectWidget > .oo-ui-optionWidget-highlighted {
+       background: #b6d4fb; /* #6da9f7 at 50% opacity */
+}
+
+.ve-ui-diffElement-highlight {
+       position: absolute;
+       /*background: #6da9f7;*/
+       outline: 3px solid #6da9f7;
+       padding: 2px;
+       margin: -2px 0 0 -2px;
+       /* TODO: Support IE9 with JS mouse events */
+       pointer-events: none; /* stylelint-disable-line 
no-unsupported-browser-features */
+}
+
+.ve-ui-diffElement-attributeChange {
+       color: #72777d;
+}
+
+.ve-ui-diffElement-attributeChange.oo-ui-labelElement 
.oo-ui-labelElement-label {
+       white-space: normal;
+       word-break: break-word;
+       word-wrap: break-word;
+       overflow-wrap: break-word;
 }
 
 [data-diff-action='insert'],
@@ -14,7 +67,6 @@
 [data-diff-action='change-insert'],
 [data-diff-action='change-remove'] {
        text-decoration: inherit;
-       border-radius: 0.15em;
 }
 
 [data-diff-action='insert'],
@@ -33,9 +85,10 @@
        box-shadow: 0 0 0 1px #e88e89;
 }
 
+[data-diff-action='change'],
 [data-diff-action='change-insert'] {
-       background-color: #8ab4e8 !important; /* stylelint-disable-line 
declaration-no-important */
-       box-shadow: 0 0 0 1px #8ab4e8;
+       background-color: #b6d4fb !important; /* stylelint-disable-line 
declaration-no-important */
+       box-shadow: 0 0 0 1px #b6d4fb;
 }
 
 [data-diff-action='change-remove'] {
@@ -58,7 +111,7 @@
 [data-diff-action='insert'] + [data-diff-action='remove'],
 [data-diff-action='remove'] + [data-diff-action='change-insert'],
 [data-diff-action='insert'] + [data-diff-action='change-remove'] {
-       margin-left: 2px;
+       margin-left: 4px;
 }
 
 [data-diff-action='none'] {
diff --git a/src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js 
b/src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js
new file mode 100644
index 0000000..d6e86cb
--- /dev/null
+++ b/src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js
@@ -0,0 +1,33 @@
+/*!
+ * VisualEditor UserInterface ChangeDescriptionsSelectWidget class.
+ *
+ * @copyright 2011-2017 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * Creates a ve.ui.ChangeDescriptionsSelectWidget object.
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ *
+ * @constructor
+ * @param {Object} config Configuration options
+ */
+ve.ui.ChangeDescriptionsSelectWidget = function 
VeUiChangeDescriptionsSelectWidget( config ) {
+       // Parent constructor
+       ve.ui.ChangeDescriptionsSelectWidget.super.call( this, config );
+
+       // DOM
+       this.$element.addClass( 've-ui-changeDescriptionsSelectWidget' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.ChangeDescriptionsSelectWidget, OO.ui.SelectWidget );
+
+/* Methods */
+
+ve.ui.ChangeDescriptionsSelectWidget.prototype.selectItem = function () {};
+
+ve.ui.ChangeDescriptionsSelectWidget.prototype.pressItem = function () {};
diff --git a/tests/index.html b/tests/index.html
index d2220f9..6f99cb9 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -423,6 +423,7 @@
                <script src="../src/ve.DiffMatchPatch.js"></script>
                <script src="../src/dm/ve.dm.VisualDiff.js"></script>
                <script src="../src/ui/elements/ve.ui.DiffElement.js"></script>
+               <script 
src="../src/ui/widgets/ve.ui.ChangeDescriptionsSelectWidget.js"></script>
 
                <!-- visualEditor.diffing.standalone -->
                <script src="../src/ui/dialogs/ve.ui.DiffDialog.js"></script>
diff --git a/tests/ui/ve.ui.DiffElement.test.js 
b/tests/ui/ve.ui.DiffElement.test.js
index 73f6baf..2825a3e 100644
--- a/tests/ui/ve.ui.DiffElement.test.js
+++ b/tests/ui/ve.ui.DiffElement.test.js
@@ -90,7 +90,7 @@
                                newDoc: '<figure><img 
src="boo.jpg"><figcaption>bar</figcaption></figure>',
                                expected:
                                        '<div 
class="ve-ui-diffElement-doc-child-change">' +
-                                               '<figure 
data-diff-action="structural-change"><img src="boo.jpg" width="0" height="0" 
alt="null"><figcaption>bar</figcaption></figure>' +
+                                               '<figure 
data-diff-action="structural-change" data-diff-id="0"><img src="boo.jpg" 
width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' +
                                        '</div>'
                        },
                        {
@@ -99,7 +99,7 @@
                                newDoc: '<figure class="ve-align-right"><img 
src="boo.jpg"><figcaption>bar</figcaption></figure>',
                                expected:
                                        '<div 
class="ve-ui-diffElement-doc-child-change">' +
-                                               '<figure class="ve-align-right" 
data-diff-action="structural-change"><img src="boo.jpg" width="0" height="0" 
alt="null"><figcaption>bar</figcaption></figure>' +
+                                               '<figure class="ve-align-right" 
data-diff-action="structural-change" data-diff-id="0"><img src="boo.jpg" 
width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' +
                                        '</div>'
                        },
                        {
@@ -210,7 +210,7 @@
                                newDoc: '<p>foo <b>bar</b> baz</p>',
                                expected:
                                        '<div 
class="ve-ui-diffElement-doc-child-change">' +
-                                               '<p>foo <span 
data-diff-action="change-remove">bar</span><b><span 
data-diff-action="change-insert">bar</span></b> baz</p>' +
+                                               '<p>foo <del 
data-diff-action="remove">bar</del><b><ins 
data-diff-action="insert">bar</ins></b> baz</p>' +
                                        '</div>'
                        },
                        {
@@ -219,7 +219,7 @@
                                newDoc: '<p>foo bar baz</p>',
                                expected:
                                        '<div 
class="ve-ui-diffElement-doc-child-change">' +
-                                               '<p>foo <b><span 
data-diff-action="change-remove">bar</span></b><span 
data-diff-action="change-insert">bar</span> baz</p>' +
+                                               '<p>foo <b><del 
data-diff-action="remove">bar</del></b><ins data-diff-action="insert">bar</ins> 
baz</p>' +
                                        '</div>'
                        },
                        {
@@ -325,6 +325,6 @@
                newDoc.getStore().merge( oldDoc.getStore() );
                visualDiff = new ve.dm.VisualDiff( oldDoc, newDoc );
                diffElement = new ve.ui.DiffElement( visualDiff );
-               assert.strictEqual( diffElement.$element.html(), cases[ i 
].expected, cases[ i ].msg );
+               assert.strictEqual( diffElement.$document.html(), cases[ i 
].expected, cases[ i ].msg );
        }
 } );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I846bfaa42c855c4d206aaa1ec64edf279581eafd
Gerrit-PatchSet: 18
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <esand...@wikimedia.org>
Gerrit-Reviewer: Esanders <esand...@wikimedia.org>
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: Tchanders <thalia.e.c...@googlemail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to