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